jettask 0.2.6__py3-none-any.whl → 0.2.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jettask/core/cli.py +152 -0
- jettask/pg_consumer/sql/add_execution_time_field.sql +29 -0
- jettask/pg_consumer/sql/create_new_tables.sql +137 -0
- jettask/pg_consumer/sql/create_tables_v3.sql +175 -0
- jettask/pg_consumer/sql/migrate_to_new_structure.sql +179 -0
- jettask/pg_consumer/sql/modify_time_fields.sql +69 -0
- jettask/webui/frontend/package.json +30 -0
- jettask/webui/frontend/src/App.css +109 -0
- jettask/webui/frontend/src/App.jsx +66 -0
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
- jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
- jettask/webui/frontend/src/components/charts/QueueChart.jsx +111 -0
- jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +115 -0
- jettask/webui/frontend/src/components/charts/WorkerChart.jsx +40 -0
- jettask/webui/frontend/src/components/common/StatsCard.jsx +18 -0
- jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
- jettask/webui/frontend/src/components/layout/Header.css +106 -0
- jettask/webui/frontend/src/components/layout/Header.jsx +106 -0
- jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
- jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
- jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
- jettask/webui/frontend/src/contexts/LoadingContext.jsx +27 -0
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
- jettask/webui/frontend/src/index.css +114 -0
- jettask/webui/frontend/src/main.jsx +20 -0
- jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
- jettask/webui/frontend/src/pages/Dashboard/index.css +35 -0
- jettask/webui/frontend/src/pages/Dashboard/index.jsx +281 -0
- jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
- jettask/webui/frontend/src/pages/QueueDetail.jsx +1117 -0
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +527 -0
- jettask/webui/frontend/src/pages/Queues.jsx +12 -0
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
- jettask/webui/frontend/src/pages/Settings.jsx +800 -0
- jettask/webui/frontend/src/pages/Workers.jsx +12 -0
- jettask/webui/frontend/src/services/api.js +114 -0
- jettask/webui/frontend/src/services/queueTrend.js +152 -0
- jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
- jettask/webui/frontend/src/utils/userPreferences.js +154 -0
- jettask/webui/frontend/vite.config.js +26 -0
- {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/METADATA +70 -2
- {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/RECORD +59 -14
- jettask/webui/static/dist/assets/index-7129cfe1.css +0 -1
- jettask/webui/static/dist/assets/index-8d1935cc.js +0 -774
- jettask/webui/static/dist/index.html +0 -15
- jettask/webui/static/index.html +0 -1734
- jettask/webui/static/queue.html +0 -981
- jettask/webui/static/queues.html +0 -549
- jettask/webui/static/workers.html +0 -734
- {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/WHEEL +0 -0
- {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.6.dist-info → jettask-0.2.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
2
|
+
import { Card, Empty, Tabs } from 'antd';
|
3
|
+
import { Column } from '@ant-design/plots';
|
4
|
+
import QueueTrendChart from './QueueTrendChart';
|
5
|
+
import { fetchQueueTrend } from '../../services/queueTrend';
|
6
|
+
|
7
|
+
const { TabPane } = Tabs;
|
8
|
+
|
9
|
+
const QueueChart = ({
|
10
|
+
data,
|
11
|
+
loading,
|
12
|
+
queues = [],
|
13
|
+
timeRange = '1h',
|
14
|
+
customTimeRange = null,
|
15
|
+
mode = 'distribution' // 'distribution' | 'trend'
|
16
|
+
}) => {
|
17
|
+
const [trendData, setTrendData] = useState([]);
|
18
|
+
const [activeTab, setActiveTab] = useState('trend');
|
19
|
+
|
20
|
+
// 获取趋势数据
|
21
|
+
useEffect(() => {
|
22
|
+
if (queues && queues.length > 0 && (mode === 'trend' || activeTab === 'trend')) {
|
23
|
+
// 获取队列名称列表
|
24
|
+
const queueNames = queues.map(q => q.name || q);
|
25
|
+
|
26
|
+
// 从API获取趋势数据
|
27
|
+
const loadTrendData = async () => {
|
28
|
+
try {
|
29
|
+
const data = await fetchQueueTrend(timeRange, queueNames, customTimeRange);
|
30
|
+
setTrendData(data);
|
31
|
+
} catch (error) {
|
32
|
+
console.error('Failed to load trend data:', error);
|
33
|
+
// 如果失败,使用空数据
|
34
|
+
setTrendData([]);
|
35
|
+
}
|
36
|
+
};
|
37
|
+
|
38
|
+
loadTrendData();
|
39
|
+
}
|
40
|
+
}, [queues, timeRange, customTimeRange, mode, activeTab]);
|
41
|
+
|
42
|
+
// 柱状图配置
|
43
|
+
const columnConfig = {
|
44
|
+
data: data || [],
|
45
|
+
xField: 'name',
|
46
|
+
yField: 'value',
|
47
|
+
label: {
|
48
|
+
position: 'middle',
|
49
|
+
style: {
|
50
|
+
fill: '#FFFFFF',
|
51
|
+
opacity: 0.6,
|
52
|
+
},
|
53
|
+
},
|
54
|
+
xAxis: {
|
55
|
+
label: {
|
56
|
+
autoHide: true,
|
57
|
+
autoRotate: false,
|
58
|
+
},
|
59
|
+
},
|
60
|
+
meta: {
|
61
|
+
name: { alias: '队列' },
|
62
|
+
value: { alias: '任务数' },
|
63
|
+
},
|
64
|
+
};
|
65
|
+
|
66
|
+
// 如果指定为趋势模式,直接返回趋势图
|
67
|
+
if (mode === 'trend') {
|
68
|
+
return (
|
69
|
+
<QueueTrendChart
|
70
|
+
data={trendData}
|
71
|
+
loading={loading}
|
72
|
+
timeRange={timeRange}
|
73
|
+
/>
|
74
|
+
);
|
75
|
+
}
|
76
|
+
|
77
|
+
// 如果没有指定模式,显示标签页切换
|
78
|
+
if (!mode || mode === 'both') {
|
79
|
+
return (
|
80
|
+
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
81
|
+
<TabPane tab="处理趋势" key="trend">
|
82
|
+
<QueueTrendChart
|
83
|
+
data={trendData}
|
84
|
+
loading={loading}
|
85
|
+
timeRange={timeRange}
|
86
|
+
/>
|
87
|
+
</TabPane>
|
88
|
+
<TabPane tab="队列分布" key="distribution">
|
89
|
+
{data && data.length > 0 ? (
|
90
|
+
<Column {...columnConfig} />
|
91
|
+
) : (
|
92
|
+
<Empty description="暂无数据" />
|
93
|
+
)}
|
94
|
+
</TabPane>
|
95
|
+
</Tabs>
|
96
|
+
);
|
97
|
+
}
|
98
|
+
|
99
|
+
// 默认显示分布图
|
100
|
+
return (
|
101
|
+
<Card title="队列任务分布" loading={loading}>
|
102
|
+
{data && data.length > 0 ? (
|
103
|
+
<Column {...columnConfig} />
|
104
|
+
) : (
|
105
|
+
<Empty description="暂无数据" />
|
106
|
+
)}
|
107
|
+
</Card>
|
108
|
+
);
|
109
|
+
};
|
110
|
+
|
111
|
+
export default QueueChart;
|
@@ -0,0 +1,115 @@
|
|
1
|
+
import { useState, useEffect } from 'react';
|
2
|
+
import { Line } from '@ant-design/plots';
|
3
|
+
import { Empty, Spin } from 'antd';
|
4
|
+
import dayjs from 'dayjs';
|
5
|
+
|
6
|
+
const QueueTrendChart = ({ data = [], loading = false }) => {
|
7
|
+
const [chartData, setChartData] = useState([]);
|
8
|
+
|
9
|
+
useEffect(() => {
|
10
|
+
if (data && data.length > 0) {
|
11
|
+
// 处理数据,转换为Date对象并按时间排序
|
12
|
+
const processedData = data.map(item => ({
|
13
|
+
...item,
|
14
|
+
date: new Date(item.time), // 使用date字段,与示例保持一致
|
15
|
+
value: item.value || 0,
|
16
|
+
queue: item.queue
|
17
|
+
}));
|
18
|
+
|
19
|
+
// 按时间排序确保连线正确
|
20
|
+
processedData.sort((a, b) => a.date.getTime() - b.date.getTime());
|
21
|
+
|
22
|
+
setChartData(processedData);
|
23
|
+
}
|
24
|
+
}, [data]);
|
25
|
+
|
26
|
+
// 图表配置 - 参考Ant Design Charts示例
|
27
|
+
const config = {
|
28
|
+
data: chartData,
|
29
|
+
xField: (d) => d.date, // 返回Date对象,让图表库自动处理
|
30
|
+
yField: 'value',
|
31
|
+
seriesField: 'queue', // 如果有多个队列,按队列分组
|
32
|
+
|
33
|
+
// 连接空值配置
|
34
|
+
connectNulls: {
|
35
|
+
connect: true,
|
36
|
+
connectStroke: '#ccc',
|
37
|
+
},
|
38
|
+
|
39
|
+
// X轴会自动优化显示,不需要手动设置formatter
|
40
|
+
xAxis: {
|
41
|
+
type: 'time',
|
42
|
+
nice: true,
|
43
|
+
},
|
44
|
+
|
45
|
+
// Y轴配置
|
46
|
+
yAxis: {
|
47
|
+
label: {
|
48
|
+
formatter: (v) => {
|
49
|
+
// 格式化数字显示
|
50
|
+
if (v >= 1000000) {
|
51
|
+
return `${(v / 1000000).toFixed(1)}M`;
|
52
|
+
} else if (v >= 1000) {
|
53
|
+
return `${(v / 1000).toFixed(1)}K`;
|
54
|
+
}
|
55
|
+
return String(v);
|
56
|
+
}
|
57
|
+
}
|
58
|
+
},
|
59
|
+
|
60
|
+
// 悬浮提示配置
|
61
|
+
tooltip: {
|
62
|
+
showTitle: true,
|
63
|
+
title: (title) => {
|
64
|
+
// 格式化时间显示
|
65
|
+
return dayjs(title).format('YYYY-MM-DD HH:mm:ss');
|
66
|
+
},
|
67
|
+
formatter: (datum) => {
|
68
|
+
return {
|
69
|
+
name: datum.queue || '任务数',
|
70
|
+
value: datum.value.toLocaleString(), // 添加千分位分隔符
|
71
|
+
};
|
72
|
+
},
|
73
|
+
},
|
74
|
+
|
75
|
+
// 平滑曲线
|
76
|
+
smooth: true,
|
77
|
+
|
78
|
+
// 动画配置
|
79
|
+
animation: {
|
80
|
+
appear: {
|
81
|
+
animation: 'path-in',
|
82
|
+
duration: 1000,
|
83
|
+
},
|
84
|
+
},
|
85
|
+
|
86
|
+
// 响应式配置
|
87
|
+
autoFit: true,
|
88
|
+
height: 300,
|
89
|
+
|
90
|
+
// 主题颜色
|
91
|
+
color: ['#5B8FF9', '#61DDAA', '#65789B', '#F6BD16', '#7262FD'],
|
92
|
+
};
|
93
|
+
|
94
|
+
// 空数据处理
|
95
|
+
if (!loading && (!chartData || chartData.length === 0)) {
|
96
|
+
return (
|
97
|
+
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
98
|
+
<Empty description="暂无数据" />
|
99
|
+
</div>
|
100
|
+
);
|
101
|
+
}
|
102
|
+
|
103
|
+
// 加载状态
|
104
|
+
if (loading) {
|
105
|
+
return (
|
106
|
+
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
107
|
+
<Spin size="large" tip="加载中..." />
|
108
|
+
</div>
|
109
|
+
);
|
110
|
+
}
|
111
|
+
|
112
|
+
return <Line {...config} />;
|
113
|
+
};
|
114
|
+
|
115
|
+
export default QueueTrendChart;
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Card, Empty } from 'antd';
|
3
|
+
import { Pie } from '@ant-design/plots';
|
4
|
+
|
5
|
+
const WorkerChart = ({ data, loading }) => {
|
6
|
+
const chartData = data || [
|
7
|
+
{ type: '在线', value: 0 },
|
8
|
+
{ type: '离线', value: 0 },
|
9
|
+
];
|
10
|
+
|
11
|
+
const config = {
|
12
|
+
appendPadding: 10,
|
13
|
+
data: chartData,
|
14
|
+
angleField: 'value',
|
15
|
+
colorField: 'type',
|
16
|
+
radius: 0.8,
|
17
|
+
label: {
|
18
|
+
type: 'inner',
|
19
|
+
offset: '-30%',
|
20
|
+
content: ({ percent }) => `${(percent * 100).toFixed(0)}%`,
|
21
|
+
style: {
|
22
|
+
fontSize: 14,
|
23
|
+
textAlign: 'center',
|
24
|
+
},
|
25
|
+
},
|
26
|
+
interactions: [{ type: 'element-active' }],
|
27
|
+
};
|
28
|
+
|
29
|
+
return (
|
30
|
+
<Card title="Worker 状态分布" loading={loading}>
|
31
|
+
{chartData.some(item => item.value > 0) ? (
|
32
|
+
<Pie {...config} />
|
33
|
+
) : (
|
34
|
+
<Empty description="暂无数据" />
|
35
|
+
)}
|
36
|
+
</Card>
|
37
|
+
);
|
38
|
+
};
|
39
|
+
|
40
|
+
export default WorkerChart;
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Card, Statistic } from 'antd';
|
3
|
+
|
4
|
+
const StatsCard = ({ title, value, icon, prefix, suffix, valueStyle }) => {
|
5
|
+
return (
|
6
|
+
<Card>
|
7
|
+
<Statistic
|
8
|
+
title={title}
|
9
|
+
value={value}
|
10
|
+
prefix={icon}
|
11
|
+
suffix={suffix}
|
12
|
+
valueStyle={valueStyle}
|
13
|
+
/>
|
14
|
+
</Card>
|
15
|
+
);
|
16
|
+
};
|
17
|
+
|
18
|
+
export default StatsCard;
|
@@ -0,0 +1,95 @@
|
|
1
|
+
/* 整体布局 */
|
2
|
+
.app-layout {
|
3
|
+
height: 100vh;
|
4
|
+
overflow: hidden;
|
5
|
+
}
|
6
|
+
|
7
|
+
/* 主布局区域 */
|
8
|
+
.main-layout {
|
9
|
+
transition: margin-left 0.2s;
|
10
|
+
display: flex;
|
11
|
+
flex-direction: column;
|
12
|
+
height: 100vh;
|
13
|
+
}
|
14
|
+
|
15
|
+
/* 顶部导航栏 */
|
16
|
+
.app-header {
|
17
|
+
background: #2a2d31;
|
18
|
+
padding: 0;
|
19
|
+
height: 48px;
|
20
|
+
line-height: 48px;
|
21
|
+
display: flex;
|
22
|
+
justify-content: space-between;
|
23
|
+
align-items: center;
|
24
|
+
border-bottom: 1px solid #f0f2f5;
|
25
|
+
z-index: 10;
|
26
|
+
position: sticky;
|
27
|
+
top: 0;
|
28
|
+
}
|
29
|
+
|
30
|
+
.header-left {
|
31
|
+
flex: 1;
|
32
|
+
display: flex;
|
33
|
+
align-items: center;
|
34
|
+
height: 100%;
|
35
|
+
overflow: hidden;
|
36
|
+
}
|
37
|
+
|
38
|
+
.header-right {
|
39
|
+
display: flex;
|
40
|
+
align-items: center;
|
41
|
+
height: 100%;
|
42
|
+
gap: 16px;
|
43
|
+
}
|
44
|
+
|
45
|
+
/* 内容区域 */
|
46
|
+
.app-content {
|
47
|
+
flex: 1;
|
48
|
+
overflow: auto;
|
49
|
+
background: #f0f2f5;
|
50
|
+
padding: 0;
|
51
|
+
}
|
52
|
+
|
53
|
+
/* 页面内容容器 */
|
54
|
+
.page-container {
|
55
|
+
height: 100%;
|
56
|
+
background: #fff;
|
57
|
+
}
|
58
|
+
|
59
|
+
/* 滚动条样式 */
|
60
|
+
.app-content::-webkit-scrollbar {
|
61
|
+
width: 8px;
|
62
|
+
height: 8px;
|
63
|
+
}
|
64
|
+
|
65
|
+
.app-content::-webkit-scrollbar-track {
|
66
|
+
background: #f0f0f0;
|
67
|
+
}
|
68
|
+
|
69
|
+
.app-content::-webkit-scrollbar-thumb {
|
70
|
+
background: #bfbfbf;
|
71
|
+
border-radius: 4px;
|
72
|
+
}
|
73
|
+
|
74
|
+
.app-content::-webkit-scrollbar-thumb:hover {
|
75
|
+
background: #8c8c8c;
|
76
|
+
}
|
77
|
+
|
78
|
+
/* 响应式调整 */
|
79
|
+
@media (max-width: 768px) {
|
80
|
+
.main-layout {
|
81
|
+
margin-left: 0 !important;
|
82
|
+
}
|
83
|
+
|
84
|
+
.app-sider {
|
85
|
+
position: fixed;
|
86
|
+
z-index: 1000;
|
87
|
+
height: 100vh;
|
88
|
+
left: -200px;
|
89
|
+
transition: left 0.2s;
|
90
|
+
}
|
91
|
+
|
92
|
+
.app-sider.ant-layout-sider-collapsed {
|
93
|
+
left: 0;
|
94
|
+
}
|
95
|
+
}
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { Layout } from 'antd';
|
3
|
+
import { Outlet } from 'react-router-dom';
|
4
|
+
import SideMenu from './SideMenu';
|
5
|
+
import TabsNav from './TabsNav';
|
6
|
+
import UserInfo from './UserInfo';
|
7
|
+
import NamespaceSelector from '../NamespaceSelector';
|
8
|
+
import { useNamespace } from '../../contexts/NamespaceContext';
|
9
|
+
import './AppLayout.css';
|
10
|
+
|
11
|
+
const { Header, Content } = Layout;
|
12
|
+
|
13
|
+
const AppLayout = () => {
|
14
|
+
const { currentNamespace, setCurrentNamespace } = useNamespace();
|
15
|
+
|
16
|
+
console.log('🔧 AppLayout渲染,currentNamespace:', currentNamespace);
|
17
|
+
console.log('🔧 AppLayout渲染,setCurrentNamespace:', typeof setCurrentNamespace);
|
18
|
+
|
19
|
+
return (
|
20
|
+
<Layout className="app-layout">
|
21
|
+
{/* 左侧菜单 */}
|
22
|
+
<SideMenu />
|
23
|
+
|
24
|
+
{/* 右侧主体 */}
|
25
|
+
<Layout className="main-layout" style={{ marginLeft: 64 }}>
|
26
|
+
{/* 顶部导航栏 */}
|
27
|
+
<Header className="app-header">
|
28
|
+
<div className="header-left">
|
29
|
+
<TabsNav />
|
30
|
+
</div>
|
31
|
+
<div className="header-right">
|
32
|
+
<NamespaceSelector
|
33
|
+
value={currentNamespace}
|
34
|
+
onChange={setCurrentNamespace}
|
35
|
+
/>
|
36
|
+
<UserInfo />
|
37
|
+
</div>
|
38
|
+
</Header>
|
39
|
+
|
40
|
+
{/* 内容区域 */}
|
41
|
+
<Content className="app-content">
|
42
|
+
<Outlet />
|
43
|
+
</Content>
|
44
|
+
</Layout>
|
45
|
+
</Layout>
|
46
|
+
);
|
47
|
+
};
|
48
|
+
|
49
|
+
export default AppLayout;
|
@@ -0,0 +1,106 @@
|
|
1
|
+
.app-header {
|
2
|
+
background: rgba(20, 20, 20, 0.95) !important;
|
3
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
4
|
+
padding: 0;
|
5
|
+
position: relative;
|
6
|
+
height: 48px !important;
|
7
|
+
line-height: 48px !important;
|
8
|
+
}
|
9
|
+
|
10
|
+
.header-container {
|
11
|
+
max-width: 1440px;
|
12
|
+
margin: 0 auto;
|
13
|
+
padding: 0 24px;
|
14
|
+
display: flex;
|
15
|
+
align-items: center;
|
16
|
+
justify-content: space-between;
|
17
|
+
height: 48px;
|
18
|
+
}
|
19
|
+
|
20
|
+
.header-left {
|
21
|
+
display: flex;
|
22
|
+
align-items: center;
|
23
|
+
}
|
24
|
+
|
25
|
+
.app-logo {
|
26
|
+
display: flex;
|
27
|
+
align-items: center;
|
28
|
+
gap: 8px;
|
29
|
+
color: white;
|
30
|
+
font-size: 16px;
|
31
|
+
font-weight: 700;
|
32
|
+
cursor: pointer;
|
33
|
+
}
|
34
|
+
|
35
|
+
.logo-icon {
|
36
|
+
font-size: 18px;
|
37
|
+
color: var(--primary-color);
|
38
|
+
}
|
39
|
+
|
40
|
+
.logo-text {
|
41
|
+
background: linear-gradient(45deg, #1890ff, #52c41a);
|
42
|
+
-webkit-background-clip: text;
|
43
|
+
-webkit-text-fill-color: transparent;
|
44
|
+
background-clip: text;
|
45
|
+
}
|
46
|
+
|
47
|
+
.header-center {
|
48
|
+
flex: 1;
|
49
|
+
display: flex;
|
50
|
+
justify-content: center;
|
51
|
+
}
|
52
|
+
|
53
|
+
.header-menu {
|
54
|
+
background: transparent !important;
|
55
|
+
border: none !important;
|
56
|
+
line-height: 48px !important;
|
57
|
+
}
|
58
|
+
|
59
|
+
.header-menu .ant-menu-item {
|
60
|
+
color: rgba(255, 255, 255, 0.65) !important;
|
61
|
+
height: 48px !important;
|
62
|
+
line-height: 48px !important;
|
63
|
+
}
|
64
|
+
|
65
|
+
.header-menu .ant-menu-item:hover {
|
66
|
+
color: white !important;
|
67
|
+
}
|
68
|
+
|
69
|
+
.header-menu .ant-menu-item-selected {
|
70
|
+
color: var(--primary-color) !important;
|
71
|
+
}
|
72
|
+
|
73
|
+
.header-right {
|
74
|
+
display: flex;
|
75
|
+
align-items: center;
|
76
|
+
}
|
77
|
+
|
78
|
+
.refresh-btn {
|
79
|
+
color: rgba(255, 255, 255, 0.65) !important;
|
80
|
+
}
|
81
|
+
|
82
|
+
.refresh-btn:hover {
|
83
|
+
color: white !important;
|
84
|
+
}
|
85
|
+
|
86
|
+
/* 命名空间选择器样式 */
|
87
|
+
.header-right {
|
88
|
+
display: flex;
|
89
|
+
align-items: center;
|
90
|
+
gap: 16px;
|
91
|
+
}
|
92
|
+
|
93
|
+
/* 脉动动画 */
|
94
|
+
@keyframes pulse {
|
95
|
+
0% {
|
96
|
+
opacity: 1;
|
97
|
+
}
|
98
|
+
50% {
|
99
|
+
opacity: 0.5;
|
100
|
+
}
|
101
|
+
100% {
|
102
|
+
opacity: 1;
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
/* 响应式设计 */
|
@@ -0,0 +1,106 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
import { useNavigate, useLocation } from 'react-router-dom'
|
3
|
+
import { Layout, Menu } from 'antd'
|
4
|
+
import {
|
5
|
+
DashboardOutlined,
|
6
|
+
AppstoreOutlined,
|
7
|
+
TeamOutlined,
|
8
|
+
RocketOutlined,
|
9
|
+
LoadingOutlined,
|
10
|
+
ClockCircleOutlined,
|
11
|
+
AlertOutlined,
|
12
|
+
CloudServerOutlined
|
13
|
+
} from '@ant-design/icons'
|
14
|
+
import { useLoading } from '../../contexts/LoadingContext'
|
15
|
+
import { useNamespace } from '../../contexts/NamespaceContext'
|
16
|
+
import NamespaceSelector from '../NamespaceSelector'
|
17
|
+
import './Header.css'
|
18
|
+
|
19
|
+
const { Header: AntHeader } = Layout
|
20
|
+
|
21
|
+
const Header = () => {
|
22
|
+
const navigate = useNavigate()
|
23
|
+
const location = useLocation()
|
24
|
+
const { isLoading } = useLoading()
|
25
|
+
const { currentNamespace, setCurrentNamespace } = useNamespace()
|
26
|
+
|
27
|
+
const menuItems = [
|
28
|
+
{
|
29
|
+
key: '/dashboard',
|
30
|
+
icon: <DashboardOutlined />,
|
31
|
+
label: '概览',
|
32
|
+
},
|
33
|
+
{
|
34
|
+
key: '/queues',
|
35
|
+
icon: <AppstoreOutlined />,
|
36
|
+
label: '队列',
|
37
|
+
},
|
38
|
+
// {
|
39
|
+
// key: '/workers',
|
40
|
+
// icon: <TeamOutlined />,
|
41
|
+
// label: 'Workers',
|
42
|
+
// },
|
43
|
+
{
|
44
|
+
key: '/scheduled-tasks',
|
45
|
+
icon: <ClockCircleOutlined />,
|
46
|
+
label: '定时任务',
|
47
|
+
},
|
48
|
+
{
|
49
|
+
key: '/alerts',
|
50
|
+
icon: <AlertOutlined />,
|
51
|
+
label: '监控告警',
|
52
|
+
},
|
53
|
+
]
|
54
|
+
|
55
|
+
const handleMenuClick = ({ key }) => {
|
56
|
+
navigate(key)
|
57
|
+
}
|
58
|
+
|
59
|
+
return (
|
60
|
+
<AntHeader className="app-header">
|
61
|
+
<div className="header-container">
|
62
|
+
<div className="header-left">
|
63
|
+
<div className="app-logo">
|
64
|
+
{isLoading ? (
|
65
|
+
<LoadingOutlined
|
66
|
+
className="logo-icon"
|
67
|
+
style={{
|
68
|
+
fontSize: 18,
|
69
|
+
color: '#1890ff'
|
70
|
+
}}
|
71
|
+
spin
|
72
|
+
/>
|
73
|
+
) : (
|
74
|
+
<RocketOutlined className="logo-icon" />
|
75
|
+
)}
|
76
|
+
<span className="logo-text">JetTask Monitor</span>
|
77
|
+
</div>
|
78
|
+
</div>
|
79
|
+
|
80
|
+
<div className="header-center">
|
81
|
+
<Menu
|
82
|
+
mode="horizontal"
|
83
|
+
selectedKeys={[location.pathname]}
|
84
|
+
items={menuItems}
|
85
|
+
onClick={handleMenuClick}
|
86
|
+
className="header-menu"
|
87
|
+
/>
|
88
|
+
</div>
|
89
|
+
|
90
|
+
<div className="header-right">
|
91
|
+
<NamespaceSelector
|
92
|
+
value={currentNamespace}
|
93
|
+
onChange={(namespace) => {
|
94
|
+
console.log('🔧 Header收到命名空间切换:', namespace);
|
95
|
+
setCurrentNamespace(namespace);
|
96
|
+
}}
|
97
|
+
style={{ marginLeft: 'auto' }}
|
98
|
+
/>
|
99
|
+
</div>
|
100
|
+
|
101
|
+
</div>
|
102
|
+
</AntHeader>
|
103
|
+
)
|
104
|
+
}
|
105
|
+
|
106
|
+
export default Header
|