jettask 0.2.5__py3-none-any.whl → 0.2.7__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/monitor/run_backlog_collector.py +96 -0
- jettask/monitor/stream_backlog_monitor.py +362 -0
- jettask/pg_consumer/pg_consumer_v2.py +403 -0
- jettask/pg_consumer/sql_utils.py +182 -0
- jettask/scheduler/__init__.py +17 -0
- jettask/scheduler/add_execution_count.sql +11 -0
- jettask/scheduler/add_priority_field.sql +26 -0
- jettask/scheduler/add_scheduler_id.sql +25 -0
- jettask/scheduler/add_scheduler_id_index.sql +10 -0
- jettask/scheduler/loader.py +249 -0
- jettask/scheduler/make_scheduler_id_required.sql +28 -0
- jettask/scheduler/manager.py +696 -0
- jettask/scheduler/migrate_interval_seconds.sql +9 -0
- jettask/scheduler/models.py +200 -0
- jettask/scheduler/multi_namespace_scheduler.py +294 -0
- jettask/scheduler/performance_optimization.sql +45 -0
- jettask/scheduler/run_scheduler.py +186 -0
- jettask/scheduler/scheduler.py +715 -0
- jettask/scheduler/schema.sql +84 -0
- jettask/scheduler/unified_manager.py +450 -0
- jettask/scheduler/unified_scheduler_manager.py +280 -0
- jettask/webui/backend/api/__init__.py +3 -0
- jettask/webui/backend/api/v1/__init__.py +17 -0
- jettask/webui/backend/api/v1/monitoring.py +431 -0
- jettask/webui/backend/api/v1/namespaces.py +504 -0
- jettask/webui/backend/api/v1/queues.py +342 -0
- jettask/webui/backend/api/v1/tasks.py +367 -0
- jettask/webui/backend/core/__init__.py +3 -0
- jettask/webui/backend/core/cache.py +221 -0
- jettask/webui/backend/core/database.py +200 -0
- jettask/webui/backend/core/exceptions.py +102 -0
- jettask/webui/backend/models/__init__.py +3 -0
- jettask/webui/backend/models/requests.py +236 -0
- jettask/webui/backend/models/responses.py +230 -0
- jettask/webui/backend/services/__init__.py +3 -0
- jettask/webui/frontend/index.html +13 -0
- jettask/webui/models/__init__.py +3 -0
- jettask/webui/models/namespace.py +63 -0
- jettask/webui/sql/batch_upsert_functions.sql +178 -0
- jettask/webui/sql/init_database.sql +640 -0
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/METADATA +80 -10
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/RECORD +46 -53
- jettask/webui/frontend/package-lock.json +0 -4833
- jettask/webui/frontend/package.json +0 -30
- jettask/webui/frontend/src/App.css +0 -109
- jettask/webui/frontend/src/App.jsx +0 -66
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +0 -166
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +0 -298
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +0 -638
- jettask/webui/frontend/src/components/QueueDetailsTable.css +0 -65
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +0 -487
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +0 -465
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +0 -423
- jettask/webui/frontend/src/components/TaskFilter.jsx +0 -425
- jettask/webui/frontend/src/components/TimeRangeSelector.css +0 -21
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +0 -160
- jettask/webui/frontend/src/components/charts/QueueChart.jsx +0 -111
- jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +0 -115
- jettask/webui/frontend/src/components/charts/WorkerChart.jsx +0 -40
- jettask/webui/frontend/src/components/common/StatsCard.jsx +0 -18
- jettask/webui/frontend/src/components/layout/AppLayout.css +0 -95
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +0 -49
- jettask/webui/frontend/src/components/layout/Header.css +0 -106
- jettask/webui/frontend/src/components/layout/Header.jsx +0 -106
- jettask/webui/frontend/src/components/layout/SideMenu.css +0 -137
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +0 -209
- jettask/webui/frontend/src/components/layout/TabsNav.css +0 -244
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +0 -206
- jettask/webui/frontend/src/components/layout/UserInfo.css +0 -197
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +0 -197
- jettask/webui/frontend/src/contexts/LoadingContext.jsx +0 -27
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +0 -72
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +0 -245
- jettask/webui/frontend/src/index.css +0 -114
- jettask/webui/frontend/src/main.jsx +0 -20
- jettask/webui/frontend/src/pages/Alerts.jsx +0 -684
- jettask/webui/frontend/src/pages/Dashboard/index.css +0 -35
- jettask/webui/frontend/src/pages/Dashboard/index.jsx +0 -281
- jettask/webui/frontend/src/pages/Dashboard.jsx +0 -1330
- jettask/webui/frontend/src/pages/QueueDetail.jsx +0 -1117
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +0 -527
- jettask/webui/frontend/src/pages/Queues.jsx +0 -12
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +0 -809
- jettask/webui/frontend/src/pages/Settings.jsx +0 -800
- jettask/webui/frontend/src/pages/Workers.jsx +0 -12
- jettask/webui/frontend/src/services/api.js +0 -114
- jettask/webui/frontend/src/services/queueTrend.js +0 -152
- jettask/webui/frontend/src/utils/suppressWarnings.js +0 -22
- jettask/webui/frontend/src/utils/userPreferences.js +0 -154
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/WHEEL +0 -0
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/top_level.txt +0 -0
@@ -1,425 +0,0 @@
|
|
1
|
-
import React, { useState } from 'react';
|
2
|
-
import { Button, Popover, Form, Select, Input, Space, Tag, message } from 'antd';
|
3
|
-
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
4
|
-
|
5
|
-
const { Option } = Select;
|
6
|
-
|
7
|
-
// 可用的字段列表
|
8
|
-
const AVAILABLE_FIELDS = [
|
9
|
-
{ value: 'id', label: '任务ID', type: 'string' },
|
10
|
-
{ value: 'task_name', label: '任务名称', type: 'string' },
|
11
|
-
{ value: 'consumer_group', label: '消费者组', type: 'string' },
|
12
|
-
{ value: 'status', label: '状态', type: 'enum', options: ['pending', 'running', 'success', 'error', 'rejected'] },
|
13
|
-
{ value: 'worker_id', label: 'Worker ID', type: 'string' },
|
14
|
-
{ value: 'task_data', label: '任务参数(JSON)', type: 'json' },
|
15
|
-
{ value: 'result', label: '执行结果(JSON)', type: 'json' },
|
16
|
-
{ value: 'created_at', label: '创建时间', type: 'datetime' },
|
17
|
-
{ value: 'started_at', label: '开始时间', type: 'datetime' },
|
18
|
-
{ value: 'completed_at', label: '完成时间', type: 'datetime' },
|
19
|
-
{ value: 'retry_count', label: '重试次数', type: 'number' },
|
20
|
-
{ value: 'priority', label: '优先级', type: 'number' },
|
21
|
-
{ value: 'max_retry', label: '最大重试次数', type: 'number' },
|
22
|
-
{ value: 'error_message', label: '错误信息', type: 'string' },
|
23
|
-
];
|
24
|
-
|
25
|
-
// 操作符列表
|
26
|
-
const OPERATORS = {
|
27
|
-
string: [
|
28
|
-
{ value: 'eq', label: '等于' },
|
29
|
-
{ value: 'ne', label: '不等于' },
|
30
|
-
{ value: 'contains', label: '包含' },
|
31
|
-
{ value: 'starts_with', label: '开始于' },
|
32
|
-
{ value: 'ends_with', label: '结束于' },
|
33
|
-
{ value: 'is_null', label: '为空' },
|
34
|
-
{ value: 'is_not_null', label: '不为空' },
|
35
|
-
],
|
36
|
-
number: [
|
37
|
-
{ value: 'eq', label: '等于' },
|
38
|
-
{ value: 'ne', label: '不等于' },
|
39
|
-
{ value: 'gt', label: '大于' },
|
40
|
-
{ value: 'lt', label: '小于' },
|
41
|
-
{ value: 'gte', label: '大于等于' },
|
42
|
-
{ value: 'lte', label: '小于等于' },
|
43
|
-
{ value: 'is_null', label: '为空' },
|
44
|
-
{ value: 'is_not_null', label: '不为空' },
|
45
|
-
],
|
46
|
-
datetime: [
|
47
|
-
{ value: 'gt', label: '晚于' },
|
48
|
-
{ value: 'lt', label: '早于' },
|
49
|
-
{ value: 'gte', label: '不早于' },
|
50
|
-
{ value: 'lte', label: '不晚于' },
|
51
|
-
{ value: 'is_null', label: '为空' },
|
52
|
-
{ value: 'is_not_null', label: '不为空' },
|
53
|
-
],
|
54
|
-
enum: [
|
55
|
-
{ value: 'eq', label: '等于' },
|
56
|
-
{ value: 'ne', label: '不等于' },
|
57
|
-
{ value: 'in', label: '在列表中' },
|
58
|
-
{ value: 'not_in', label: '不在列表中' },
|
59
|
-
],
|
60
|
-
json: [
|
61
|
-
{ value: 'contains', label: '包含文本' },
|
62
|
-
{ value: 'json_key_exists', label: '包含键名' },
|
63
|
-
{ value: 'json_path_value', label: 'JSON路径值' },
|
64
|
-
{ value: 'is_null', label: '为空' },
|
65
|
-
{ value: 'is_not_null', label: '不为空' },
|
66
|
-
],
|
67
|
-
};
|
68
|
-
|
69
|
-
function TaskFilter({ filters, onFiltersChange }) {
|
70
|
-
const [visible, setVisible] = useState(false);
|
71
|
-
const [form] = Form.useForm();
|
72
|
-
const [selectedField, setSelectedField] = useState(null);
|
73
|
-
const [selectedOperator, setSelectedOperator] = useState(null);
|
74
|
-
const [disabledFilters, setDisabledFilters] = useState(new Set()); // 记录被禁用的筛选条件索引
|
75
|
-
|
76
|
-
// 获取当前字段的类型
|
77
|
-
const getFieldType = (fieldValue) => {
|
78
|
-
const field = AVAILABLE_FIELDS.find(f => f.value === fieldValue);
|
79
|
-
return field ? field.type : 'string';
|
80
|
-
};
|
81
|
-
|
82
|
-
// 获取当前字段的选项(用于枚举类型)
|
83
|
-
const getFieldOptions = (fieldValue) => {
|
84
|
-
const field = AVAILABLE_FIELDS.find(f => f.value === fieldValue);
|
85
|
-
return field && field.options ? field.options : [];
|
86
|
-
};
|
87
|
-
|
88
|
-
// 添加筛选条件
|
89
|
-
const handleAddFilter = () => {
|
90
|
-
form.validateFields().then(values => {
|
91
|
-
const newFilter = {
|
92
|
-
id: Date.now(), // 添加唯一ID
|
93
|
-
field: values.field,
|
94
|
-
operator: values.operator,
|
95
|
-
value: values.value,
|
96
|
-
enabled: true, // 默认启用
|
97
|
-
};
|
98
|
-
|
99
|
-
// 处理特殊操作符
|
100
|
-
if (values.operator === 'is_null' || values.operator === 'is_not_null') {
|
101
|
-
newFilter.value = null;
|
102
|
-
} else if (values.operator === 'in' || values.operator === 'not_in') {
|
103
|
-
// 如果是枚举类型的多选
|
104
|
-
if (getFieldType(values.field) === 'enum') {
|
105
|
-
newFilter.value = values.value; // 已经是数组
|
106
|
-
} else {
|
107
|
-
// 普通文本,用逗号分隔
|
108
|
-
newFilter.value = values.value.split(',').map(v => v.trim());
|
109
|
-
}
|
110
|
-
}
|
111
|
-
|
112
|
-
const updatedFilters = [...(filters || []), newFilter];
|
113
|
-
onFiltersChange(updatedFilters);
|
114
|
-
|
115
|
-
// 重置表单
|
116
|
-
form.resetFields();
|
117
|
-
setSelectedField(null);
|
118
|
-
setSelectedOperator(null);
|
119
|
-
setVisible(false);
|
120
|
-
message.success('筛选条件已添加');
|
121
|
-
});
|
122
|
-
};
|
123
|
-
|
124
|
-
// 切换筛选条件的启用/禁用状态
|
125
|
-
const handleToggleFilter = (index) => {
|
126
|
-
const updatedFilters = [...filters];
|
127
|
-
updatedFilters[index] = {
|
128
|
-
...updatedFilters[index],
|
129
|
-
enabled: !updatedFilters[index].enabled
|
130
|
-
};
|
131
|
-
onFiltersChange(updatedFilters);
|
132
|
-
};
|
133
|
-
|
134
|
-
// 删除筛选条件
|
135
|
-
const handleRemoveFilter = (index) => {
|
136
|
-
const updatedFilters = filters.filter((_, i) => i !== index);
|
137
|
-
onFiltersChange(updatedFilters);
|
138
|
-
message.success('筛选条件已删除');
|
139
|
-
};
|
140
|
-
|
141
|
-
// 渲染筛选条件标签
|
142
|
-
const renderFilterTag = (filter, index) => {
|
143
|
-
const field = AVAILABLE_FIELDS.find(f => f.value === filter.field);
|
144
|
-
const fieldLabel = field ? field.label : filter.field;
|
145
|
-
|
146
|
-
const operator = OPERATORS[getFieldType(filter.field)]?.find(op => op.value === filter.operator);
|
147
|
-
const operatorLabel = operator ? operator.label : filter.operator;
|
148
|
-
|
149
|
-
let valueLabel = filter.value;
|
150
|
-
if (filter.operator === 'is_null' || filter.operator === 'is_not_null') {
|
151
|
-
valueLabel = '';
|
152
|
-
} else if (Array.isArray(filter.value)) {
|
153
|
-
valueLabel = filter.value.join(', ');
|
154
|
-
} else if (filter.operator === 'json_key_exists') {
|
155
|
-
valueLabel = `键: ${filter.value}`;
|
156
|
-
} else if (filter.operator === 'json_path_value') {
|
157
|
-
// 显示更友好的格式
|
158
|
-
if (filter.value && filter.value.includes('=')) {
|
159
|
-
const [path, val] = filter.value.split('=', 2);
|
160
|
-
valueLabel = `${path} = ${val}`;
|
161
|
-
}
|
162
|
-
}
|
163
|
-
|
164
|
-
const isDisabled = filter.enabled === false;
|
165
|
-
|
166
|
-
return (
|
167
|
-
<Tag
|
168
|
-
key={index}
|
169
|
-
closable
|
170
|
-
onClose={() => handleRemoveFilter(index)}
|
171
|
-
onClick={() => handleToggleFilter(index)}
|
172
|
-
style={{
|
173
|
-
margin: 0,
|
174
|
-
height: '24px',
|
175
|
-
lineHeight: '22px',
|
176
|
-
display: 'inline-flex',
|
177
|
-
alignItems: 'center',
|
178
|
-
cursor: 'pointer',
|
179
|
-
opacity: isDisabled ? 0.5 : 1,
|
180
|
-
textDecoration: isDisabled ? 'line-through' : 'none',
|
181
|
-
backgroundColor: isDisabled ? '#f5f5f5' : undefined,
|
182
|
-
borderStyle: isDisabled ? 'dashed' : 'solid',
|
183
|
-
}}
|
184
|
-
color={isDisabled ? 'default' : undefined}
|
185
|
-
>
|
186
|
-
{fieldLabel} {operatorLabel} {valueLabel}
|
187
|
-
</Tag>
|
188
|
-
);
|
189
|
-
};
|
190
|
-
|
191
|
-
// 渲染值输入组件
|
192
|
-
const renderValueInput = () => {
|
193
|
-
if (!selectedField || !selectedOperator) return null;
|
194
|
-
|
195
|
-
// 如果是空值判断,不需要输入值
|
196
|
-
if (selectedOperator === 'is_null' || selectedOperator === 'is_not_null') {
|
197
|
-
return null;
|
198
|
-
}
|
199
|
-
|
200
|
-
const fieldType = getFieldType(selectedField);
|
201
|
-
const fieldOptions = getFieldOptions(selectedField);
|
202
|
-
|
203
|
-
// 枚举类型的多选
|
204
|
-
if (fieldType === 'enum' && (selectedOperator === 'in' || selectedOperator === 'not_in')) {
|
205
|
-
return (
|
206
|
-
<Form.Item
|
207
|
-
name="value"
|
208
|
-
label="值"
|
209
|
-
rules={[{ required: true, message: '请选择值' }]}
|
210
|
-
>
|
211
|
-
<Select mode="multiple" placeholder="选择值">
|
212
|
-
{fieldOptions.map(opt => (
|
213
|
-
<Option key={opt} value={opt}>{opt}</Option>
|
214
|
-
))}
|
215
|
-
</Select>
|
216
|
-
</Form.Item>
|
217
|
-
);
|
218
|
-
}
|
219
|
-
|
220
|
-
// 枚举类型的单选
|
221
|
-
if (fieldType === 'enum') {
|
222
|
-
return (
|
223
|
-
<Form.Item
|
224
|
-
name="value"
|
225
|
-
label="值"
|
226
|
-
rules={[{ required: true, message: '请选择值' }]}
|
227
|
-
>
|
228
|
-
<Select placeholder="选择值">
|
229
|
-
{fieldOptions.map(opt => (
|
230
|
-
<Option key={opt} value={opt}>{opt}</Option>
|
231
|
-
))}
|
232
|
-
</Select>
|
233
|
-
</Form.Item>
|
234
|
-
);
|
235
|
-
}
|
236
|
-
|
237
|
-
// 日期时间类型
|
238
|
-
if (fieldType === 'datetime') {
|
239
|
-
return (
|
240
|
-
<Form.Item
|
241
|
-
name="value"
|
242
|
-
label="值"
|
243
|
-
rules={[{ required: true, message: '请输入值' }]}
|
244
|
-
>
|
245
|
-
<Input type="datetime-local" />
|
246
|
-
</Form.Item>
|
247
|
-
);
|
248
|
-
}
|
249
|
-
|
250
|
-
// 数字类型
|
251
|
-
if (fieldType === 'number') {
|
252
|
-
return (
|
253
|
-
<Form.Item
|
254
|
-
name="value"
|
255
|
-
label="值"
|
256
|
-
rules={[{ required: true, message: '请输入值' }]}
|
257
|
-
>
|
258
|
-
<Input type="number" placeholder="输入数字" />
|
259
|
-
</Form.Item>
|
260
|
-
);
|
261
|
-
}
|
262
|
-
|
263
|
-
// JSON类型
|
264
|
-
if (fieldType === 'json') {
|
265
|
-
// 根据不同的操作符显示不同的输入提示
|
266
|
-
let label = '搜索内容';
|
267
|
-
let placeholder = '输入搜索内容';
|
268
|
-
let extra = '';
|
269
|
-
|
270
|
-
if (selectedOperator === 'json_key_exists') {
|
271
|
-
label = '键名';
|
272
|
-
placeholder = '输入要查找的键名,如:user_id';
|
273
|
-
extra = '检查JSON中是否存在指定的键';
|
274
|
-
} else if (selectedOperator === 'json_path_value') {
|
275
|
-
label = 'JSON路径和值';
|
276
|
-
placeholder = '路径=值,如:$.user_id=123 或 $.data.status=active';
|
277
|
-
extra = '使用JSON路径语法,格式:路径=值。支持嵌套路径如 $.data.user.name';
|
278
|
-
} else if (selectedOperator === 'contains') {
|
279
|
-
label = '搜索文本';
|
280
|
-
placeholder = '输入要搜索的文本内容';
|
281
|
-
extra = '在JSON数据中搜索包含此文本的任务';
|
282
|
-
}
|
283
|
-
|
284
|
-
return (
|
285
|
-
<Form.Item
|
286
|
-
name="value"
|
287
|
-
label={label}
|
288
|
-
rules={[{ required: true, message: `请输入${label}` }]}
|
289
|
-
extra={extra}
|
290
|
-
>
|
291
|
-
<Input.TextArea
|
292
|
-
placeholder={placeholder}
|
293
|
-
rows={2}
|
294
|
-
spellCheck={false} // 关闭拼写检查,避免JSON输入时的红线
|
295
|
-
/>
|
296
|
-
</Form.Item>
|
297
|
-
);
|
298
|
-
}
|
299
|
-
|
300
|
-
// 默认文本输入
|
301
|
-
return (
|
302
|
-
<Form.Item
|
303
|
-
name="value"
|
304
|
-
label="值"
|
305
|
-
rules={[{ required: true, message: '请输入值' }]}
|
306
|
-
extra={
|
307
|
-
(selectedOperator === 'in' || selectedOperator === 'not_in')
|
308
|
-
? '多个值用逗号分隔' : null
|
309
|
-
}
|
310
|
-
>
|
311
|
-
<Input placeholder="输入值" />
|
312
|
-
</Form.Item>
|
313
|
-
);
|
314
|
-
};
|
315
|
-
|
316
|
-
const filterContent = (
|
317
|
-
<div style={{ width: 350 }}>
|
318
|
-
<Form
|
319
|
-
form={form}
|
320
|
-
layout="vertical"
|
321
|
-
onFinish={handleAddFilter}
|
322
|
-
>
|
323
|
-
<Form.Item
|
324
|
-
name="field"
|
325
|
-
label="字段"
|
326
|
-
rules={[{ required: true, message: '请选择字段' }]}
|
327
|
-
>
|
328
|
-
<Select
|
329
|
-
placeholder="选择字段"
|
330
|
-
onChange={(value) => {
|
331
|
-
setSelectedField(value);
|
332
|
-
setSelectedOperator(null);
|
333
|
-
form.setFieldsValue({ operator: undefined, value: undefined });
|
334
|
-
}}
|
335
|
-
>
|
336
|
-
{AVAILABLE_FIELDS.map(field => (
|
337
|
-
<Option key={field.value} value={field.value}>
|
338
|
-
{field.label}
|
339
|
-
</Option>
|
340
|
-
))}
|
341
|
-
</Select>
|
342
|
-
</Form.Item>
|
343
|
-
|
344
|
-
{selectedField && (
|
345
|
-
<Form.Item
|
346
|
-
name="operator"
|
347
|
-
label="操作符"
|
348
|
-
rules={[{ required: true, message: '请选择操作符' }]}
|
349
|
-
>
|
350
|
-
<Select
|
351
|
-
placeholder="选择操作符"
|
352
|
-
onChange={(value) => {
|
353
|
-
setSelectedOperator(value);
|
354
|
-
form.setFieldsValue({ value: undefined });
|
355
|
-
}}
|
356
|
-
>
|
357
|
-
{OPERATORS[getFieldType(selectedField)]?.map(op => (
|
358
|
-
<Option key={op.value} value={op.value}>
|
359
|
-
{op.label}
|
360
|
-
</Option>
|
361
|
-
))}
|
362
|
-
</Select>
|
363
|
-
</Form.Item>
|
364
|
-
)}
|
365
|
-
|
366
|
-
{renderValueInput()}
|
367
|
-
|
368
|
-
<Form.Item>
|
369
|
-
<Space>
|
370
|
-
<Button type="primary" htmlType="submit">
|
371
|
-
添加筛选条件
|
372
|
-
</Button>
|
373
|
-
<Button onClick={() => {
|
374
|
-
setVisible(false);
|
375
|
-
form.resetFields();
|
376
|
-
setSelectedField(null);
|
377
|
-
setSelectedOperator(null);
|
378
|
-
}}>
|
379
|
-
取消
|
380
|
-
</Button>
|
381
|
-
</Space>
|
382
|
-
</Form.Item>
|
383
|
-
</Form>
|
384
|
-
</div>
|
385
|
-
);
|
386
|
-
|
387
|
-
return (
|
388
|
-
<div style={{
|
389
|
-
display: 'inline-flex',
|
390
|
-
alignItems: 'center',
|
391
|
-
flexWrap: 'wrap',
|
392
|
-
gap: '8px',
|
393
|
-
verticalAlign: 'middle'
|
394
|
-
}}>
|
395
|
-
{filters && filters.map((filter, index) => renderFilterTag(filter, index))}
|
396
|
-
|
397
|
-
<Popover
|
398
|
-
content={filterContent}
|
399
|
-
title="添加筛选条件"
|
400
|
-
trigger="click"
|
401
|
-
open={visible}
|
402
|
-
onOpenChange={setVisible}
|
403
|
-
placement="bottomLeft"
|
404
|
-
>
|
405
|
-
<Button
|
406
|
-
type="dashed"
|
407
|
-
icon={<PlusOutlined />}
|
408
|
-
size="small"
|
409
|
-
style={{
|
410
|
-
height: '24px',
|
411
|
-
padding: '0 8px',
|
412
|
-
display: 'inline-flex',
|
413
|
-
alignItems: 'center',
|
414
|
-
lineHeight: '22px',
|
415
|
-
verticalAlign: 'middle'
|
416
|
-
}}
|
417
|
-
>
|
418
|
-
Add Filter
|
419
|
-
</Button>
|
420
|
-
</Popover>
|
421
|
-
</div>
|
422
|
-
);
|
423
|
-
}
|
424
|
-
|
425
|
-
export default TaskFilter;
|
@@ -1,21 +0,0 @@
|
|
1
|
-
/* 时间范围选择器样式 */
|
2
|
-
.time-range-selector-wrapper {
|
3
|
-
position: relative;
|
4
|
-
display: inline-block;
|
5
|
-
}
|
6
|
-
|
7
|
-
/* 预设标签覆盖层 hover 效果 */
|
8
|
-
.preset-label-overlay:hover {
|
9
|
-
border-color: #4096ff !important;
|
10
|
-
}
|
11
|
-
|
12
|
-
/* 隐藏下面的 RangePicker 输入框文字,但保留交互 */
|
13
|
-
.time-range-selector-wrapper .ant-picker-input {
|
14
|
-
position: relative;
|
15
|
-
}
|
16
|
-
|
17
|
-
/* 确保日历图标在覆盖层之上 */
|
18
|
-
.time-range-selector-wrapper .ant-picker-suffix {
|
19
|
-
position: relative;
|
20
|
-
z-index: 2;
|
21
|
-
}
|
@@ -1,160 +0,0 @@
|
|
1
|
-
import { useState, useEffect } from 'react';
|
2
|
-
import { DatePicker } from 'antd';
|
3
|
-
import dayjs from 'dayjs';
|
4
|
-
import './TimeRangeSelector.css';
|
5
|
-
|
6
|
-
const { RangePicker } = DatePicker;
|
7
|
-
|
8
|
-
// 预设时间范围选项
|
9
|
-
const PRESET_RANGES = {
|
10
|
-
'最近15分钟': () => [dayjs().subtract(15, 'minute'), dayjs()],
|
11
|
-
'最近30分钟': () => [dayjs().subtract(30, 'minute'), dayjs()],
|
12
|
-
'最近1小时': () => [dayjs().subtract(1, 'hour'), dayjs()],
|
13
|
-
'最近3小时': () => [dayjs().subtract(3, 'hour'), dayjs()],
|
14
|
-
'最近6小时': () => [dayjs().subtract(6, 'hour'), dayjs()],
|
15
|
-
'最近12小时': () => [dayjs().subtract(12, 'hour'), dayjs()],
|
16
|
-
'最近24小时': () => [dayjs().subtract(24, 'hour'), dayjs()],
|
17
|
-
'最近7天': () => [dayjs().subtract(7, 'day'), dayjs()],
|
18
|
-
'最近30天': () => [dayjs().subtract(30, 'day'), dayjs()],
|
19
|
-
};
|
20
|
-
|
21
|
-
// 时间范围值映射
|
22
|
-
const TIME_RANGE_MAP = {
|
23
|
-
'最近15分钟': '15m',
|
24
|
-
'最近30分钟': '30m',
|
25
|
-
'最近1小时': '1h',
|
26
|
-
'最近3小时': '3h',
|
27
|
-
'最近6小时': '6h',
|
28
|
-
'最近12小时': '12h',
|
29
|
-
'最近24小时': '24h',
|
30
|
-
'最近7天': '7d',
|
31
|
-
'最近30天': '30d',
|
32
|
-
};
|
33
|
-
|
34
|
-
// 反向映射,从值到标签
|
35
|
-
const VALUE_TO_LABEL_MAP = Object.fromEntries(
|
36
|
-
Object.entries(TIME_RANGE_MAP).map(([label, value]) => [value, label])
|
37
|
-
);
|
38
|
-
|
39
|
-
function TimeRangeSelector({
|
40
|
-
value,
|
41
|
-
onChange,
|
42
|
-
customValue,
|
43
|
-
onCustomChange,
|
44
|
-
style = {},
|
45
|
-
...props
|
46
|
-
}) {
|
47
|
-
const [isOpen, setIsOpen] = useState(false);
|
48
|
-
const [presetLabel, setPresetLabel] = useState(
|
49
|
-
value !== 'custom' && VALUE_TO_LABEL_MAP[value] ? VALUE_TO_LABEL_MAP[value] : null
|
50
|
-
);
|
51
|
-
|
52
|
-
// 当外部value改变时,更新预设标签
|
53
|
-
useEffect(() => {
|
54
|
-
if (value !== 'custom' && VALUE_TO_LABEL_MAP[value]) {
|
55
|
-
setPresetLabel(VALUE_TO_LABEL_MAP[value]);
|
56
|
-
} else if (value === 'custom') {
|
57
|
-
setPresetLabel(null);
|
58
|
-
}
|
59
|
-
}, [value]);
|
60
|
-
|
61
|
-
// 处理RangePicker变化
|
62
|
-
const handleRangeChange = (dates) => {
|
63
|
-
console.log('[TimeRangeSelector] handleRangeChange 被调用, dates:', dates);
|
64
|
-
if (!dates || dates.length !== 2) {
|
65
|
-
console.log('[TimeRangeSelector] dates 无效,返回');
|
66
|
-
return;
|
67
|
-
}
|
68
|
-
|
69
|
-
const [start, end] = dates;
|
70
|
-
|
71
|
-
// 检查是否匹配预设时间范围
|
72
|
-
let matchedPreset = null;
|
73
|
-
for (const [label, getRangeFn] of Object.entries(PRESET_RANGES)) {
|
74
|
-
const [presetStart, presetEnd] = getRangeFn();
|
75
|
-
// 允许2秒的误差
|
76
|
-
if (
|
77
|
-
Math.abs(start.diff(presetStart, 'second')) <= 2 &&
|
78
|
-
Math.abs(end.diff(presetEnd, 'second')) <= 2
|
79
|
-
) {
|
80
|
-
matchedPreset = label;
|
81
|
-
break;
|
82
|
-
}
|
83
|
-
}
|
84
|
-
|
85
|
-
console.log('[TimeRangeSelector] handleRangeChange - matchedPreset:', matchedPreset, 'start:', start.format(), 'end:', end.format());
|
86
|
-
|
87
|
-
if (matchedPreset) {
|
88
|
-
// 匹配到预设
|
89
|
-
setPresetLabel(matchedPreset);
|
90
|
-
const presetValue = TIME_RANGE_MAP[matchedPreset];
|
91
|
-
console.log('[TimeRangeSelector] 设置预设值:', presetValue, '调用 onChange');
|
92
|
-
onChange(presetValue);
|
93
|
-
onCustomChange(null);
|
94
|
-
} else {
|
95
|
-
// 自定义时间范围
|
96
|
-
setPresetLabel(null);
|
97
|
-
console.log('[TimeRangeSelector] 设置自定义时间范围');
|
98
|
-
onChange('custom');
|
99
|
-
onCustomChange(dates);
|
100
|
-
}
|
101
|
-
};
|
102
|
-
|
103
|
-
// 获取RangePicker的值
|
104
|
-
const getRangePickerValue = () => {
|
105
|
-
if (value === 'custom' && customValue) {
|
106
|
-
return customValue;
|
107
|
-
}
|
108
|
-
// 预设时返回空,让RangePicker不显示具体时间
|
109
|
-
return undefined;
|
110
|
-
};
|
111
|
-
|
112
|
-
return (
|
113
|
-
<div className="time-range-selector-wrapper" style={{ position: 'relative', display: 'inline-block', ...style }}>
|
114
|
-
<RangePicker
|
115
|
-
showTime
|
116
|
-
format="YYYY-MM-DD HH:mm:ss"
|
117
|
-
value={getRangePickerValue()}
|
118
|
-
onChange={handleRangeChange}
|
119
|
-
placeholder={['开始时间', '结束时间']}
|
120
|
-
presets={Object.entries(PRESET_RANGES).map(([label, getRangeFn]) => ({
|
121
|
-
label,
|
122
|
-
value: getRangeFn(),
|
123
|
-
}))}
|
124
|
-
style={{ width: 360 }}
|
125
|
-
open={isOpen}
|
126
|
-
onOpenChange={setIsOpen}
|
127
|
-
allowClear={false}
|
128
|
-
{...props}
|
129
|
-
/>
|
130
|
-
|
131
|
-
{/* 预设标签覆盖层 */}
|
132
|
-
{presetLabel && !isOpen && (
|
133
|
-
<div
|
134
|
-
className="preset-label-overlay"
|
135
|
-
onClick={() => setIsOpen(true)}
|
136
|
-
style={{
|
137
|
-
position: 'absolute',
|
138
|
-
top: 0,
|
139
|
-
left: 0,
|
140
|
-
right: 0,
|
141
|
-
bottom: 0,
|
142
|
-
display: 'flex',
|
143
|
-
alignItems: 'center',
|
144
|
-
paddingLeft: 11,
|
145
|
-
paddingRight: 35,
|
146
|
-
backgroundColor: '#fff',
|
147
|
-
border: '1px solid #d9d9d9',
|
148
|
-
borderRadius: 6,
|
149
|
-
cursor: 'pointer',
|
150
|
-
zIndex: 1,
|
151
|
-
}}
|
152
|
-
>
|
153
|
-
<span style={{ color: 'rgba(0, 0, 0, 0.88)' }}>{presetLabel}</span>
|
154
|
-
</div>
|
155
|
-
)}
|
156
|
-
</div>
|
157
|
-
);
|
158
|
-
}
|
159
|
-
|
160
|
-
export default TimeRangeSelector;
|