@gadmin2n/schematics 0.0.70 → 0.0.72

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.
Files changed (28) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/system.prisma +21 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/server/gadmin-cli.json +9 -1
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package-lock.json +15579 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +5 -3
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permissions.ts +16 -4
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/app.module.ts +2 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +106 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.module.ts +10 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +569 -0
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/yarn.lock +1159 -481
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/web/.env +1 -0
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +1 -1
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/inspectorActions.ts +130 -3
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/NumCard/index.tsx +5 -5
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/NumLineCard/index.tsx +2 -2
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +15 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/DevShell.tsx +55 -43
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/SkillMenu.tsx +1 -1
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +2 -4
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +2 -4
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +536 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/show.tsx +671 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +9 -60
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +2 -2
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/ComponentThumbnail.tsx +3 -1
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasContextMenuRegistry.tsx +11 -11
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/vite-env.d.ts +12 -0
  28. package/package.json +1 -1
@@ -0,0 +1,536 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Badge,
4
+ Button,
5
+ Card,
6
+ Col,
7
+ Input,
8
+ Popconfirm,
9
+ Row,
10
+ Select,
11
+ Space,
12
+ Statistic,
13
+ Switch,
14
+ Table,
15
+ Tag,
16
+ Tooltip,
17
+ Typography,
18
+ message,
19
+ } from 'antd';
20
+ import {
21
+ DeleteOutlined,
22
+ PlayCircleOutlined,
23
+ ReloadOutlined,
24
+ } from '@ant-design/icons';
25
+ import { useSearchParams, useNavigate } from 'react-router-dom';
26
+ import { customRequest } from 'helpers/http';
27
+
28
+ const { Title } = Typography;
29
+
30
+ // ─── Types ────────────────────────────────────────────────────────────────────
31
+
32
+ interface JobItem {
33
+ id: string;
34
+ name: string;
35
+ module: string | null;
36
+ state: string;
37
+ runs: { queued: number; success: number; running: number; failed: number };
38
+ nextRunAt: string | null;
39
+ lastRunAt: string | null;
40
+ lastFinishedAt: string | null;
41
+ failReason: string | null;
42
+ failedAt: string | null;
43
+ repeatInterval: string | null;
44
+ data: Record<string, any> | null;
45
+ lockedAt: string | null;
46
+ disabled: boolean;
47
+ }
48
+
49
+ interface Overview {
50
+ running: number;
51
+ scheduled: number;
52
+ queued: number;
53
+ completed: number;
54
+ failed: number;
55
+ repeating: number;
56
+ paused: number;
57
+ active: number;
58
+ }
59
+
60
+ interface JobsResponse {
61
+ overview: Overview;
62
+ modules: string[];
63
+ jobs: JobItem[];
64
+ total: number;
65
+ totalPages: number;
66
+ }
67
+
68
+ // ─── State badge helpers ───────────────────────────────────────────────────────
69
+
70
+ const STATE_COLOR: Record<string, string> = {
71
+ running: 'processing',
72
+ scheduled: 'default',
73
+ queued: 'warning',
74
+ completed: 'success',
75
+ failed: 'error',
76
+ repeating: 'purple',
77
+ paused: 'default',
78
+ };
79
+
80
+ function StateTag({ state }: { state: string }) {
81
+ return <Badge status={STATE_COLOR[state] as any} text={state} />;
82
+ }
83
+
84
+ /** 圆圈指示器:有数量时显示彩色圆圈+内嵌数字,无数量时显示灰色空心圆 */
85
+ function RunDot({
86
+ count,
87
+ color,
88
+ label,
89
+ }: {
90
+ count: number;
91
+ color: string;
92
+ label: string;
93
+ }) {
94
+ const size = 28;
95
+ const hasCount = count > 0;
96
+ return (
97
+ <Tooltip title={`${label}: ${count}`}>
98
+ <span
99
+ style={{
100
+ display: 'inline-flex',
101
+ alignItems: 'center',
102
+ justifyContent: 'center',
103
+ verticalAlign: 'middle',
104
+ width: size,
105
+ height: size,
106
+ borderRadius: '50%',
107
+ border: `2.5px solid ${hasCount ? color : '#d9d9d9'}`,
108
+ fontSize: 11,
109
+ fontWeight: 600,
110
+ color: hasCount ? color : '#d9d9d9',
111
+ cursor: 'default',
112
+ lineHeight: 1,
113
+ }}
114
+ >
115
+ {hasCount ? count : ''}
116
+ </span>
117
+ </Tooltip>
118
+ );
119
+ }
120
+
121
+ // ─── AgendaJobsPage ───────────────────────────────────────────────────────────
122
+
123
+ export default function AgendaJobsPage() {
124
+ const navigate = useNavigate();
125
+ const [jobs, setJobs] = useState<JobItem[]>([]);
126
+ const [overview, setOverview] = useState<Overview | null>(null);
127
+ const [modules, setModules] = useState<string[]>([]);
128
+ const [total, setTotal] = useState(0);
129
+ const [loading, setLoading] = useState(false);
130
+
131
+ // Filters — synced with URL search params
132
+ const [searchParams, setSearchParams] = useSearchParams();
133
+ const filterState = searchParams.get('state') || undefined;
134
+ const filterToggle = searchParams.get('toggle') || undefined;
135
+ const filterName = searchParams.get('name') || undefined;
136
+ const filterModule = searchParams.get('module') || undefined;
137
+ const page = Number(searchParams.get('page')) || 1;
138
+ const PAGE_SIZE = 20;
139
+
140
+ const updateParams = useCallback(
141
+ (updates: Record<string, string | undefined>) => {
142
+ setSearchParams(
143
+ (prev) => {
144
+ const next = new URLSearchParams(prev);
145
+ for (const [k, v] of Object.entries(updates)) {
146
+ if (v) next.set(k, v);
147
+ else next.delete(k);
148
+ }
149
+ return next;
150
+ },
151
+ { replace: true },
152
+ );
153
+ },
154
+ [setSearchParams],
155
+ );
156
+
157
+ const setFilterState = useCallback(
158
+ (v: string | undefined) => updateParams({ state: v, page: undefined }),
159
+ [updateParams],
160
+ );
161
+ const setFilterToggle = useCallback(
162
+ (v: string | undefined) => updateParams({ toggle: v, page: undefined }),
163
+ [updateParams],
164
+ );
165
+ const setFilterName = useCallback(
166
+ (v: string | undefined) => updateParams({ name: v, page: undefined }),
167
+ [updateParams],
168
+ );
169
+ const setFilterModule = useCallback(
170
+ (v: string | undefined) => updateParams({ module: v, page: undefined }),
171
+ [updateParams],
172
+ );
173
+ const setPage = useCallback(
174
+ (p: number) => updateParams({ page: p > 1 ? String(p) : undefined }),
175
+ [updateParams],
176
+ );
177
+
178
+ // Auto-refresh
179
+ const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
180
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
181
+
182
+ const fetchJobs = useCallback(async () => {
183
+ setLoading(true);
184
+ try {
185
+ const params: Record<string, any> = {
186
+ skip: (page - 1) * PAGE_SIZE,
187
+ limit: PAGE_SIZE,
188
+ };
189
+ if (filterState) params.state = filterState;
190
+ if (filterToggle) params.toggle = filterToggle;
191
+ if (filterName) params.q = filterName;
192
+ if (filterModule) params.module = filterModule;
193
+
194
+ const res = await customRequest<JobsResponse>(
195
+ 'agenda/jobs',
196
+ 'GET',
197
+ params,
198
+ );
199
+ setJobs(res.jobs);
200
+ setOverview(res.overview);
201
+ setModules(res.modules);
202
+ setTotal(res.total);
203
+ } catch (e: any) {
204
+ message.error(e?.message ?? 'Failed to fetch jobs');
205
+ } finally {
206
+ setLoading(false);
207
+ }
208
+ }, [page, filterState, filterToggle, filterName, filterModule]);
209
+
210
+ // Initial fetch + auto-refresh every 5s
211
+ useEffect(() => {
212
+ fetchJobs();
213
+ timerRef.current = setInterval(fetchJobs, 5000);
214
+ return () => {
215
+ if (timerRef.current) clearInterval(timerRef.current);
216
+ };
217
+ }, [fetchJobs]);
218
+
219
+ // ─── Row actions ───────────────────────────────────────────────────────────
220
+
221
+ async function handleAction(
222
+ action: 'requeue' | 'delete' | 'disable' | 'enable',
223
+ ids: string[],
224
+ ) {
225
+ try {
226
+ const res = await customRequest<{ affected: number }>(
227
+ `agenda/${action}`,
228
+ 'POST',
229
+ { jobIds: ids },
230
+ );
231
+ message.success(`操作成功,影响 ${res.affected} 条任务`);
232
+ fetchJobs();
233
+ } catch (e: any) {
234
+ message.error(e?.message ?? '操作失败');
235
+ }
236
+ }
237
+
238
+ // ─── Overview cards ────────────────────────────────────────────────────────
239
+
240
+ const OVERVIEW_ITEMS: {
241
+ key: keyof Overview;
242
+ label: string;
243
+ color: string;
244
+ }[] = [
245
+ { key: 'running', label: 'Running', color: '#1677ff' },
246
+ { key: 'scheduled', label: 'Scheduled', color: '#595959' },
247
+ { key: 'queued', label: 'Queued', color: '#faad14' },
248
+ { key: 'completed', label: 'Completed', color: '#52c41a' },
249
+ { key: 'failed', label: 'Failed', color: '#ff4d4f' },
250
+ { key: 'repeating', label: 'Repeating', color: '#722ed1' },
251
+ { key: 'paused', label: 'Paused', color: '#8c8c8c' },
252
+ ];
253
+
254
+ // ─── Table columns ─────────────────────────────────────────────────────────
255
+
256
+ const columns = [
257
+ {
258
+ title: '',
259
+ key: 'toggle',
260
+ width: 50,
261
+ render: (_: any, record: JobItem) => (
262
+ <Popconfirm
263
+ title={record.disabled ? '确定恢复此任务?' : '确定暂停此任务?'}
264
+ okText="确认"
265
+ cancelText="取消"
266
+ onConfirm={() =>
267
+ handleAction(record.disabled ? 'enable' : 'disable', [record.id])
268
+ }
269
+ >
270
+ <Switch size="small" checked={!record.disabled} />
271
+ </Popconfirm>
272
+ ),
273
+ },
274
+ {
275
+ title: 'Name',
276
+ dataIndex: 'name',
277
+ key: 'name',
278
+ render: (_: string, record: JobItem) => (
279
+ <div>
280
+ <a
281
+ onClick={() => navigate(`show/${encodeURIComponent(record.name)}`)}
282
+ style={{
283
+ display: 'block',
284
+ overflow: 'hidden',
285
+ textOverflow: 'ellipsis',
286
+ whiteSpace: 'nowrap',
287
+ }}
288
+ title={record.name}
289
+ >
290
+ {record.name}
291
+ </a>
292
+ {record.module && (
293
+ <Tag color="cyan" style={{ marginTop: 4 }}>
294
+ {record.module}
295
+ </Tag>
296
+ )}
297
+ </div>
298
+ ),
299
+ },
300
+ {
301
+ title: 'State',
302
+ dataIndex: 'state',
303
+ key: 'state',
304
+ width: 100,
305
+ render: (state: string) => <StateTag state={state} />,
306
+ },
307
+ {
308
+ title: 'Schedule',
309
+ dataIndex: 'repeatInterval',
310
+ key: 'repeatInterval',
311
+ width: 110,
312
+ render: (v: string | null) => (v ? <Tag color="purple">{v}</Tag> : '—'),
313
+ },
314
+ {
315
+ title: 'Last Run',
316
+ dataIndex: 'lastRunAt',
317
+ key: 'lastRunAt',
318
+ width: 120,
319
+ render: (v: string | null) =>
320
+ v ? (
321
+ <Tooltip title={new Date(v).toLocaleString()}>
322
+ <span>
323
+ {new Date(v).toLocaleString(undefined, {
324
+ month: '2-digit',
325
+ day: '2-digit',
326
+ hour: '2-digit',
327
+ minute: '2-digit',
328
+ })}
329
+ </span>
330
+ </Tooltip>
331
+ ) : (
332
+ '—'
333
+ ),
334
+ },
335
+ {
336
+ title: 'Next Run',
337
+ dataIndex: 'nextRunAt',
338
+ key: 'nextRunAt',
339
+ width: 120,
340
+ render: (v: string | null) =>
341
+ v ? (
342
+ <Tooltip title={new Date(v).toLocaleString()}>
343
+ <span>
344
+ {new Date(v).toLocaleString(undefined, {
345
+ month: '2-digit',
346
+ day: '2-digit',
347
+ hour: '2-digit',
348
+ minute: '2-digit',
349
+ })}
350
+ </span>
351
+ </Tooltip>
352
+ ) : (
353
+ '—'
354
+ ),
355
+ },
356
+ {
357
+ title: 'Runs',
358
+ key: 'runs',
359
+ width: 170,
360
+ render: (_: any, record: JobItem) => (
361
+ <Space size={6}>
362
+ <RunDot count={record.runs.queued} color="#faad14" label="Queued" />
363
+ <RunDot count={record.runs.success} color="#006d32" label="Success" />
364
+ <RunDot count={record.runs.running} color="#52c41a" label="Running" />
365
+ <RunDot count={record.runs.failed} color="#ff4d4f" label="Failed" />
366
+ </Space>
367
+ ),
368
+ },
369
+ {
370
+ title: 'Actions',
371
+ key: 'actions',
372
+ render: (_: any, record: JobItem) => (
373
+ <Space>
374
+ <Popconfirm
375
+ title="确定立即执行此任务?"
376
+ okText="确认"
377
+ cancelText="取消"
378
+ onConfirm={() => handleAction('requeue', [record.id])}
379
+ >
380
+ <Tooltip title="Trigger: 立即执行一次">
381
+ <Button size="small" icon={<PlayCircleOutlined />} />
382
+ </Tooltip>
383
+ </Popconfirm>
384
+ <Popconfirm
385
+ title="确定删除此任务?"
386
+ description="删除后无法恢复,请谨慎操作。"
387
+ okText="确认删除"
388
+ cancelText="取消"
389
+ okButtonProps={{ danger: true }}
390
+ onConfirm={() => handleAction('delete', [record.id])}
391
+ >
392
+ <Tooltip title="Delete">
393
+ <Button size="small" danger icon={<DeleteOutlined />} />
394
+ </Tooltip>
395
+ </Popconfirm>
396
+ </Space>
397
+ ),
398
+ },
399
+ ];
400
+
401
+ // ─── Render ────────────────────────────────────────────────────────────────
402
+
403
+ return (
404
+ <div style={{ background: '#f0f2f5', minHeight: 'calc(100vh - 64px)' }}>
405
+ {/* Header */}
406
+ <Card style={{ marginBottom: 16 }}>
407
+ <div
408
+ style={{
409
+ display: 'flex',
410
+ justifyContent: 'space-between',
411
+ alignItems: 'center',
412
+ }}
413
+ >
414
+ <Title level={4} style={{ margin: 0 }}>
415
+ Cron Jobs
416
+ </Title>
417
+ <Button icon={<ReloadOutlined />} onClick={fetchJobs}></Button>
418
+ </div>
419
+ </Card>
420
+
421
+ {/* Overview cards */}
422
+ {overview && (
423
+ <Row gutter={12} style={{ marginBottom: 16 }}>
424
+ {OVERVIEW_ITEMS.map(({ key, label, color }) => (
425
+ <Col key={key} flex="1">
426
+ <Card
427
+ size="small"
428
+ hoverable
429
+ onClick={() =>
430
+ setFilterState(filterState === key ? undefined : key)
431
+ }
432
+ style={{
433
+ borderColor: filterState === key ? color : undefined,
434
+ cursor: 'pointer',
435
+ }}
436
+ >
437
+ <Statistic
438
+ title={label}
439
+ value={overview[key]}
440
+ valueStyle={{ color }}
441
+ />
442
+ </Card>
443
+ </Col>
444
+ ))}
445
+ </Row>
446
+ )}
447
+
448
+ {/* Filters */}
449
+ <Card style={{ marginBottom: 16 }}>
450
+ <Space wrap>
451
+ <Space size={0}>
452
+ {[
453
+ {
454
+ key: undefined,
455
+ label: 'All',
456
+ count: overview ? overview.active + overview.paused : 0,
457
+ },
458
+ { key: 'active', label: 'Active', count: overview?.active ?? 0 },
459
+ { key: 'paused', label: 'Paused', count: overview?.paused ?? 0 },
460
+ ].map(({ key, label, count }) => {
461
+ const selected = filterToggle === key;
462
+ return (
463
+ <Button
464
+ key={label}
465
+ type={selected ? 'primary' : 'default'}
466
+ onClick={() => setFilterToggle(key)}
467
+ style={{
468
+ borderRadius: 0,
469
+ ...(label === 'All'
470
+ ? { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }
471
+ : {}),
472
+ ...(label === 'Paused'
473
+ ? { borderTopRightRadius: 6, borderBottomRightRadius: 6 }
474
+ : {}),
475
+ }}
476
+ >
477
+ {label}{' '}
478
+ <Badge
479
+ count={count}
480
+ showZero
481
+ style={{
482
+ marginLeft: 6,
483
+ backgroundColor: selected
484
+ ? 'rgba(255,255,255,0.3)'
485
+ : '#8c8c8c',
486
+ }}
487
+ />
488
+ </Button>
489
+ );
490
+ })}
491
+ </Space>
492
+ <Select
493
+ placeholder="模块筛选"
494
+ allowClear
495
+ value={filterModule}
496
+ onChange={(v) => setFilterModule(v)}
497
+ style={{ width: 160 }}
498
+ options={modules.map((m) => ({ value: m, label: m }))}
499
+ />
500
+ <Input
501
+ placeholder="按名称过滤"
502
+ allowClear
503
+ defaultValue={filterName}
504
+ onChange={(e) => {
505
+ const v = e.target.value;
506
+ if (debounceRef.current) clearTimeout(debounceRef.current);
507
+ debounceRef.current = setTimeout(
508
+ () => setFilterName(v || undefined),
509
+ 300,
510
+ );
511
+ }}
512
+ style={{ width: 220 }}
513
+ />
514
+ </Space>
515
+ </Card>
516
+
517
+ {/* Jobs table */}
518
+ <Card>
519
+ <Table
520
+ rowKey="id"
521
+ loading={loading}
522
+ columns={columns}
523
+ dataSource={jobs}
524
+ pagination={{
525
+ current: page,
526
+ pageSize: PAGE_SIZE,
527
+ total,
528
+ onChange: (p) => setPage(p),
529
+ showTotal: (t) => `共 ${t} 条`,
530
+ }}
531
+ size="small"
532
+ />
533
+ </Card>
534
+ </div>
535
+ );
536
+ }