balibabu
commited on
Commit
·
f36a767
1
Parent(s):
9640d9a
feat: Show task_executor heartbeat #3409 (#3461)
Browse files### What problem does this PR solve?
feat: Show task_executor heartbeat #3409
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
web/src/interfaces/database/user-setting.ts
CHANGED
|
@@ -22,16 +22,23 @@ export interface IUserInfo {
|
|
| 22 |
|
| 23 |
export type TaskExecutorElapsed = Record<string, number[]>;
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
export interface ISystemStatus {
|
| 26 |
es: Es;
|
| 27 |
storage: Storage;
|
| 28 |
database: Database;
|
| 29 |
redis: Redis;
|
| 30 |
-
|
| 31 |
-
error?: string;
|
| 32 |
-
status: string;
|
| 33 |
-
elapsed?: TaskExecutorElapsed;
|
| 34 |
-
};
|
| 35 |
}
|
| 36 |
|
| 37 |
interface Redis {
|
|
|
|
| 22 |
|
| 23 |
export type TaskExecutorElapsed = Record<string, number[]>;
|
| 24 |
|
| 25 |
+
export interface TaskExecutorHeartbeatItem {
|
| 26 |
+
boot_at: string;
|
| 27 |
+
current: null;
|
| 28 |
+
done: number;
|
| 29 |
+
failed: number;
|
| 30 |
+
lag: number;
|
| 31 |
+
name: string;
|
| 32 |
+
now: string;
|
| 33 |
+
pending: number;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
export interface ISystemStatus {
|
| 37 |
es: Es;
|
| 38 |
storage: Storage;
|
| 39 |
database: Database;
|
| 40 |
redis: Redis;
|
| 41 |
+
task_executor_heartbeat: Record<string, TaskExecutorHeartbeatItem[]>;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
interface Redis {
|
web/src/pages/user-setting/setting-system/index.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import SvgIcon from '@/components/svg-icon';
|
|
| 2 |
import { useFetchSystemStatus } from '@/hooks/user-setting-hooks';
|
| 3 |
import {
|
| 4 |
ISystemStatus,
|
| 5 |
-
|
| 6 |
} from '@/interfaces/database/user-setting';
|
| 7 |
import { Badge, Card, Flex, Spin, Typography } from 'antd';
|
| 8 |
import classNames from 'classnames';
|
|
@@ -11,6 +11,7 @@ import upperFirst from 'lodash/upperFirst';
|
|
| 11 |
import { useEffect } from 'react';
|
| 12 |
|
| 13 |
import { toFixed } from '@/utils/common-util';
|
|
|
|
| 14 |
import styles from './index.less';
|
| 15 |
import TaskBarChat from './task-bar-chat';
|
| 16 |
|
|
@@ -27,7 +28,7 @@ const TitleMap = {
|
|
| 27 |
storage: 'Object Storage',
|
| 28 |
redis: 'Redis',
|
| 29 |
database: 'Database',
|
| 30 |
-
|
| 31 |
};
|
| 32 |
|
| 33 |
const IconMap = {
|
|
@@ -60,10 +61,13 @@ const SystemInfo = () => {
|
|
| 60 |
type="inner"
|
| 61 |
title={
|
| 62 |
<Flex align="center" gap={10}>
|
| 63 |
-
{key === '
|
| 64 |
<img src="/logo.svg" alt="" width={26} />
|
| 65 |
) : (
|
| 66 |
-
<SvgIcon
|
|
|
|
|
|
|
|
|
|
| 67 |
)}
|
| 68 |
<span className={styles.title}>
|
| 69 |
{TitleMap[key as keyof typeof TitleMap]}
|
|
@@ -76,13 +80,15 @@ const SystemInfo = () => {
|
|
| 76 |
}
|
| 77 |
key={key}
|
| 78 |
>
|
| 79 |
-
{key === '
|
| 80 |
-
info
|
| 81 |
<TaskBarChat
|
| 82 |
-
data={info
|
| 83 |
></TaskBarChat>
|
| 84 |
) : (
|
| 85 |
-
<Text className={styles.error}>
|
|
|
|
|
|
|
| 86 |
)
|
| 87 |
) : (
|
| 88 |
Object.keys(info)
|
|
|
|
| 2 |
import { useFetchSystemStatus } from '@/hooks/user-setting-hooks';
|
| 3 |
import {
|
| 4 |
ISystemStatus,
|
| 5 |
+
TaskExecutorHeartbeatItem,
|
| 6 |
} from '@/interfaces/database/user-setting';
|
| 7 |
import { Badge, Card, Flex, Spin, Typography } from 'antd';
|
| 8 |
import classNames from 'classnames';
|
|
|
|
| 11 |
import { useEffect } from 'react';
|
| 12 |
|
| 13 |
import { toFixed } from '@/utils/common-util';
|
| 14 |
+
import { isObject } from 'lodash';
|
| 15 |
import styles from './index.less';
|
| 16 |
import TaskBarChat from './task-bar-chat';
|
| 17 |
|
|
|
|
| 28 |
storage: 'Object Storage',
|
| 29 |
redis: 'Redis',
|
| 30 |
database: 'Database',
|
| 31 |
+
task_executor_heartbeats: 'Task Executor',
|
| 32 |
};
|
| 33 |
|
| 34 |
const IconMap = {
|
|
|
|
| 61 |
type="inner"
|
| 62 |
title={
|
| 63 |
<Flex align="center" gap={10}>
|
| 64 |
+
{key === 'task_executor_heartbeats' ? (
|
| 65 |
<img src="/logo.svg" alt="" width={26} />
|
| 66 |
) : (
|
| 67 |
+
<SvgIcon
|
| 68 |
+
name={IconMap[key as keyof typeof IconMap]}
|
| 69 |
+
width={26}
|
| 70 |
+
></SvgIcon>
|
| 71 |
)}
|
| 72 |
<span className={styles.title}>
|
| 73 |
{TitleMap[key as keyof typeof TitleMap]}
|
|
|
|
| 80 |
}
|
| 81 |
key={key}
|
| 82 |
>
|
| 83 |
+
{key === 'task_executor_heartbeats' ? (
|
| 84 |
+
isObject(info) ? (
|
| 85 |
<TaskBarChat
|
| 86 |
+
data={info as Record<string, TaskExecutorHeartbeatItem[]>}
|
| 87 |
></TaskBarChat>
|
| 88 |
) : (
|
| 89 |
+
<Text className={styles.error}>
|
| 90 |
+
{typeof info.error === 'string' ? info.error : ''}
|
| 91 |
+
</Text>
|
| 92 |
)
|
| 93 |
) : (
|
| 94 |
Object.keys(info)
|
web/src/pages/user-setting/setting-system/task-bar-chat.tsx
CHANGED
|
@@ -1,57 +1,47 @@
|
|
| 1 |
-
import {
|
| 2 |
import { Divider, Flex } from 'antd';
|
| 3 |
-
import { max } from 'lodash';
|
| 4 |
import {
|
| 5 |
Bar,
|
| 6 |
BarChart,
|
| 7 |
CartesianGrid,
|
|
|
|
|
|
|
| 8 |
ResponsiveContainer,
|
| 9 |
Tooltip,
|
|
|
|
| 10 |
} from 'recharts';
|
| 11 |
|
|
|
|
|
|
|
|
|
|
| 12 |
import styles from './index.less';
|
| 13 |
|
| 14 |
interface IProps {
|
| 15 |
-
data:
|
| 16 |
}
|
| 17 |
|
| 18 |
-
const
|
| 19 |
-
if (value > 120) {
|
| 20 |
-
return 'red';
|
| 21 |
-
} else if (value <= 120 && value > 50) {
|
| 22 |
-
return '#faad14';
|
| 23 |
-
}
|
| 24 |
-
return '#52c41a';
|
| 25 |
-
};
|
| 26 |
-
|
| 27 |
-
const getMaxLength = (data: TaskExecutorElapsed) => {
|
| 28 |
-
const lengths = Object.keys(data).reduce<number[]>((pre, cur) => {
|
| 29 |
-
pre.push(data[cur].length);
|
| 30 |
-
return pre;
|
| 31 |
-
}, []);
|
| 32 |
-
return max(lengths) ?? 0;
|
| 33 |
-
};
|
| 34 |
-
|
| 35 |
-
const fillEmptyElementByMaxLength = (list: any[], maxLength: number) => {
|
| 36 |
-
if (list.length === maxLength) {
|
| 37 |
-
return list;
|
| 38 |
-
}
|
| 39 |
-
return list.concat(
|
| 40 |
-
new Array(maxLength - list.length).fill({
|
| 41 |
-
value: 0,
|
| 42 |
-
actualValue: 0,
|
| 43 |
-
fill: getColor(0),
|
| 44 |
-
}),
|
| 45 |
-
);
|
| 46 |
-
};
|
| 47 |
-
|
| 48 |
-
const CustomTooltip = ({ active, payload }: any) => {
|
| 49 |
if (active && payload && payload.length) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
return (
|
| 51 |
-
<div className="
|
| 52 |
-
<
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
</div>
|
| 56 |
);
|
| 57 |
}
|
|
@@ -60,32 +50,56 @@ const CustomTooltip = ({ active, payload }: any) => {
|
|
| 60 |
};
|
| 61 |
|
| 62 |
const TaskBarChat = ({ data }: IProps) => {
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
<
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
};
|
| 90 |
|
| 91 |
export default TaskBarChat;
|
|
|
|
| 1 |
+
import { TaskExecutorHeartbeatItem } from '@/interfaces/database/user-setting';
|
| 2 |
import { Divider, Flex } from 'antd';
|
|
|
|
| 3 |
import {
|
| 4 |
Bar,
|
| 5 |
BarChart,
|
| 6 |
CartesianGrid,
|
| 7 |
+
Legend,
|
| 8 |
+
Rectangle,
|
| 9 |
ResponsiveContainer,
|
| 10 |
Tooltip,
|
| 11 |
+
XAxis,
|
| 12 |
} from 'recharts';
|
| 13 |
|
| 14 |
+
import { formatDate, formatTime } from '@/utils/date';
|
| 15 |
+
import dayjs from 'dayjs';
|
| 16 |
+
import { get } from 'lodash';
|
| 17 |
import styles from './index.less';
|
| 18 |
|
| 19 |
interface IProps {
|
| 20 |
+
data: Record<string, TaskExecutorHeartbeatItem[]>;
|
| 21 |
}
|
| 22 |
|
| 23 |
+
const CustomTooltip = ({ active, payload, ...restProps }: any) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
if (active && payload && payload.length) {
|
| 25 |
+
const taskExecutorHeartbeatItem: TaskExecutorHeartbeatItem = get(
|
| 26 |
+
payload,
|
| 27 |
+
'0.payload',
|
| 28 |
+
{},
|
| 29 |
+
);
|
| 30 |
return (
|
| 31 |
+
<div className="bg-slate-50 p-2 rounded-md border border-indigo-100">
|
| 32 |
+
<div className="font-semibold text-lg">
|
| 33 |
+
{formatDate(restProps.label)}
|
| 34 |
+
</div>
|
| 35 |
+
{Object.entries(taskExecutorHeartbeatItem).map(([key, val], index) => {
|
| 36 |
+
return (
|
| 37 |
+
<div key={index} className="flex gap-1">
|
| 38 |
+
<span className="font-semibold">{`${key}: `}</span>
|
| 39 |
+
<span>
|
| 40 |
+
{key === 'now' || key === 'boot_at' ? formatDate(val) : val}
|
| 41 |
+
</span>
|
| 42 |
+
</div>
|
| 43 |
+
);
|
| 44 |
+
})}
|
| 45 |
</div>
|
| 46 |
);
|
| 47 |
}
|
|
|
|
| 50 |
};
|
| 51 |
|
| 52 |
const TaskBarChat = ({ data }: IProps) => {
|
| 53 |
+
return Object.entries(data).map(([key, val]) => {
|
| 54 |
+
const data = val.map((x) => ({
|
| 55 |
+
...x,
|
| 56 |
+
now: dayjs(x.now).valueOf(),
|
| 57 |
+
failed: 5,
|
| 58 |
+
}));
|
| 59 |
+
const firstItem = data[0];
|
| 60 |
+
const lastItem = data[data.length - 1];
|
| 61 |
+
|
| 62 |
+
const domain = [firstItem.now, lastItem.now];
|
| 63 |
+
|
| 64 |
+
return (
|
| 65 |
+
<Flex key={key} className={styles.taskBar} vertical>
|
| 66 |
+
<div className="flex gap-8">
|
| 67 |
+
<b className={styles.taskBarTitle}>ID: {key}</b>
|
| 68 |
+
<b className={styles.taskBarTitle}>Lag: {lastItem.lag}</b>
|
| 69 |
+
<b className={styles.taskBarTitle}>Pending: {lastItem.pending}</b>
|
| 70 |
+
</div>
|
| 71 |
+
<ResponsiveContainer>
|
| 72 |
+
<BarChart data={data} barSize={20}>
|
| 73 |
+
<XAxis
|
| 74 |
+
dataKey="now"
|
| 75 |
+
type="number"
|
| 76 |
+
scale={'time'}
|
| 77 |
+
domain={domain}
|
| 78 |
+
tickFormatter={(x) => formatTime(x)}
|
| 79 |
+
allowDataOverflow
|
| 80 |
+
angle={60}
|
| 81 |
+
padding={{ left: 20, right: 20 }}
|
| 82 |
+
tickMargin={20}
|
| 83 |
+
/>
|
| 84 |
+
<CartesianGrid strokeDasharray="3 3" />
|
| 85 |
+
<Tooltip content={<CustomTooltip></CustomTooltip>} />
|
| 86 |
+
<Legend wrapperStyle={{ bottom: -22 }} />
|
| 87 |
+
<Bar
|
| 88 |
+
dataKey="done"
|
| 89 |
+
fill="#8884d8"
|
| 90 |
+
activeBar={<Rectangle fill="pink" stroke="blue" />}
|
| 91 |
+
/>
|
| 92 |
+
<Bar
|
| 93 |
+
dataKey="failed"
|
| 94 |
+
fill="#82ca9d"
|
| 95 |
+
activeBar={<Rectangle fill="gold" stroke="purple" />}
|
| 96 |
+
/>
|
| 97 |
+
</BarChart>
|
| 98 |
+
</ResponsiveContainer>
|
| 99 |
+
<Divider></Divider>
|
| 100 |
+
</Flex>
|
| 101 |
+
);
|
| 102 |
+
});
|
| 103 |
};
|
| 104 |
|
| 105 |
export default TaskBarChat;
|
web/src/utils/date.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
| 1 |
import dayjs from 'dayjs';
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
export function today() {
|
| 4 |
return formatDate(dayjs());
|
| 5 |
}
|
|
@@ -11,10 +25,3 @@ export function lastDay() {
|
|
| 11 |
export function lastWeek() {
|
| 12 |
return formatDate(dayjs().subtract(1, 'weeks'));
|
| 13 |
}
|
| 14 |
-
|
| 15 |
-
export function formatDate(date: any) {
|
| 16 |
-
if (!date) {
|
| 17 |
-
return '';
|
| 18 |
-
}
|
| 19 |
-
return dayjs(date).format('DD/MM/YYYY HH:mm:ss');
|
| 20 |
-
}
|
|
|
|
| 1 |
import dayjs from 'dayjs';
|
| 2 |
|
| 3 |
+
export function formatDate(date: any) {
|
| 4 |
+
if (!date) {
|
| 5 |
+
return '';
|
| 6 |
+
}
|
| 7 |
+
return dayjs(date).format('DD/MM/YYYY HH:mm:ss');
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function formatTime(date: any) {
|
| 11 |
+
if (!date) {
|
| 12 |
+
return '';
|
| 13 |
+
}
|
| 14 |
+
return dayjs(date).format('HH:mm:ss');
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
export function today() {
|
| 18 |
return formatDate(dayjs());
|
| 19 |
}
|
|
|
|
| 25 |
export function lastWeek() {
|
| 26 |
return formatDate(dayjs().subtract(1, 'weeks'));
|
| 27 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|