jettask 0.2.7__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.7.dist-info → jettask-0.2.8.dist-info}/METADATA +1 -1
- {jettask-0.2.7.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.7.dist-info → jettask-0.2.8.dist-info}/WHEEL +0 -0
- {jettask-0.2.7.dist-info → jettask-0.2.8.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.7.dist-info → jettask-0.2.8.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.7.dist-info → jettask-0.2.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
import axios from 'axios';
|
2
|
+
|
3
|
+
// 智能获取 API 基础 URL
|
4
|
+
const getApiBaseUrl = () => {
|
5
|
+
// 在开发环境中使用 localhost
|
6
|
+
if (typeof window !== 'undefined') {
|
7
|
+
const hostname = window.location.hostname;
|
8
|
+
const protocol = window.location.protocol;
|
9
|
+
|
10
|
+
// 如果是生产环境,使用相同的域名
|
11
|
+
if (hostname !== 'localhost' && hostname !== '127.0.0.1') {
|
12
|
+
return `${protocol}//${hostname}:8001/api`;
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
// 默认开发环境
|
17
|
+
return 'http://localhost:8001/api';
|
18
|
+
};
|
19
|
+
|
20
|
+
// 智能获取 WebSocket URL
|
21
|
+
const getWsBaseUrl = () => {
|
22
|
+
if (typeof window !== 'undefined') {
|
23
|
+
const hostname = window.location.hostname;
|
24
|
+
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
25
|
+
|
26
|
+
// 如果是生产环境,使用相同的域名
|
27
|
+
if (hostname !== 'localhost' && hostname !== '127.0.0.1') {
|
28
|
+
return `${wsProtocol}//${hostname}:8001/ws`;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
// 默认开发环境
|
33
|
+
return 'ws://localhost:8001/ws';
|
34
|
+
};
|
35
|
+
|
36
|
+
const API_BASE_URL = getApiBaseUrl();
|
37
|
+
const WS_BASE_URL = getWsBaseUrl();
|
38
|
+
|
39
|
+
// 创建 axios 实例
|
40
|
+
const api = axios.create({
|
41
|
+
baseURL: API_BASE_URL,
|
42
|
+
timeout: 10000,
|
43
|
+
});
|
44
|
+
|
45
|
+
// 获取全局统计信息
|
46
|
+
export const fetchGlobalStats = async () => {
|
47
|
+
try {
|
48
|
+
const response = await api.get('/stats');
|
49
|
+
return response.data.data;
|
50
|
+
} catch (error) {
|
51
|
+
console.error('Failed to fetch global stats:', error);
|
52
|
+
throw error;
|
53
|
+
}
|
54
|
+
};
|
55
|
+
|
56
|
+
// 获取队列列表
|
57
|
+
export const fetchQueues = async () => {
|
58
|
+
try {
|
59
|
+
const response = await api.get('/queues');
|
60
|
+
return response.data;
|
61
|
+
} catch (error) {
|
62
|
+
console.error('Failed to fetch queues:', error);
|
63
|
+
throw error;
|
64
|
+
}
|
65
|
+
};
|
66
|
+
|
67
|
+
// 获取队列时间线数据
|
68
|
+
export const fetchQueueTimeline = async (params) => {
|
69
|
+
try {
|
70
|
+
// 从参数中提取命名空间,如果没有则使用 default
|
71
|
+
const namespace = params.namespace || 'default';
|
72
|
+
const response = await api.post(`/queue-timeline/${namespace}`, params);
|
73
|
+
// 处理数据,确保不包含填充的空值
|
74
|
+
const data = response.data.data || response.data || [];
|
75
|
+
return {
|
76
|
+
...response.data,
|
77
|
+
data: data.filter(item => item.value > 0) // 只返回有实际值的数据点
|
78
|
+
};
|
79
|
+
} catch (error) {
|
80
|
+
console.error('Failed to fetch queue timeline:', error);
|
81
|
+
throw error;
|
82
|
+
}
|
83
|
+
};
|
84
|
+
|
85
|
+
// WebSocket 连接
|
86
|
+
export const connectWebSocket = (onMessage, onError) => {
|
87
|
+
const ws = new WebSocket(`${WS_BASE_URL}/monitor`);
|
88
|
+
|
89
|
+
ws.onopen = () => {
|
90
|
+
console.log('WebSocket connected');
|
91
|
+
};
|
92
|
+
|
93
|
+
ws.onmessage = (event) => {
|
94
|
+
try {
|
95
|
+
const data = JSON.parse(event.data);
|
96
|
+
onMessage(data);
|
97
|
+
} catch (error) {
|
98
|
+
console.error('Failed to parse WebSocket message:', error);
|
99
|
+
}
|
100
|
+
};
|
101
|
+
|
102
|
+
ws.onerror = (error) => {
|
103
|
+
console.error('WebSocket error:', error);
|
104
|
+
if (onError) onError(error);
|
105
|
+
};
|
106
|
+
|
107
|
+
ws.onclose = () => {
|
108
|
+
console.log('WebSocket disconnected');
|
109
|
+
};
|
110
|
+
|
111
|
+
return ws;
|
112
|
+
};
|
113
|
+
|
114
|
+
export default api;
|
@@ -0,0 +1,152 @@
|
|
1
|
+
import axios from 'axios';
|
2
|
+
|
3
|
+
// 在 React 应用中,环境变量需要以 REACT_APP_ 开头
|
4
|
+
// 使用 window.location 作为后备方案
|
5
|
+
const getApiBaseUrl = () => {
|
6
|
+
// 尝试从环境变量获取(Create React App 会在构建时注入)
|
7
|
+
if (typeof window !== 'undefined' && window.REACT_APP_API_URL) {
|
8
|
+
return window.REACT_APP_API_URL;
|
9
|
+
}
|
10
|
+
|
11
|
+
// 如果是生产环境,使用相同的域名和端口
|
12
|
+
if (typeof window !== 'undefined' && window.location.hostname !== 'localhost') {
|
13
|
+
return `${window.location.protocol}//${window.location.hostname}:8001`;
|
14
|
+
}
|
15
|
+
|
16
|
+
// 默认使用 localhost
|
17
|
+
return 'http://localhost:8001';
|
18
|
+
};
|
19
|
+
|
20
|
+
const API_BASE_URL = getApiBaseUrl();
|
21
|
+
|
22
|
+
/**
|
23
|
+
* 获取队列趋势数据
|
24
|
+
* @param {string} timeRange - 时间范围 (15m, 30m, 1h, 3h, 6h, 12h, 24h, 3d, 7d)
|
25
|
+
* @param {Array} queues - 队列名称列表
|
26
|
+
* @param {Object} customTimeRange - 自定义时间范围 {start: Date, end: Date}
|
27
|
+
* @returns {Promise<Array>} 趋势数据
|
28
|
+
*/
|
29
|
+
export const fetchQueueTrend = async (timeRange = '1h', queues = [], customTimeRange = null) => {
|
30
|
+
try {
|
31
|
+
const params = {
|
32
|
+
range: timeRange,
|
33
|
+
queues: queues.join(',')
|
34
|
+
};
|
35
|
+
|
36
|
+
// 如果有自定义时间范围
|
37
|
+
if (customTimeRange) {
|
38
|
+
params.start = customTimeRange[0].valueOf();
|
39
|
+
params.end = customTimeRange[1].valueOf();
|
40
|
+
}
|
41
|
+
|
42
|
+
const response = await axios.get(`${API_BASE_URL}/api/queue-trend`, { params });
|
43
|
+
|
44
|
+
// 处理响应数据 - 后端现在直接返回时间线数据
|
45
|
+
const data = response.data.data || response.data || [];
|
46
|
+
|
47
|
+
// 转换数据格式,保持时间字段为ISO字符串,让组件转换为Date对象
|
48
|
+
return data.map(item => ({
|
49
|
+
time: item.time || item.timestamp, // 保持原始时间字符串
|
50
|
+
value: item.value || item.count || 0,
|
51
|
+
queue: item.queue || item.queue_name
|
52
|
+
}));
|
53
|
+
} catch (error) {
|
54
|
+
console.error('Failed to fetch queue trend:', error);
|
55
|
+
|
56
|
+
// 如果API失败,返回模拟数据
|
57
|
+
return generateMockTrendData(timeRange, queues);
|
58
|
+
}
|
59
|
+
};
|
60
|
+
|
61
|
+
/**
|
62
|
+
* 生成模拟趋势数据(用于开发和测试)
|
63
|
+
*/
|
64
|
+
const generateMockTrendData = (timeRange, queues) => {
|
65
|
+
const now = Date.now();
|
66
|
+
const points = getDataPoints(timeRange);
|
67
|
+
const interval = getIntervalByRange(timeRange);
|
68
|
+
|
69
|
+
const data = [];
|
70
|
+
|
71
|
+
// 为每个队列生成数据
|
72
|
+
queues.slice(0, 3).forEach(queue => {
|
73
|
+
// 生成基础值和趋势
|
74
|
+
const baseValue = Math.floor(Math.random() * 50) + 20;
|
75
|
+
const trend = Math.random() > 0.5 ? 1 : -1;
|
76
|
+
|
77
|
+
for (let i = 0; i < points; i++) {
|
78
|
+
// 添加一些随机波动
|
79
|
+
const variation = Math.sin(i / 5) * 10 + Math.random() * 10 - 5;
|
80
|
+
const value = Math.max(0, baseValue + trend * i * 0.5 + variation);
|
81
|
+
|
82
|
+
data.push({
|
83
|
+
time: now - (points - i - 1) * interval,
|
84
|
+
value: Math.floor(value),
|
85
|
+
queue: queue.name || queue,
|
86
|
+
originalTime: new Date(now - (points - i - 1) * interval).toISOString()
|
87
|
+
});
|
88
|
+
}
|
89
|
+
});
|
90
|
+
|
91
|
+
return data;
|
92
|
+
};
|
93
|
+
|
94
|
+
/**
|
95
|
+
* 根据时间范围获取数据点数量
|
96
|
+
*/
|
97
|
+
const getDataPoints = (range) => {
|
98
|
+
const points = {
|
99
|
+
'15m': 15, // 每分钟一个点
|
100
|
+
'30m': 30, // 每分钟一个点
|
101
|
+
'1h': 30, // 每2分钟一个点
|
102
|
+
'3h': 36, // 每5分钟一个点
|
103
|
+
'6h': 36, // 每10分钟一个点
|
104
|
+
'12h': 36, // 每20分钟一个点
|
105
|
+
'24h': 48, // 每30分钟一个点
|
106
|
+
'3d': 36, // 每2小时一个点
|
107
|
+
'7d': 42, // 每4小时一个点
|
108
|
+
};
|
109
|
+
return points[range] || 30;
|
110
|
+
};
|
111
|
+
|
112
|
+
/**
|
113
|
+
* 根据时间范围获取数据间隔(毫秒)
|
114
|
+
*/
|
115
|
+
const getIntervalByRange = (range) => {
|
116
|
+
const intervals = {
|
117
|
+
'15m': 60 * 1000, // 1分钟
|
118
|
+
'30m': 60 * 1000, // 1分钟
|
119
|
+
'1h': 2 * 60 * 1000, // 2分钟
|
120
|
+
'3h': 5 * 60 * 1000, // 5分钟
|
121
|
+
'6h': 10 * 60 * 1000, // 10分钟
|
122
|
+
'12h': 20 * 60 * 1000, // 20分钟
|
123
|
+
'24h': 30 * 60 * 1000, // 30分钟
|
124
|
+
'3d': 2 * 60 * 60 * 1000, // 2小时
|
125
|
+
'7d': 4 * 60 * 60 * 1000, // 4小时
|
126
|
+
};
|
127
|
+
return intervals[range] || 60 * 1000;
|
128
|
+
};
|
129
|
+
|
130
|
+
/**
|
131
|
+
* 获取队列统计信息
|
132
|
+
*/
|
133
|
+
export const fetchQueueStats = async (queueName, timeRange = '1h') => {
|
134
|
+
try {
|
135
|
+
const response = await axios.get(`${API_BASE_URL}/api/queue/${queueName}/stats`, {
|
136
|
+
params: { range: timeRange }
|
137
|
+
});
|
138
|
+
|
139
|
+
return response.data;
|
140
|
+
} catch (error) {
|
141
|
+
console.error('Failed to fetch queue stats:', error);
|
142
|
+
return {
|
143
|
+
total: 0,
|
144
|
+
pending: 0,
|
145
|
+
running: 0,
|
146
|
+
completed: 0,
|
147
|
+
failed: 0,
|
148
|
+
avgProcessTime: 0,
|
149
|
+
throughput: 0
|
150
|
+
};
|
151
|
+
}
|
152
|
+
};
|
@@ -0,0 +1,22 @@
|
|
1
|
+
// 在开发环境中临时屏蔽特定的控制台警告
|
2
|
+
// 注意:这只是为了减少开发时的控制台噪音,不影响实际功能
|
3
|
+
|
4
|
+
if (process.env.NODE_ENV === 'development') {
|
5
|
+
const originalWarning = console.warn;
|
6
|
+
console.warn = (...args) => {
|
7
|
+
const warningMessage = args[0]?.toString() || '';
|
8
|
+
|
9
|
+
// 屏蔽 findDOMNode 相关警告(来自第三方库)
|
10
|
+
if (warningMessage.includes('findDOMNode')) {
|
11
|
+
return;
|
12
|
+
}
|
13
|
+
|
14
|
+
// 屏蔽 React Router v7 迁移提示
|
15
|
+
if (warningMessage.includes('React Router Future Flag Warning')) {
|
16
|
+
return;
|
17
|
+
}
|
18
|
+
|
19
|
+
// 其他警告正常显示
|
20
|
+
originalWarning.apply(console, args);
|
21
|
+
};
|
22
|
+
}
|
@@ -0,0 +1,154 @@
|
|
1
|
+
/**
|
2
|
+
* 用户偏好设置管理工具
|
3
|
+
*/
|
4
|
+
|
5
|
+
const STORAGE_KEY = 'jettask_user_preferences';
|
6
|
+
|
7
|
+
/**
|
8
|
+
* 获取用户偏好设置
|
9
|
+
* @returns {Object} 用户偏好设置对象
|
10
|
+
*/
|
11
|
+
export const getUserPreferences = () => {
|
12
|
+
try {
|
13
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
14
|
+
return stored ? JSON.parse(stored) : {};
|
15
|
+
} catch (error) {
|
16
|
+
console.error('Failed to load user preferences:', error);
|
17
|
+
return {};
|
18
|
+
}
|
19
|
+
};
|
20
|
+
|
21
|
+
/**
|
22
|
+
* 保存用户偏好设置
|
23
|
+
* @param {Object} preferences - 偏好设置对象
|
24
|
+
*/
|
25
|
+
export const saveUserPreferences = (preferences) => {
|
26
|
+
try {
|
27
|
+
const current = getUserPreferences();
|
28
|
+
console.log('[saveUserPreferences] 当前值:', current);
|
29
|
+
console.log('[saveUserPreferences] 要更新的值:', preferences);
|
30
|
+
const updated = { ...current, ...preferences };
|
31
|
+
console.log('[saveUserPreferences] 合并后的值:', updated);
|
32
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
33
|
+
console.log('[saveUserPreferences] 已保存到 localStorage');
|
34
|
+
} catch (error) {
|
35
|
+
console.error('Failed to save user preferences:', error);
|
36
|
+
}
|
37
|
+
};
|
38
|
+
|
39
|
+
/**
|
40
|
+
* 获取特定的偏好设置
|
41
|
+
* @param {string} key - 设置项的键
|
42
|
+
* @param {*} defaultValue - 默认值
|
43
|
+
* @returns {*} 设置值
|
44
|
+
*/
|
45
|
+
export const getPreference = (key, defaultValue = null) => {
|
46
|
+
const preferences = getUserPreferences();
|
47
|
+
return preferences[key] !== undefined ? preferences[key] : defaultValue;
|
48
|
+
};
|
49
|
+
|
50
|
+
/**
|
51
|
+
* 保存特定的偏好设置
|
52
|
+
* @param {string} key - 设置项的键
|
53
|
+
* @param {*} value - 设置值
|
54
|
+
*/
|
55
|
+
export const setPreference = (key, value) => {
|
56
|
+
console.log('[setPreference] 保存偏好设置 - key:', key, 'value:', value);
|
57
|
+
saveUserPreferences({ [key]: value });
|
58
|
+
// 立即验证保存
|
59
|
+
const saved = getUserPreferences();
|
60
|
+
console.log('[setPreference] 保存后立即验证 - saved[key]:', saved[key]);
|
61
|
+
};
|
62
|
+
|
63
|
+
/**
|
64
|
+
* 清除所有偏好设置
|
65
|
+
*/
|
66
|
+
export const clearPreferences = () => {
|
67
|
+
try {
|
68
|
+
localStorage.removeItem(STORAGE_KEY);
|
69
|
+
} catch (error) {
|
70
|
+
console.error('Failed to clear user preferences:', error);
|
71
|
+
}
|
72
|
+
};
|
73
|
+
|
74
|
+
// 偏好设置的键
|
75
|
+
export const PREFERENCE_KEYS = {
|
76
|
+
QUEUE_MONITOR_TIME_RANGE: 'queueMonitor.timeRange',
|
77
|
+
QUEUE_MONITOR_SELECTED_QUEUES: 'queueMonitor.selectedQueues',
|
78
|
+
QUEUE_MONITOR_CUSTOM_TIME_RANGE: 'queueMonitor.customTimeRange',
|
79
|
+
QUEUE_DETAILS_PAGE_SIZE: 'queueDetails.pageSize',
|
80
|
+
};
|
81
|
+
|
82
|
+
/**
|
83
|
+
* 获取队列特定的筛选条件
|
84
|
+
* @param {string} queueName - 队列名称
|
85
|
+
* @returns {Object} 队列的筛选设置
|
86
|
+
*/
|
87
|
+
export const getQueueFilters = (queueName) => {
|
88
|
+
const key = `queue.${queueName}.filters`;
|
89
|
+
const preferences = getUserPreferences();
|
90
|
+
const queueSettings = preferences[key] || {};
|
91
|
+
|
92
|
+
// 返回默认结构
|
93
|
+
return {
|
94
|
+
filters: queueSettings.filters || [],
|
95
|
+
timeRange: queueSettings.timeRange || '1h',
|
96
|
+
customTimeRange: queueSettings.customTimeRange || null
|
97
|
+
};
|
98
|
+
};
|
99
|
+
|
100
|
+
/**
|
101
|
+
* 保存队列特定的筛选条件
|
102
|
+
* @param {string} queueName - 队列名称
|
103
|
+
* @param {Object} settings - 筛选设置
|
104
|
+
*/
|
105
|
+
export const saveQueueFilters = (queueName, settings) => {
|
106
|
+
const key = `queue.${queueName}.filters`;
|
107
|
+
const currentPrefs = getUserPreferences();
|
108
|
+
|
109
|
+
// 限制存储的队列数量,防止localStorage过大
|
110
|
+
const queueKeys = Object.keys(currentPrefs).filter(k => k.startsWith('queue.'));
|
111
|
+
if (queueKeys.length > 50) {
|
112
|
+
// 删除最旧的队列设置(简单策略:删除第一个)
|
113
|
+
delete currentPrefs[queueKeys[0]];
|
114
|
+
}
|
115
|
+
|
116
|
+
// 保存新的设置
|
117
|
+
currentPrefs[key] = {
|
118
|
+
filters: settings.filters || [],
|
119
|
+
timeRange: settings.timeRange || '1h',
|
120
|
+
customTimeRange: settings.customTimeRange || null,
|
121
|
+
lastUpdated: new Date().toISOString()
|
122
|
+
};
|
123
|
+
|
124
|
+
try {
|
125
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(currentPrefs));
|
126
|
+
} catch (error) {
|
127
|
+
console.error('Failed to save queue filters:', error);
|
128
|
+
// 如果存储失败(可能是容量问题),清理旧数据
|
129
|
+
if (error.name === 'QuotaExceededError') {
|
130
|
+
// 清理最旧的一半队列设置
|
131
|
+
const halfLength = Math.floor(queueKeys.length / 2);
|
132
|
+
for (let i = 0; i < halfLength; i++) {
|
133
|
+
delete currentPrefs[queueKeys[i]];
|
134
|
+
}
|
135
|
+
// 重试保存
|
136
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(currentPrefs));
|
137
|
+
}
|
138
|
+
}
|
139
|
+
};
|
140
|
+
|
141
|
+
/**
|
142
|
+
* 清除特定队列的筛选设置
|
143
|
+
* @param {string} queueName - 队列名称
|
144
|
+
*/
|
145
|
+
export const clearQueueFilters = (queueName) => {
|
146
|
+
const key = `queue.${queueName}.filters`;
|
147
|
+
const currentPrefs = getUserPreferences();
|
148
|
+
delete currentPrefs[key];
|
149
|
+
try {
|
150
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(currentPrefs));
|
151
|
+
} catch (error) {
|
152
|
+
console.error('Failed to clear queue filters:', error);
|
153
|
+
}
|
154
|
+
};
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import { defineConfig } from 'vite'
|
2
|
+
import react from '@vitejs/plugin-react'
|
3
|
+
|
4
|
+
export default defineConfig({
|
5
|
+
plugins: [react()],
|
6
|
+
server: {
|
7
|
+
port: 3000,
|
8
|
+
proxy: {
|
9
|
+
'/api': {
|
10
|
+
target: 'http://localhost:8001',
|
11
|
+
changeOrigin: true,
|
12
|
+
},
|
13
|
+
'/ws': {
|
14
|
+
target: 'ws://localhost:8001',
|
15
|
+
ws: true,
|
16
|
+
},
|
17
|
+
},
|
18
|
+
},
|
19
|
+
build: {
|
20
|
+
outDir: '../static/dist',
|
21
|
+
emptyOutDir: true,
|
22
|
+
},
|
23
|
+
optimizeDeps: {
|
24
|
+
include: ['antd', '@ant-design/icons', '@ant-design/plots'],
|
25
|
+
},
|
26
|
+
})
|
@@ -7,7 +7,7 @@ jettask/config/performance.py,sha256=bOdLEskfB_6cRfS10IRgmtKEsJw_CaIZsPHbXxaHwbU
|
|
7
7
|
jettask/core/__init__.py,sha256=CvBoBCERXCo-jgnkPqAuIgT4uC7oQMnSi7okRxMi6Vc,181
|
8
8
|
jettask/core/app.py,sha256=D6wqdGIVR8E1a7RNevNDaICfKGDsea4RqS9aRlMU810,74906
|
9
9
|
jettask/core/app_importer.py,sha256=B8WiSUz5_O5jlFIBr1CxI_F2gqFYK6ItpONiY_4AiXI,10266
|
10
|
-
jettask/core/cli.py,sha256=
|
10
|
+
jettask/core/cli.py,sha256=QG7MOE3m89cQSyHwEiGTzKlFaWHJOFLWqpY6aLdV4Wc,24452
|
11
11
|
jettask/core/consumer_manager.py,sha256=7z3IBvH85YD61ZkVNfOVsuP5NaAwV2Ki8aVz9TCplAM,79870
|
12
12
|
jettask/core/context.py,sha256=XI4Q1s75NIWImcrHIkCLgGo_kpuJIk8MlBoNIJuEfF0,993
|
13
13
|
jettask/core/delay_scanner.py,sha256=rwbIA7SFyOxAV14FW7NB40_djFrrqJJpNkpOri7XcZI,8394
|
@@ -32,6 +32,11 @@ jettask/monitoring/__init__.py,sha256=xWEEH5C2e8TW2iH6KNxBj5R-aTMg1GH3DrrwjRzRGD
|
|
32
32
|
jettask/monitoring/file_watcher.py,sha256=r3Mgekb_5sOssDrnFBCbbyvpWkwD2ZPA_j22ztzRCT0,1207
|
33
33
|
jettask/pg_consumer/pg_consumer_v2.py,sha256=ajd6ZM_HY9YWK_I5BNbGuWxNqSS892d3ZzvDlTSQgwc,16211
|
34
34
|
jettask/pg_consumer/sql_utils.py,sha256=dZM_8kIzUmRq0me4yLwb3UeoDvIVKJdIc9VNP1nbS0E,6461
|
35
|
+
jettask/pg_consumer/sql/add_execution_time_field.sql,sha256=z2Jx-0uG-IW5RsTGxcLFu2_BcMReluEptMJWB6Qrj08,1184
|
36
|
+
jettask/pg_consumer/sql/create_new_tables.sql,sha256=layb08_MFGVSE-NQEcjlQBklZ307JtJaccrnqkcj7Vg,6566
|
37
|
+
jettask/pg_consumer/sql/create_tables_v3.sql,sha256=ZwBjxKWU6imWACoWMCIACLsYUPiPep9WmJsdIjI2a58,8173
|
38
|
+
jettask/pg_consumer/sql/migrate_to_new_structure.sql,sha256=elhObWvbtqAhb4osJC6D5JhxcOxhGJgIBqx4ItB_xO8,5716
|
39
|
+
jettask/pg_consumer/sql/modify_time_fields.sql,sha256=mRfoXbP9iXbAJBj-xNume8-4AV_UPQcBALNUqEf3aKw,2572
|
35
40
|
jettask/scheduler/__init__.py,sha256=egJ-b3VjifFPOFhdEe6JOGyBBNxtqtH4cQvB6Vm_t-c,372
|
36
41
|
jettask/scheduler/add_execution_count.sql,sha256=x4jBsOFGj4JvEd-Ene3OhbR5QCKh5rv8dF01_qLSeE8,383
|
37
42
|
jettask/scheduler/add_priority_field.sql,sha256=z9S10u2D2eekVqB8xOAtDD4Rw1Ey497cmDL4pN2MgB0,915
|
@@ -108,20 +113,60 @@ jettask/webui/backend/models/requests.py,sha256=fsHiv4sU46biQ1bK1KfV-yxak0LRoFLo
|
|
108
113
|
jettask/webui/backend/models/responses.py,sha256=FwGpAFNL61a2okJFD41TYLcuwu-41FHXrk6P190jxzw,8528
|
109
114
|
jettask/webui/backend/services/__init__.py,sha256=DCf9NuJo1a65q3CXG0GtRksQpDhg3QuvmTYiWjxGjVE,57
|
110
115
|
jettask/webui/frontend/index.html,sha256=A2PgxT8KBJW15LVnmobQxDNctxsJJc2kA9ej4Jf7yec,367
|
116
|
+
jettask/webui/frontend/package.json,sha256=5e-hqT-GhjgXXQT669jYhllJ51Edz1-6wZiZKh_M1bw,723
|
117
|
+
jettask/webui/frontend/vite.config.js,sha256=WqgVesX8iqlLFtWq7u4L33hdJXaDqy9EOJJFhKpqNto,517
|
118
|
+
jettask/webui/frontend/src/App.css,sha256=TLwSOdKNsUtfAtnTyCxeLZBkRhhw34zWeA8zhEP7dDs,1753
|
119
|
+
jettask/webui/frontend/src/App.jsx,sha256=wyTfHb3hp0dThO9fhn4r8JLc9RIkaxnwOieY0o8e7tg,2497
|
120
|
+
jettask/webui/frontend/src/index.css,sha256=s2mxkzEDoY10XqRcV_5_32r4GZk22cvZoBMh6_NKVq0,2074
|
121
|
+
jettask/webui/frontend/src/main.jsx,sha256=u6wZT3jYm4Ae19rvs7l4kdg8r-k2XKLOdl68DRpfL2U,501
|
122
|
+
jettask/webui/frontend/src/components/NamespaceSelector.jsx,sha256=xUIvLPhY_KdUQEHJD7sxim5ZCprk6lK2RRyI3qIs9UQ,5341
|
123
|
+
jettask/webui/frontend/src/components/QueueBacklogChart.jsx,sha256=fzwI4QBUN8lBuE89Sf-7fM3kFgH5J8mbDCPyHjkL8ao,8114
|
124
|
+
jettask/webui/frontend/src/components/QueueBacklogTrend.jsx,sha256=SPwEeGWDoxemeIrcRqgEA0Yx59VUO4cNvOx_EHCIp_I,21255
|
125
|
+
jettask/webui/frontend/src/components/QueueDetailsTable.css,sha256=6d4baJkfPVqjouOkmEt1auqE1lsNr-vfZaMhQpXLQPU,1270
|
126
|
+
jettask/webui/frontend/src/components/QueueDetailsTable.jsx,sha256=mL5g0Gu7Q2kFqV796esa7UMG93khcJ2Fv_3esOAImZM,15443
|
127
|
+
jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx,sha256=Gw-xKmvcJ9ey2MH2luco5Yu8Mp4X6doOQ_kPuKs1dkg,12956
|
128
|
+
jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx,sha256=fE0q62TU_T7tK3zUANwHfvKfZesV54P6B7fgZzEJWXY,13580
|
129
|
+
jettask/webui/frontend/src/components/TaskFilter.jsx,sha256=hcm-6-G0IINhU2vgJBSefJ7s2OIzONPsNRU6OI__RfI,13423
|
130
|
+
jettask/webui/frontend/src/components/TimeRangeSelector.css,sha256=UZftnjo7tFLV0VBIfdkEvDnKn4NItz0r3iwCzgpesCc,497
|
131
|
+
jettask/webui/frontend/src/components/TimeRangeSelector.jsx,sha256=5tJRafL0zJT0SwpH6wXkmOifwphWUvbUhilLoPTVbuo,4861
|
132
|
+
jettask/webui/frontend/src/components/charts/QueueChart.jsx,sha256=UHtImnh_zRzMB6znfI8Sh8fIgZS4Py5a8Y2z88wI2fc,2796
|
133
|
+
jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx,sha256=riJXsPFpgP_4FpB5Eea3DZVOpA_wpcgSQPnEhLe9oNo,2879
|
134
|
+
jettask/webui/frontend/src/components/charts/WorkerChart.jsx,sha256=jek5e97ioLlHcsEIBmoLsCi_j16X2eWgJo95g4sxQIo,901
|
135
|
+
jettask/webui/frontend/src/components/common/StatsCard.jsx,sha256=YYT2yZ9vPKq-GZE0NoTCjwvgyTlhoMulX6Zh-q9_j2s,360
|
136
|
+
jettask/webui/frontend/src/components/layout/AppLayout.css,sha256=We0la8VcffRxUgijAYrjjKIJYHhzWXG7EvbNNKeNy3A,1456
|
137
|
+
jettask/webui/frontend/src/components/layout/AppLayout.jsx,sha256=yaR7VzzHWRgv3Qv3rG3FeggvxhtXFOnnB2990IzF2Ng,1429
|
138
|
+
jettask/webui/frontend/src/components/layout/Header.css,sha256=lLkEsTGmKCRcZtJjWj_qOLfzlj-y61_IVf-WAUpRgFU,1730
|
139
|
+
jettask/webui/frontend/src/components/layout/Header.jsx,sha256=rC-jGhx201jcv5k-ansOrlVU0toI42TXkXZ88Aqx0vY,2639
|
140
|
+
jettask/webui/frontend/src/components/layout/SideMenu.css,sha256=pf_xQuBSfQoh6F-PNE3pZb0gKM2gr-Fg-KjRjivL4Ro,2473
|
141
|
+
jettask/webui/frontend/src/components/layout/SideMenu.jsx,sha256=P-mnj_sB-A3mKmCeqUBMq31vVC1v0fTiMOvjnYUs-Yg,5124
|
142
|
+
jettask/webui/frontend/src/components/layout/TabsNav.css,sha256=snT4sTHox1lUQg_h7sALyOwJYldlY1LergufgKHYpes,4745
|
143
|
+
jettask/webui/frontend/src/components/layout/TabsNav.jsx,sha256=aTapaf0TH9BrHWBFuy2hPcEprA0ClmOMd-8TwznEEL0,5496
|
144
|
+
jettask/webui/frontend/src/components/layout/UserInfo.css,sha256=nTUsAtPK4v_yk38BXi5jW3vLgCy2TNKkpkdpnQlj0QI,3241
|
145
|
+
jettask/webui/frontend/src/components/layout/UserInfo.jsx,sha256=xHyCN0C4FNXkkDE-j_kdBuLo7OsoinLHsLBFt_uz9Iw,5896
|
146
|
+
jettask/webui/frontend/src/contexts/LoadingContext.jsx,sha256=q-Ltc0teW0jt1RJrg2JdiaPiOZXmE9AenVeU8cRI8-M,715
|
147
|
+
jettask/webui/frontend/src/contexts/NamespaceContext.jsx,sha256=84iTN_kZfr67eSEHy6QgbFyDYjKXHCdNmwOGkhUI-eo,2348
|
148
|
+
jettask/webui/frontend/src/contexts/TabsContext.backup.jsx,sha256=yov_rmnzkBC84YA9pgkOrYk2d_p4srEv1rpc57osMxA,7136
|
149
|
+
jettask/webui/frontend/src/pages/Alerts.jsx,sha256=o5fHDHly-sRCR4cXTm0_jDVO-xYRdv4_zTO5bkKZ_Mo,20504
|
150
|
+
jettask/webui/frontend/src/pages/Dashboard.jsx,sha256=uMD5hIpgFgapACkz_exQI-tz5u0jA2KQhbuFjrbasBo,46490
|
151
|
+
jettask/webui/frontend/src/pages/QueueDetail.jsx,sha256=04LlmiIf5qJiPh6ilTIBb1x9mAQHkza7vA2QeM75Nqo,36054
|
152
|
+
jettask/webui/frontend/src/pages/QueueMonitor.jsx,sha256=zp5eR0jBiqZktzsP6NsbTbIo7J7Vr2pwdyozMakB1gE,17749
|
153
|
+
jettask/webui/frontend/src/pages/Queues.jsx,sha256=FRE5EMaW3ln_yU9K1_LyFDLgfwmR7jUqIcC_n6mVPVk,201
|
154
|
+
jettask/webui/frontend/src/pages/ScheduledTasks.jsx,sha256=vJtSAgJImlom-Qb4xHu3tmQZGJMC2qk8GJnHwReej58,23972
|
155
|
+
jettask/webui/frontend/src/pages/Settings.jsx,sha256=9h0K6GJwoRju2GC39kSkThD2lLhcKFWjtpuqGMuT9Bg,26506
|
156
|
+
jettask/webui/frontend/src/pages/Workers.jsx,sha256=S3JI-gDfkBuAIeHQdBCyFg8OuwOa970Kz4cgBGqaAmo,227
|
157
|
+
jettask/webui/frontend/src/pages/Dashboard/index.css,sha256=DNzBEusK0LtDgcVie_Tg-bcKHs_0EtLUPtqnBfYdERs,511
|
158
|
+
jettask/webui/frontend/src/pages/Dashboard/index.jsx,sha256=h-852VjLYcuoDGlkmJ0odrnLU554NsFh332lK99AzWI,7873
|
159
|
+
jettask/webui/frontend/src/services/api.js,sha256=ziLbv2lMzSizb-t_1gdHUuYqt30K5K0D1oMJ7gikKS0,2943
|
160
|
+
jettask/webui/frontend/src/services/queueTrend.js,sha256=z-oADVYIUD923ilyAn0pVUzfhEmo6OGQ6RHRCvgfWgs,4591
|
161
|
+
jettask/webui/frontend/src/utils/suppressWarnings.js,sha256=L2_mnKbkh7_5l0JkLDOxXe6smgMsv7I3sB0ANGrS2JA,678
|
162
|
+
jettask/webui/frontend/src/utils/userPreferences.js,sha256=u9VC47XrG0fl0tfx4dY5BBs4ztmGQR0oS-V8LYVw8a4,4685
|
111
163
|
jettask/webui/models/__init__.py,sha256=5cv0oZksj1B3_rzCqsPmF3Gn9NRZLwzMnaJ8bOKGB4I,25
|
112
164
|
jettask/webui/models/namespace.py,sha256=jDj-hZF_-wXzrWAWVDyZVU0JUWDax9apb4Gyykwg-rE,2006
|
113
165
|
jettask/webui/sql/batch_upsert_functions.sql,sha256=5eWOhOD8gWHhtcop_BrCpZTxPFeyBHtt_leNQZO89Cs,6615
|
114
166
|
jettask/webui/sql/init_database.sql,sha256=CSjhBZldtfV0SGBLTFf576ALaRfCFe3wywpezzkX1TQ,22369
|
115
|
-
jettask/
|
116
|
-
jettask/
|
117
|
-
jettask/
|
118
|
-
jettask/
|
119
|
-
jettask
|
120
|
-
jettask
|
121
|
-
jettask/webui/static/dist/assets/index-8d1935cc.js,sha256=FKAjJHhI3Fz0kROP3JgDI46YvsJxW9zW4eV0nOdLTIQ,3339169
|
122
|
-
jettask-0.2.7.dist-info/licenses/LICENSE,sha256=sKR8OPWvnqxzcHAmnaVSANpRpeM0Z52PNLCB0ZlFN6c,1062
|
123
|
-
jettask-0.2.7.dist-info/METADATA,sha256=faOpINqIDkRT24ppNhgWrZM223kyhhBJ9ytV76KxfOI,2955
|
124
|
-
jettask-0.2.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
125
|
-
jettask-0.2.7.dist-info/entry_points.txt,sha256=VC3byRkkSfRHu_QzczGtpGjcFERkJUlCrD__TFLVqxI,153
|
126
|
-
jettask-0.2.7.dist-info/top_level.txt,sha256=uymyRUF87-OsSurk5NhpeTW0jy3Wltnn91Zoa6jmAaw,8
|
127
|
-
jettask-0.2.7.dist-info/RECORD,,
|
167
|
+
jettask-0.2.8.dist-info/licenses/LICENSE,sha256=sKR8OPWvnqxzcHAmnaVSANpRpeM0Z52PNLCB0ZlFN6c,1062
|
168
|
+
jettask-0.2.8.dist-info/METADATA,sha256=qqiSm1sQjdaDL6zI70A6eFcOZHHEWQGIyfNIuk2cm0o,2955
|
169
|
+
jettask-0.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
170
|
+
jettask-0.2.8.dist-info/entry_points.txt,sha256=VC3byRkkSfRHu_QzczGtpGjcFERkJUlCrD__TFLVqxI,153
|
171
|
+
jettask-0.2.8.dist-info/top_level.txt,sha256=uymyRUF87-OsSurk5NhpeTW0jy3Wltnn91Zoa6jmAaw,8
|
172
|
+
jettask-0.2.8.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
.app-sider{position:fixed;left:0;top:0;bottom:0;z-index:100;box-shadow:2px 0 8px #00000026}.sider-header{height:48px;display:flex;align-items:center;justify-content:center;border-bottom:1px solid #303030}.logo-container{display:flex;align-items:center;justify-content:center}.logo-icon{color:#1890ff;font-size:20px;font-weight:700;width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:rgba(24,144,255,.1);border-radius:4px}.side-menu{padding:12px 0}.side-menu.compact .ant-menu-item{height:56px;line-height:56px;margin:0 0 8px!important;padding:0!important;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#ffffffa6;transition:all .2s}.side-menu.compact .ant-menu-item:hover{color:#fff!important;background:rgba(24,144,255,.1)!important}.side-menu.compact .ant-menu-item-selected{background:transparent!important;color:#1890ff!important;position:relative}.side-menu.compact .ant-menu-item-selected:before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:3px;height:24px;background:#1890ff;border-radius:0 2px 2px 0}.side-menu.compact .ant-menu-item-selected:after{display:none}.menu-item-content{display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;height:100%}.menu-icon-wrapper{display:flex;align-items:center;justify-content:center;font-size:18px;margin-bottom:6px}.menu-label{font-size:11px;line-height:1;white-space:nowrap;opacity:.85;font-weight:400}.side-menu.compact .ant-menu-item .ant-menu-item-icon{margin:0;min-width:auto}.side-menu.compact .ant-menu-title-content{margin:0;padding:0}.ant-menu-item-divider{margin:12px 8px;border-color:#303030}.side-menu.compact .ant-menu-inline{border:none}.side-menu.compact .ant-menu-sub{background:transparent!important}.tabs-nav-container{flex:1;height:100%;display:flex;align-items:center;padding:0 8px 0 0;overflow:hidden;position:relative}.tabs-nav{height:100%;width:100%}.tabs-nav .ant-tabs-nav{height:100%;margin:0;background:transparent;border:none;display:flex;align-items:center}.tabs-nav .ant-tabs-nav:before{border:none}.tabs-nav .ant-tabs-nav-wrap{display:flex;align-items:center;height:auto}.tabs-nav .ant-tabs-nav-list{height:48px;display:flex;align-items:flex-end;gap:2px;padding-top:8px}.tabs-nav .ant-tabs-tab{background:transparent;border:none;border-radius:4px 4px 0 0;margin:0;padding:0 12px;height:40px;line-height:40px;min-width:90px;color:#ffffffa6;transition:all .2s}.tabs-nav .ant-tabs-tab:first-child{border-top-left-radius:0;padding-left:16px}.tabs-nav .ant-tabs-tab:hover{background:rgba(255,255,255,.08);color:#ffffffd9}.tabs-nav .ant-tabs-tab-active{background:#f0f2f5;color:#262626;position:relative;border-left:1px solid #d9d9d9;border-top:1px solid #d9d9d9;border-right:1px solid #d9d9d9;border-bottom:1px solid #f0f2f5;font-weight:500;z-index:20;height:41px;padding:0 12px;margin-bottom:-1px}.tabs-nav .ant-tabs-tab-active:first-child{padding-left:16px;border-left:none}.tabs-nav .ant-tabs-tab-active:hover{background:#f0f2f5;color:#262626}.tabs-nav .ant-tabs-tab-btn{color:inherit}.tab-label{display:inline-flex;align-items:center;gap:4px;-webkit-user-select:none;user-select:none;font-size:13px;white-space:nowrap;line-height:1}.tab-label-center{justify-content:center;width:100%;text-align:center}.tab-icon{display:inline-flex;align-items:center;font-size:12px;opacity:.8}.tabs-nav .ant-tabs-tab-remove{margin-left:8px;margin-right:-6px;color:#ffffff73;font-size:10px;transition:all .2s;padding:0;display:inline-flex;align-items:center;justify-content:center;width:12px;height:12px;opacity:0;visibility:hidden}.tabs-nav .ant-tabs-tab:hover .ant-tabs-tab-remove{opacity:1;visibility:visible}.tabs-nav .ant-tabs-tab-remove:hover{color:#ffffffd9;background:rgba(255,255,255,.1);border-radius:2px}.tabs-nav .ant-tabs-tab-active .ant-tabs-tab-remove{color:#00000073;opacity:1;visibility:visible}.tabs-nav .ant-tabs-tab-active .ant-tabs-tab-remove:hover{color:#000000a6;background:rgba(0,0,0,.06)}.tabs-nav.ant-tabs-card>.ant-tabs-nav .ant-tabs-tab,.tabs-nav.ant-tabs-editable-card>.ant-tabs-nav .ant-tabs-tab{background:transparent;border:none}.tabs-nav.ant-tabs-card>.ant-tabs-nav .ant-tabs-tab-active,.tabs-nav.ant-tabs-editable-card>.ant-tabs-nav .ant-tabs-tab-active{background:#f0f2f5;border-left:1px solid #d9d9d9;border-top:1px solid #d9d9d9;border-right:1px solid #d9d9d9;border-bottom:1px solid #f0f2f5;margin-bottom:-1px}.tabs-nav .ant-tabs-nav-operations,.tabs-nav .ant-tabs-ink-bar{display:none}.tabs-nav .ant-tabs-nav-wrap{overflow:hidden}.tabs-nav .ant-tabs-nav-list{transition:transform .3s}.ant-dropdown-menu{min-width:120px}.ant-dropdown-menu-item{padding:8px 12px}.ant-dropdown-menu-item .anticon{margin-right:8px}.tabs-nav.ant-tabs{line-height:1}.tabs-nav .ant-tabs-content-holder{display:none}.tabs-nav .ant-tabs-tab{position:relative}.tabs-nav .ant-tabs-tab:not(.ant-tabs-tab-active):after{content:"";position:absolute;top:50%;right:0;transform:translateY(-50%);width:1px;height:14px;background:rgba(255,255,255,.08)}.tabs-nav .ant-tabs-tab:last-child:after,.tabs-nav .ant-tabs-tab-active:after,.tabs-nav .ant-tabs-tab-active+.ant-tabs-tab:after{display:none}.user-info-container{display:flex;align-items:center;height:100%;padding:0 16px}.header-icon-btn{color:#ffffffd9;font-size:16px;padding:4px 8px;height:32px;width:32px;display:flex;align-items:center;justify-content:center;transition:all .3s}.header-icon-btn:hover{color:#fff;background:rgba(255,255,255,.1)}.user-avatar-container{display:flex;align-items:center;gap:8px;cursor:pointer;padding:4px 8px;border-radius:4px;transition:all .3s}.user-avatar-container:hover{background:rgba(255,255,255,.1)}.username{color:#ffffffd9;font-size:14px;max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.user-dropdown-menu{min-width:180px}.user-dropdown-menu .ant-menu-item{height:40px;line-height:40px}.theme-badge,.language-badge{background:#f0f0f0;padding:2px 8px;border-radius:4px;font-size:12px;color:#666}.notification-dropdown-container{width:360px}.notification-dropdown{background:#fff;border-radius:4px;box-shadow:0 2px 8px #00000026}.notification-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #f0f0f0}.notification-header span{font-size:16px;font-weight:500}.notification-menu{max-height:400px;overflow-y:auto;border:none}.notification-menu .ant-menu-item{height:auto;padding:12px 16px;border-bottom:1px solid #f0f0f0}.notification-menu .ant-menu-item:last-child{border-bottom:none}.notification-menu .ant-menu-item.unread{background:#e6f7ff}.notification-menu .ant-menu-item:hover{background:#f5f5f5}.notification-item{display:flex;flex-direction:column;gap:4px}.notification-title{font-size:14px;font-weight:500;color:#262626}.notification-description{font-size:12px;color:#8c8c8c;line-height:1.5}.notification-time{font-size:11px;color:#bfbfbf}.empty-notification{padding:32px;text-align:center;color:#8c8c8c;font-size:14px}.notification-footer{padding:8px;text-align:center;border-top:1px solid #f0f0f0}.help-dropdown-menu{min-width:140px}.help-dropdown-menu .ant-menu-item{height:36px;line-height:36px}.ant-badge-count{height:16px;min-width:16px;line-height:16px;font-size:10px;padding:0 4px;right:-6px;top:-6px}.notification-menu::-webkit-scrollbar{width:6px}.notification-menu::-webkit-scrollbar-track{background:#f0f0f0;border-radius:3px}.notification-menu::-webkit-scrollbar-thumb{background:#bfbfbf;border-radius:3px}.notification-menu::-webkit-scrollbar-thumb:hover{background:#8c8c8c}.app-layout{height:100vh;overflow:hidden}.main-layout{transition:margin-left .2s;display:flex;flex-direction:column;height:100vh}.app-header{background:#2a2d31;padding:0;height:48px;line-height:48px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #f0f2f5;z-index:10;position:sticky;top:0}.header-left{flex:1;display:flex;align-items:center;height:100%;overflow:hidden}.header-right{display:flex;align-items:center;height:100%;gap:16px}.app-content{flex:1;overflow:auto;background:#f0f2f5;padding:0}.page-container{height:100%;background:#fff}.app-content::-webkit-scrollbar{width:8px;height:8px}.app-content::-webkit-scrollbar-track{background:#f0f0f0}.app-content::-webkit-scrollbar-thumb{background:#bfbfbf;border-radius:4px}.app-content::-webkit-scrollbar-thumb:hover{background:#8c8c8c}@media (max-width: 768px){.main-layout{margin-left:0!important}.app-sider{position:fixed;z-index:1000;height:100vh;left:-200px;transition:left .2s}.app-sider.ant-layout-sider-collapsed{left:0}}.compact-table .ant-table-cell{padding:4px 8px!important}.compact-table .ant-table-thead .ant-table-cell{padding:6px 8px!important;background-color:#fafafa}.compact-table .ant-pagination{margin:8px 0!important}.compact-table .ant-tag{margin:0;padding:0 6px;line-height:18px;font-size:12px}.compact-table .ant-table-tbody>tr:hover>td{background:#f5f5f5}.compact-table .ant-table{border:none}.compact-table .ant-table-summary{background:#f0f0f0}.compact-table .ant-table-summary .ant-table-cell{font-weight:600;background:#f0f0f0}.compact-table .ant-table-tbody>tr{height:36px}.compact-table .ant-table-body::-webkit-scrollbar{height:6px;width:6px}.compact-table .ant-table-body::-webkit-scrollbar-thumb{background-color:#00000026;border-radius:3px}.compact-table .ant-table-body::-webkit-scrollbar-track{background-color:transparent}.time-range-selector-wrapper{position:relative;display:inline-block}.preset-label-overlay:hover{border-color:#4096ff!important}.time-range-selector-wrapper .ant-picker-input{position:relative}.time-range-selector-wrapper .ant-picker-suffix{position:relative;z-index:2}html,body,#root{height:100%;width:100%;overflow:hidden}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.page-container,.page-wrapper{padding:24px;height:100%;overflow:auto;background:#f0f2f5}.page-header{background:#fff;padding:16px 24px;border-radius:4px;margin-bottom:16px;box-shadow:0 1px 2px #00000008}.page-title{margin:0;font-size:20px;font-weight:500;color:#262626}.ant-card{border-radius:4px;box-shadow:0 1px 2px #00000008}.ant-table{font-size:13px}.ant-table-thead>tr>th{background:#fafafa;font-weight:600}.ant-btn{border-radius:4px;font-weight:400}::-webkit-scrollbar-track{background:#f0f0f0;border-radius:4px}::-webkit-scrollbar-thumb{background:#bfbfbf;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#8c8c8c}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.fade-in{animation:fadeIn .3s ease-in-out}*{margin:0;padding:0;box-sizing:border-box}:root{--primary-color: #1890ff;--success-color: #52c41a;--warning-color: #faad14;--error-color: #ff4d4f;--bg-primary: #ffffff;--bg-secondary: #f5f5f5;--bg-card: #ffffff;--border-color: rgba(0, 0, 0, .06)}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#f5f5f5;min-height:100vh;color:#000000d9}#root{position:relative;z-index:1;min-height:100vh}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:rgba(0,0,0,.05);border-radius:4px}::-webkit-scrollbar-thumb{background:rgba(0,0,0,.2);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.3)}.ant-layout{background:transparent!important}.ant-card{background:#ffffff!important;border:1px solid #f0f0f0}.stats-card{background:#ffffff;border-radius:8px;border:1px solid #f0f0f0;transition:all .3s cubic-bezier(.4,0,.2,1);overflow:hidden;position:relative}.stats-card:before{content:"";position:absolute;top:0;left:0;right:0;height:4px;background:linear-gradient(90deg,transparent,var(--primary-color),transparent);opacity:0;transition:opacity .3s}.stats-card:hover{transform:translateY(-4px);box-shadow:0 4px 12px #0000001a;border-color:#e8e8e8}.stats-card:hover:before{opacity:1}.chart-container{position:relative;height:100%;min-height:300px}.loading-spinner{display:inline-block;width:20px;height:20px;border:2px solid rgba(0,0,0,.1);border-top-color:var(--primary-color);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}
|