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 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|