jettask 0.2.1__py3-none-any.whl → 0.2.4__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/constants.py +213 -0
- jettask/core/app.py +525 -205
- jettask/core/cli.py +193 -185
- jettask/core/consumer_manager.py +126 -34
- jettask/core/context.py +3 -0
- jettask/core/enums.py +137 -0
- jettask/core/event_pool.py +501 -168
- jettask/core/message.py +147 -0
- jettask/core/offline_worker_recovery.py +181 -114
- jettask/core/task.py +10 -174
- jettask/core/task_batch.py +153 -0
- jettask/core/unified_manager_base.py +243 -0
- jettask/core/worker_scanner.py +54 -54
- jettask/executors/asyncio.py +184 -64
- jettask/webui/backend/config.py +51 -0
- jettask/webui/backend/data_access.py +2083 -92
- jettask/webui/backend/data_api.py +3294 -0
- jettask/webui/backend/dependencies.py +261 -0
- jettask/webui/backend/init_meta_db.py +158 -0
- jettask/webui/backend/main.py +1358 -69
- jettask/webui/backend/main_unified.py +78 -0
- jettask/webui/backend/main_v2.py +394 -0
- jettask/webui/backend/namespace_api.py +295 -0
- jettask/webui/backend/namespace_api_old.py +294 -0
- jettask/webui/backend/namespace_data_access.py +611 -0
- jettask/webui/backend/queue_backlog_api.py +727 -0
- jettask/webui/backend/queue_stats_v2.py +521 -0
- jettask/webui/backend/redis_monitor_api.py +476 -0
- jettask/webui/backend/unified_api_router.py +1601 -0
- jettask/webui/db_init.py +204 -32
- jettask/webui/frontend/package-lock.json +492 -1
- jettask/webui/frontend/package.json +4 -1
- jettask/webui/frontend/src/App.css +105 -7
- jettask/webui/frontend/src/App.jsx +49 -20
- 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/layout/AppLayout.css +95 -0
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
- jettask/webui/frontend/src/components/layout/Header.css +34 -10
- jettask/webui/frontend/src/components/layout/Header.jsx +31 -23
- 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/NamespaceContext.jsx +72 -0
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
- jettask/webui/frontend/src/main.jsx +1 -0
- jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
- jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
- jettask/webui/frontend/src/pages/QueueDetail.jsx +1109 -10
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +236 -115
- jettask/webui/frontend/src/pages/Queues.jsx +5 -1
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
- jettask/webui/frontend/src/pages/Settings.jsx +800 -0
- jettask/webui/frontend/src/services/api.js +7 -5
- jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
- jettask/webui/frontend/src/utils/userPreferences.js +154 -0
- jettask/webui/multi_namespace_consumer.py +543 -0
- jettask/webui/pg_consumer.py +983 -246
- jettask/webui/static/dist/assets/index-7129cfe1.css +1 -0
- jettask/webui/static/dist/assets/index-8d1935cc.js +774 -0
- jettask/webui/static/dist/index.html +2 -2
- jettask/webui/task_center.py +216 -0
- jettask/webui/task_center_client.py +150 -0
- jettask/webui/unified_consumer_manager.py +193 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/METADATA +1 -1
- jettask-0.2.4.dist-info/RECORD +134 -0
- jettask/webui/pg_consumer_slow.py +0 -1099
- jettask/webui/pg_consumer_test.py +0 -678
- jettask/webui/static/dist/assets/index-823408e8.css +0 -1
- jettask/webui/static/dist/assets/index-9968b0b8.js +0 -543
- jettask/webui/test_pg_consumer_recovery.py +0 -547
- jettask/webui/test_recovery_simple.py +0 -492
- jettask/webui/test_self_recovery.py +0 -467
- jettask-0.2.1.dist-info/RECORD +0 -91
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/WHEEL +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,423 @@
|
|
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: 'number' },
|
10
|
+
{ value: 'scheduler_id', label: '调度器ID', type: 'string' },
|
11
|
+
{ value: 'name', label: '任务名称', type: 'string' },
|
12
|
+
{ value: 'queue_name', label: '队列名称', type: 'string' },
|
13
|
+
{ value: 'schedule_type', label: '调度类型', type: 'enum', options: ['cron', 'interval', 'once'] },
|
14
|
+
{ value: 'is_active', label: '状态', type: 'boolean' },
|
15
|
+
{ value: 'description', label: '描述', type: 'string' },
|
16
|
+
{ value: 'last_run', label: '上次执行时间', type: 'datetime' },
|
17
|
+
{ value: 'next_run', label: '下次执行时间', type: 'datetime' },
|
18
|
+
{ value: 'created_at', label: '创建时间', type: 'datetime' },
|
19
|
+
{ value: 'task_data', label: '任务参数', type: 'json' },
|
20
|
+
{ value: 'tags', label: '标签', type: 'json' },
|
21
|
+
{ value: 'metadata', label: '元数据', type: 'json' },
|
22
|
+
];
|
23
|
+
|
24
|
+
// 操作符列表
|
25
|
+
const OPERATORS = {
|
26
|
+
string: [
|
27
|
+
{ value: 'eq', label: '等于' },
|
28
|
+
{ value: 'ne', label: '不等于' },
|
29
|
+
{ value: 'contains', label: '包含' },
|
30
|
+
{ value: 'starts_with', label: '开始于' },
|
31
|
+
{ value: 'ends_with', label: '结束于' },
|
32
|
+
{ value: 'is_null', label: '为空' },
|
33
|
+
{ value: 'is_not_null', label: '不为空' },
|
34
|
+
],
|
35
|
+
number: [
|
36
|
+
{ value: 'eq', label: '等于' },
|
37
|
+
{ value: 'ne', label: '不等于' },
|
38
|
+
{ value: 'gt', label: '大于' },
|
39
|
+
{ value: 'lt', label: '小于' },
|
40
|
+
{ value: 'gte', label: '大于等于' },
|
41
|
+
{ value: 'lte', label: '小于等于' },
|
42
|
+
{ value: 'is_null', label: '为空' },
|
43
|
+
{ value: 'is_not_null', label: '不为空' },
|
44
|
+
],
|
45
|
+
datetime: [
|
46
|
+
{ value: 'gt', label: '晚于' },
|
47
|
+
{ value: 'lt', label: '早于' },
|
48
|
+
{ value: 'gte', label: '不早于' },
|
49
|
+
{ value: 'lte', label: '不晚于' },
|
50
|
+
{ value: 'is_null', label: '为空' },
|
51
|
+
{ value: 'is_not_null', label: '不为空' },
|
52
|
+
],
|
53
|
+
enum: [
|
54
|
+
{ value: 'eq', label: '等于' },
|
55
|
+
{ value: 'ne', label: '不等于' },
|
56
|
+
{ value: 'in', label: '在列表中' },
|
57
|
+
{ value: 'not_in', label: '不在列表中' },
|
58
|
+
],
|
59
|
+
boolean: [
|
60
|
+
{ value: 'eq', label: '等于' },
|
61
|
+
],
|
62
|
+
json: [
|
63
|
+
{ value: 'contains', label: '包含文本' },
|
64
|
+
{ value: 'json_key_exists', label: '包含键名' },
|
65
|
+
{ value: 'json_path_value', label: 'JSON路径值' },
|
66
|
+
{ value: 'is_null', label: '为空' },
|
67
|
+
{ value: 'is_not_null', label: '不为空' },
|
68
|
+
],
|
69
|
+
};
|
70
|
+
|
71
|
+
function ScheduledTaskFilter({ filters, onFiltersChange }) {
|
72
|
+
const [visible, setVisible] = useState(false);
|
73
|
+
const [form] = Form.useForm();
|
74
|
+
const [selectedField, setSelectedField] = useState(null);
|
75
|
+
const [selectedOperator, setSelectedOperator] = useState(null);
|
76
|
+
const [disabledFilters, setDisabledFilters] = useState(new Set()); // 记录被禁用的筛选条件索引
|
77
|
+
|
78
|
+
// 获取当前字段的类型
|
79
|
+
const getFieldType = (fieldValue) => {
|
80
|
+
const field = AVAILABLE_FIELDS.find(f => f.value === fieldValue);
|
81
|
+
return field ? field.type : 'string';
|
82
|
+
};
|
83
|
+
|
84
|
+
// 获取当前字段的选项(用于枚举类型)
|
85
|
+
const getFieldOptions = (fieldValue) => {
|
86
|
+
const field = AVAILABLE_FIELDS.find(f => f.value === fieldValue);
|
87
|
+
return field && field.options ? field.options : [];
|
88
|
+
};
|
89
|
+
|
90
|
+
// 添加筛选条件
|
91
|
+
const handleAddFilter = () => {
|
92
|
+
form.validateFields().then(values => {
|
93
|
+
const newFilter = {
|
94
|
+
id: Date.now(), // 添加唯一ID
|
95
|
+
field: values.field,
|
96
|
+
operator: values.operator,
|
97
|
+
value: values.value,
|
98
|
+
enabled: true, // 默认启用
|
99
|
+
};
|
100
|
+
|
101
|
+
// 处理特殊操作符
|
102
|
+
if (values.operator === 'is_null' || values.operator === 'is_not_null') {
|
103
|
+
newFilter.value = null;
|
104
|
+
} else if (values.operator === 'in' || values.operator === 'not_in') {
|
105
|
+
// 如果是枚举类型的多选
|
106
|
+
if (getFieldType(values.field) === 'enum') {
|
107
|
+
newFilter.value = values.value; // 已经是数组
|
108
|
+
} else {
|
109
|
+
// 普通文本,用逗号分隔
|
110
|
+
newFilter.value = values.value.split(',').map(v => v.trim());
|
111
|
+
}
|
112
|
+
} else if (getFieldType(values.field) === 'boolean') {
|
113
|
+
// 布尔类型转换
|
114
|
+
newFilter.value = values.value === 'true';
|
115
|
+
}
|
116
|
+
|
117
|
+
const updatedFilters = [...(filters || []), newFilter];
|
118
|
+
onFiltersChange(updatedFilters);
|
119
|
+
|
120
|
+
// 重置表单
|
121
|
+
form.resetFields();
|
122
|
+
setSelectedField(null);
|
123
|
+
setSelectedOperator(null);
|
124
|
+
setVisible(false);
|
125
|
+
message.success('筛选条件已添加');
|
126
|
+
});
|
127
|
+
};
|
128
|
+
|
129
|
+
// 切换筛选条件的启用/禁用状态
|
130
|
+
const handleToggleFilter = (index) => {
|
131
|
+
const updatedFilters = [...filters];
|
132
|
+
updatedFilters[index] = {
|
133
|
+
...updatedFilters[index],
|
134
|
+
enabled: !updatedFilters[index].enabled
|
135
|
+
};
|
136
|
+
onFiltersChange(updatedFilters);
|
137
|
+
};
|
138
|
+
|
139
|
+
// 删除筛选条件
|
140
|
+
const handleRemoveFilter = (index) => {
|
141
|
+
const updatedFilters = filters.filter((_, i) => i !== index);
|
142
|
+
onFiltersChange(updatedFilters);
|
143
|
+
message.success('筛选条件已删除');
|
144
|
+
};
|
145
|
+
|
146
|
+
// 渲染筛选条件标签
|
147
|
+
const renderFilterTag = (filter, index) => {
|
148
|
+
const field = AVAILABLE_FIELDS.find(f => f.value === filter.field);
|
149
|
+
const fieldLabel = field ? field.label : filter.field;
|
150
|
+
|
151
|
+
const operator = OPERATORS[getFieldType(filter.field)]?.find(op => op.value === filter.operator);
|
152
|
+
const operatorLabel = operator ? operator.label : filter.operator;
|
153
|
+
|
154
|
+
let valueLabel = filter.value;
|
155
|
+
if (filter.operator === 'is_null' || filter.operator === 'is_not_null') {
|
156
|
+
valueLabel = '';
|
157
|
+
} else if (Array.isArray(filter.value)) {
|
158
|
+
valueLabel = filter.value.join(', ');
|
159
|
+
} else if (typeof filter.value === 'boolean') {
|
160
|
+
valueLabel = filter.value ? '是' : '否';
|
161
|
+
}
|
162
|
+
|
163
|
+
const tagLabel = `${fieldLabel} ${operatorLabel} ${valueLabel}`.trim();
|
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
|
+
{tagLabel}
|
187
|
+
</Tag>
|
188
|
+
);
|
189
|
+
};
|
190
|
+
|
191
|
+
// 渲染值输入组件
|
192
|
+
const renderValueInput = () => {
|
193
|
+
if (!selectedField) return null;
|
194
|
+
|
195
|
+
const fieldType = getFieldType(selectedField);
|
196
|
+
const needsValue = selectedOperator !== 'is_null' && selectedOperator !== 'is_not_null';
|
197
|
+
|
198
|
+
if (!needsValue) return null;
|
199
|
+
|
200
|
+
if (fieldType === 'enum') {
|
201
|
+
const options = getFieldOptions(selectedField);
|
202
|
+
if (selectedOperator === 'in' || selectedOperator === 'not_in') {
|
203
|
+
return (
|
204
|
+
<Form.Item
|
205
|
+
name="value"
|
206
|
+
label="值"
|
207
|
+
rules={[{ required: true, message: '请选择值' }]}
|
208
|
+
>
|
209
|
+
<Select mode="multiple" placeholder="选择多个值">
|
210
|
+
{options.map(opt => (
|
211
|
+
<Option key={opt} value={opt}>{opt}</Option>
|
212
|
+
))}
|
213
|
+
</Select>
|
214
|
+
</Form.Item>
|
215
|
+
);
|
216
|
+
} else {
|
217
|
+
return (
|
218
|
+
<Form.Item
|
219
|
+
name="value"
|
220
|
+
label="值"
|
221
|
+
rules={[{ required: true, message: '请选择值' }]}
|
222
|
+
>
|
223
|
+
<Select placeholder="选择值">
|
224
|
+
{options.map(opt => (
|
225
|
+
<Option key={opt} value={opt}>{opt}</Option>
|
226
|
+
))}
|
227
|
+
</Select>
|
228
|
+
</Form.Item>
|
229
|
+
);
|
230
|
+
}
|
231
|
+
} else if (fieldType === 'boolean') {
|
232
|
+
return (
|
233
|
+
<Form.Item
|
234
|
+
name="value"
|
235
|
+
label="值"
|
236
|
+
rules={[{ required: true, message: '请选择值' }]}
|
237
|
+
>
|
238
|
+
<Select placeholder="选择值">
|
239
|
+
<Option value="true">是</Option>
|
240
|
+
<Option value="false">否</Option>
|
241
|
+
</Select>
|
242
|
+
</Form.Item>
|
243
|
+
);
|
244
|
+
} else if (fieldType === 'number') {
|
245
|
+
return (
|
246
|
+
<Form.Item
|
247
|
+
name="value"
|
248
|
+
label="值"
|
249
|
+
rules={[{ required: true, message: '请输入值' }]}
|
250
|
+
>
|
251
|
+
<Input type="number" placeholder="输入数值" />
|
252
|
+
</Form.Item>
|
253
|
+
);
|
254
|
+
} else if (fieldType === 'datetime') {
|
255
|
+
return (
|
256
|
+
<Form.Item
|
257
|
+
name="value"
|
258
|
+
label="值"
|
259
|
+
rules={[{ required: true, message: '请输入值' }]}
|
260
|
+
>
|
261
|
+
<Input
|
262
|
+
placeholder="格式: YYYY-MM-DD HH:mm:ss"
|
263
|
+
title="请输入日期时间,格式: YYYY-MM-DD HH:mm:ss"
|
264
|
+
/>
|
265
|
+
</Form.Item>
|
266
|
+
);
|
267
|
+
} else if (fieldType === 'json') {
|
268
|
+
// JSON类型的输入
|
269
|
+
let label = '搜索内容';
|
270
|
+
let placeholder = '输入搜索内容';
|
271
|
+
let extra = '';
|
272
|
+
|
273
|
+
if (selectedOperator === 'json_key_exists') {
|
274
|
+
label = '键名';
|
275
|
+
placeholder = '输入要查找的键名';
|
276
|
+
extra = '检查JSON中是否存在指定的键';
|
277
|
+
} else if (selectedOperator === 'json_path_value') {
|
278
|
+
label = 'JSON路径和值';
|
279
|
+
placeholder = '路径=值,如:$.user_id=123 或 $.kwargs.message=test';
|
280
|
+
extra = '使用JSON路径语法,格式:路径=值。支持嵌套路径如 $.kwargs.user_id';
|
281
|
+
} else if (selectedOperator === 'contains') {
|
282
|
+
label = '搜索文本';
|
283
|
+
placeholder = '输入要搜索的文本内容';
|
284
|
+
extra = '在JSON数据中搜索包含此文本的任务';
|
285
|
+
}
|
286
|
+
|
287
|
+
return (
|
288
|
+
<Form.Item
|
289
|
+
name="value"
|
290
|
+
label={label}
|
291
|
+
rules={[{ required: true, message: `请输入${label}` }]}
|
292
|
+
extra={extra}
|
293
|
+
>
|
294
|
+
<Input.TextArea
|
295
|
+
placeholder={placeholder}
|
296
|
+
rows={2}
|
297
|
+
spellCheck={false}
|
298
|
+
/>
|
299
|
+
</Form.Item>
|
300
|
+
);
|
301
|
+
} else {
|
302
|
+
return (
|
303
|
+
<Form.Item
|
304
|
+
name="value"
|
305
|
+
label="值"
|
306
|
+
rules={[{ required: true, message: '请输入值' }]}
|
307
|
+
>
|
308
|
+
<Input placeholder={selectedOperator === 'in' || selectedOperator === 'not_in' ? '多个值用逗号分隔' : '输入值'} />
|
309
|
+
</Form.Item>
|
310
|
+
);
|
311
|
+
}
|
312
|
+
};
|
313
|
+
|
314
|
+
const filterContent = (
|
315
|
+
<div style={{ width: 350 }}>
|
316
|
+
<Form
|
317
|
+
form={form}
|
318
|
+
layout="vertical"
|
319
|
+
onFinish={handleAddFilter}
|
320
|
+
>
|
321
|
+
<Form.Item
|
322
|
+
name="field"
|
323
|
+
label="字段"
|
324
|
+
rules={[{ required: true, message: '请选择字段' }]}
|
325
|
+
>
|
326
|
+
<Select
|
327
|
+
placeholder="选择字段"
|
328
|
+
onChange={(value) => {
|
329
|
+
setSelectedField(value);
|
330
|
+
setSelectedOperator(null);
|
331
|
+
form.setFieldsValue({ operator: undefined, value: undefined });
|
332
|
+
}}
|
333
|
+
>
|
334
|
+
{AVAILABLE_FIELDS.map(field => (
|
335
|
+
<Option key={field.value} value={field.value}>
|
336
|
+
{field.label}
|
337
|
+
</Option>
|
338
|
+
))}
|
339
|
+
</Select>
|
340
|
+
</Form.Item>
|
341
|
+
|
342
|
+
{selectedField && (
|
343
|
+
<Form.Item
|
344
|
+
name="operator"
|
345
|
+
label="操作符"
|
346
|
+
rules={[{ required: true, message: '请选择操作符' }]}
|
347
|
+
>
|
348
|
+
<Select
|
349
|
+
placeholder="选择操作符"
|
350
|
+
onChange={(value) => {
|
351
|
+
setSelectedOperator(value);
|
352
|
+
form.setFieldsValue({ value: undefined });
|
353
|
+
}}
|
354
|
+
>
|
355
|
+
{OPERATORS[getFieldType(selectedField)]?.map(op => (
|
356
|
+
<Option key={op.value} value={op.value}>
|
357
|
+
{op.label}
|
358
|
+
</Option>
|
359
|
+
))}
|
360
|
+
</Select>
|
361
|
+
</Form.Item>
|
362
|
+
)}
|
363
|
+
|
364
|
+
{renderValueInput()}
|
365
|
+
|
366
|
+
<Form.Item>
|
367
|
+
<Space>
|
368
|
+
<Button type="primary" htmlType="submit">
|
369
|
+
添加筛选条件
|
370
|
+
</Button>
|
371
|
+
<Button onClick={() => {
|
372
|
+
setVisible(false);
|
373
|
+
form.resetFields();
|
374
|
+
setSelectedField(null);
|
375
|
+
setSelectedOperator(null);
|
376
|
+
}}>
|
377
|
+
取消
|
378
|
+
</Button>
|
379
|
+
</Space>
|
380
|
+
</Form.Item>
|
381
|
+
</Form>
|
382
|
+
</div>
|
383
|
+
);
|
384
|
+
|
385
|
+
return (
|
386
|
+
<div style={{
|
387
|
+
display: 'inline-flex',
|
388
|
+
alignItems: 'center',
|
389
|
+
flexWrap: 'wrap',
|
390
|
+
gap: '8px',
|
391
|
+
verticalAlign: 'middle'
|
392
|
+
}}>
|
393
|
+
{filters && filters.map((filter, index) => renderFilterTag(filter, index))}
|
394
|
+
|
395
|
+
<Popover
|
396
|
+
content={filterContent}
|
397
|
+
title="添加筛选条件"
|
398
|
+
trigger="click"
|
399
|
+
open={visible}
|
400
|
+
onOpenChange={setVisible}
|
401
|
+
placement="bottomLeft"
|
402
|
+
>
|
403
|
+
<Button
|
404
|
+
type="dashed"
|
405
|
+
icon={<PlusOutlined />}
|
406
|
+
size="small"
|
407
|
+
style={{
|
408
|
+
height: '24px',
|
409
|
+
padding: '0 8px',
|
410
|
+
display: 'inline-flex',
|
411
|
+
alignItems: 'center',
|
412
|
+
lineHeight: '22px',
|
413
|
+
verticalAlign: 'middle'
|
414
|
+
}}
|
415
|
+
>
|
416
|
+
Add Filter
|
417
|
+
</Button>
|
418
|
+
</Popover>
|
419
|
+
</div>
|
420
|
+
);
|
421
|
+
}
|
422
|
+
|
423
|
+
export default ScheduledTaskFilter;
|