@lobehub/lobehub 2.0.0-next.276 → 2.0.0-next.277
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.
- package/CHANGELOG.md +26 -0
- package/changelog/v1.json +9 -0
- package/locales/en-US/setting.json +11 -0
- package/locales/zh-CN/setting.json +11 -0
- package/package.json +1 -1
- package/packages/builtin-tool-group-agent-builder/src/client/Inspector/BatchCreateAgents/index.tsx +2 -2
- package/packages/builtin-tool-group-agent-builder/src/client/Inspector/UpdateGroup/index.tsx +56 -56
- package/packages/builtin-tool-group-agent-builder/src/client/Render/BatchCreateAgents.tsx +3 -2
- package/packages/builtin-tool-group-agent-builder/src/executor.ts +2 -1
- package/packages/types/src/agentCronJob/index.ts +19 -23
- package/packages/types/src/serverConfig.ts +1 -0
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/Actions.tsx +31 -0
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/CronTopicGroup.tsx +10 -6
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/index.tsx +7 -11
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/useDropdownMenu.tsx +102 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/CronConfig.ts +179 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +111 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobHeader.tsx +45 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobSaveButton.tsx +31 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobScheduleConfig.tsx +213 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/index.tsx +186 -344
- package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/index.tsx +42 -97
- package/src/app/[variants]/(main)/agent/profile/features/ProfileEditor/index.tsx +4 -20
- package/src/app/[variants]/(main)/community/features/UserAvatar/index.tsx +15 -5
- package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/AgentProfilePopup.tsx +1 -6
- package/src/server/globalConfig/index.ts +1 -0
- package/src/services/chatGroup/index.ts +1 -4
- package/src/store/serverConfig/selectors.ts +1 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Dayjs } from 'dayjs';
|
|
2
|
+
|
|
3
|
+
export type ScheduleType = 'daily' | 'hourly' | 'weekly';
|
|
4
|
+
|
|
5
|
+
// Schedule type options
|
|
6
|
+
export const SCHEDULE_TYPE_OPTIONS = [
|
|
7
|
+
{ label: 'agentCronJobs.scheduleType.daily', value: 'daily' },
|
|
8
|
+
{ label: 'agentCronJobs.scheduleType.hourly', value: 'hourly' },
|
|
9
|
+
{ label: 'agentCronJobs.scheduleType.weekly', value: 'weekly' },
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
// Timezone options - covering major cities worldwide
|
|
13
|
+
export const TIMEZONE_OPTIONS = [
|
|
14
|
+
{ label: 'UTC', value: 'UTC' },
|
|
15
|
+
|
|
16
|
+
// Americas
|
|
17
|
+
{ label: 'America/New_York (EST/EDT, UTC-5/-4)', value: 'America/New_York' },
|
|
18
|
+
{ label: 'America/Chicago (CST/CDT, UTC-6/-5)', value: 'America/Chicago' },
|
|
19
|
+
{ label: 'America/Denver (MST/MDT, UTC-7/-6)', value: 'America/Denver' },
|
|
20
|
+
{ label: 'America/Los_Angeles (PST/PDT, UTC-8/-7)', value: 'America/Los_Angeles' },
|
|
21
|
+
{ label: 'America/Toronto (EST/EDT, UTC-5/-4)', value: 'America/Toronto' },
|
|
22
|
+
{ label: 'America/Vancouver (PST/PDT, UTC-8/-7)', value: 'America/Vancouver' },
|
|
23
|
+
{ label: 'America/Mexico_City (CST, UTC-6)', value: 'America/Mexico_City' },
|
|
24
|
+
{ label: 'America/Sao_Paulo (BRT, UTC-3)', value: 'America/Sao_Paulo' },
|
|
25
|
+
{ label: 'America/Buenos_Aires (ART, UTC-3)', value: 'America/Buenos_Aires' },
|
|
26
|
+
|
|
27
|
+
// Europe
|
|
28
|
+
{ label: 'Europe/London (GMT/BST, UTC+0/+1)', value: 'Europe/London' },
|
|
29
|
+
{ label: 'Europe/Paris (CET/CEST, UTC+1/+2)', value: 'Europe/Paris' },
|
|
30
|
+
{ label: 'Europe/Berlin (CET/CEST, UTC+1/+2)', value: 'Europe/Berlin' },
|
|
31
|
+
{ label: 'Europe/Madrid (CET/CEST, UTC+1/+2)', value: 'Europe/Madrid' },
|
|
32
|
+
{ label: 'Europe/Rome (CET/CEST, UTC+1/+2)', value: 'Europe/Rome' },
|
|
33
|
+
{ label: 'Europe/Amsterdam (CET/CEST, UTC+1/+2)', value: 'Europe/Amsterdam' },
|
|
34
|
+
{ label: 'Europe/Brussels (CET/CEST, UTC+1/+2)', value: 'Europe/Brussels' },
|
|
35
|
+
{ label: 'Europe/Moscow (MSK, UTC+3)', value: 'Europe/Moscow' },
|
|
36
|
+
{ label: 'Europe/Istanbul (TRT, UTC+3)', value: 'Europe/Istanbul' },
|
|
37
|
+
|
|
38
|
+
// Asia
|
|
39
|
+
{ label: 'Asia/Dubai (GST, UTC+4)', value: 'Asia/Dubai' },
|
|
40
|
+
{ label: 'Asia/Kolkata (IST, UTC+5:30)', value: 'Asia/Kolkata' },
|
|
41
|
+
{ label: 'Asia/Shanghai (CST, UTC+8)', value: 'Asia/Shanghai' },
|
|
42
|
+
{ label: 'Asia/Hong_Kong (HKT, UTC+8)', value: 'Asia/Hong_Kong' },
|
|
43
|
+
{ label: 'Asia/Taipei (CST, UTC+8)', value: 'Asia/Taipei' },
|
|
44
|
+
{ label: 'Asia/Singapore (SGT, UTC+8)', value: 'Asia/Singapore' },
|
|
45
|
+
{ label: 'Asia/Tokyo (JST, UTC+9)', value: 'Asia/Tokyo' },
|
|
46
|
+
{ label: 'Asia/Seoul (KST, UTC+9)', value: 'Asia/Seoul' },
|
|
47
|
+
{ label: 'Asia/Bangkok (ICT, UTC+7)', value: 'Asia/Bangkok' },
|
|
48
|
+
{ label: 'Asia/Jakarta (WIB, UTC+7)', value: 'Asia/Jakarta' },
|
|
49
|
+
|
|
50
|
+
// Oceania
|
|
51
|
+
{ label: 'Australia/Sydney (AEDT/AEST, UTC+11/+10)', value: 'Australia/Sydney' },
|
|
52
|
+
{ label: 'Australia/Melbourne (AEDT/AEST, UTC+11/+10)', value: 'Australia/Melbourne' },
|
|
53
|
+
{ label: 'Australia/Brisbane (AEST, UTC+10)', value: 'Australia/Brisbane' },
|
|
54
|
+
{ label: 'Australia/Perth (AWST, UTC+8)', value: 'Australia/Perth' },
|
|
55
|
+
{ label: 'Pacific/Auckland (NZDT/NZST, UTC+13/+12)', value: 'Pacific/Auckland' },
|
|
56
|
+
|
|
57
|
+
// Africa & Middle East
|
|
58
|
+
{ label: 'Africa/Cairo (EET, UTC+2)', value: 'Africa/Cairo' },
|
|
59
|
+
{ label: 'Africa/Johannesburg (SAST, UTC+2)', value: 'Africa/Johannesburg' },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Weekday options for checkbox group
|
|
63
|
+
export const WEEKDAY_OPTIONS = [
|
|
64
|
+
{ label: 'Mon', value: 1 },
|
|
65
|
+
{ label: 'Tue', value: 2 },
|
|
66
|
+
{ label: 'Wed', value: 3 },
|
|
67
|
+
{ label: 'Thu', value: 4 },
|
|
68
|
+
{ label: 'Fri', value: 5 },
|
|
69
|
+
{ label: 'Sat', value: 6 },
|
|
70
|
+
{ label: 'Sun', value: 0 },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// Weekday labels for display
|
|
74
|
+
export const WEEKDAY_LABELS: Record<number, string> = {
|
|
75
|
+
0: 'Sunday',
|
|
76
|
+
1: 'Monday',
|
|
77
|
+
2: 'Tuesday',
|
|
78
|
+
3: 'Wednesday',
|
|
79
|
+
4: 'Thursday',
|
|
80
|
+
5: 'Friday',
|
|
81
|
+
6: 'Saturday',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse cron pattern to extract schedule info
|
|
86
|
+
* Format: minute hour day month weekday
|
|
87
|
+
*/
|
|
88
|
+
export const parseCronPattern = (
|
|
89
|
+
cronPattern: string,
|
|
90
|
+
): {
|
|
91
|
+
hourlyInterval?: number;
|
|
92
|
+
scheduleType: ScheduleType;
|
|
93
|
+
triggerHour: number;
|
|
94
|
+
triggerMinute: number;
|
|
95
|
+
weekdays?: number[];
|
|
96
|
+
} => {
|
|
97
|
+
const parts = cronPattern.split(' ');
|
|
98
|
+
if (parts.length !== 5) {
|
|
99
|
+
return { scheduleType: 'daily', triggerHour: 0, triggerMinute: 0 };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// eslint-disable-next-line unicorn/no-unreadable-array-destructuring
|
|
103
|
+
const [minute, hour, , , weekday] = parts;
|
|
104
|
+
const rawMinute = minute === '*' ? 0 : Number.parseInt(minute);
|
|
105
|
+
// Normalize to nearest 30-minute interval (0 or 30)
|
|
106
|
+
const triggerMinute = rawMinute >= 15 && rawMinute < 45 ? 30 : 0;
|
|
107
|
+
|
|
108
|
+
// Hourly: 0 * * * * or 0 */N * * *
|
|
109
|
+
if (hour.startsWith('*/')) {
|
|
110
|
+
const interval = Number.parseInt(hour.slice(2));
|
|
111
|
+
return {
|
|
112
|
+
hourlyInterval: interval,
|
|
113
|
+
scheduleType: 'hourly',
|
|
114
|
+
triggerHour: 0,
|
|
115
|
+
triggerMinute,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (hour === '*') {
|
|
119
|
+
return {
|
|
120
|
+
hourlyInterval: 1,
|
|
121
|
+
scheduleType: 'hourly',
|
|
122
|
+
triggerHour: 0,
|
|
123
|
+
triggerMinute,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const triggerHour = Number.parseInt(hour);
|
|
128
|
+
|
|
129
|
+
// Weekly: has specific weekday(s)
|
|
130
|
+
if (weekday !== '*') {
|
|
131
|
+
const weekdays = weekday.split(',').map((d) => Number.parseInt(d));
|
|
132
|
+
return {
|
|
133
|
+
scheduleType: 'weekly',
|
|
134
|
+
triggerHour,
|
|
135
|
+
triggerMinute,
|
|
136
|
+
weekdays,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Daily: specific hour, any weekday
|
|
141
|
+
return {
|
|
142
|
+
scheduleType: 'daily',
|
|
143
|
+
triggerHour,
|
|
144
|
+
triggerMinute,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build cron pattern from schedule info
|
|
150
|
+
* Format: minute hour day month weekday
|
|
151
|
+
*/
|
|
152
|
+
export const buildCronPattern = (
|
|
153
|
+
scheduleType: ScheduleType,
|
|
154
|
+
triggerTime: Dayjs,
|
|
155
|
+
hourlyInterval?: number,
|
|
156
|
+
weekdays?: number[],
|
|
157
|
+
): string => {
|
|
158
|
+
const rawMinute = triggerTime.minute();
|
|
159
|
+
// Normalize to 0 or 30
|
|
160
|
+
const minute = rawMinute >= 15 && rawMinute < 45 ? 30 : 0;
|
|
161
|
+
const hour = triggerTime.hour();
|
|
162
|
+
|
|
163
|
+
switch (scheduleType) {
|
|
164
|
+
case 'hourly': {
|
|
165
|
+
const interval = hourlyInterval || 1;
|
|
166
|
+
if (interval === 1) {
|
|
167
|
+
return `${minute} * * * *`;
|
|
168
|
+
}
|
|
169
|
+
return `${minute} */${interval} * * *`;
|
|
170
|
+
}
|
|
171
|
+
case 'daily': {
|
|
172
|
+
return `${minute} ${hour} * * *`;
|
|
173
|
+
}
|
|
174
|
+
case 'weekly': {
|
|
175
|
+
const days = weekdays && weekdays.length > 0 ? weekdays.sort().join(',') : '0,1,2,3,4,5,6';
|
|
176
|
+
return `${minute} ${hour} * * ${days}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReactCodePlugin,
|
|
3
|
+
ReactCodemirrorPlugin,
|
|
4
|
+
ReactHRPlugin,
|
|
5
|
+
ReactLinkPlugin,
|
|
6
|
+
ReactListPlugin,
|
|
7
|
+
ReactMathPlugin,
|
|
8
|
+
ReactTablePlugin,
|
|
9
|
+
} from '@lobehub/editor';
|
|
10
|
+
import { Editor, useEditor } from '@lobehub/editor/react';
|
|
11
|
+
import { Flexbox, Icon, Text } from '@lobehub/ui';
|
|
12
|
+
import { Card } from 'antd';
|
|
13
|
+
import { Clock } from 'lucide-react';
|
|
14
|
+
import { memo, useCallback, useEffect, useRef } from 'react';
|
|
15
|
+
import { useTranslation } from 'react-i18next';
|
|
16
|
+
|
|
17
|
+
interface CronJobContentEditorProps {
|
|
18
|
+
enableRichRender: boolean;
|
|
19
|
+
initialValue: string;
|
|
20
|
+
onChange: (value: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CronJobContentEditor = memo<CronJobContentEditorProps>(
|
|
24
|
+
({ enableRichRender, initialValue, onChange }) => {
|
|
25
|
+
const { t } = useTranslation('setting');
|
|
26
|
+
const editor = useEditor();
|
|
27
|
+
const currentValueRef = useRef(initialValue);
|
|
28
|
+
|
|
29
|
+
// Update currentValueRef when initialValue changes
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
currentValueRef.current = initialValue;
|
|
32
|
+
}, [initialValue]);
|
|
33
|
+
|
|
34
|
+
// Initialize editor content when editor is ready
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!editor) return;
|
|
37
|
+
try {
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
if (initialValue) {
|
|
40
|
+
editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
|
|
41
|
+
}
|
|
42
|
+
}, 100);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('[CronJobContentEditor] Failed to initialize editor content:', error);
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
|
|
47
|
+
}, 100);
|
|
48
|
+
}
|
|
49
|
+
}, [editor, enableRichRender, initialValue]);
|
|
50
|
+
|
|
51
|
+
// Handle content changes
|
|
52
|
+
const handleContentChange = useCallback(
|
|
53
|
+
(e: any) => {
|
|
54
|
+
const nextContent = enableRichRender
|
|
55
|
+
? (e.getDocument('markdown') as unknown as string)
|
|
56
|
+
: (e.getDocument('text') as unknown as string);
|
|
57
|
+
|
|
58
|
+
const finalContent = nextContent || '';
|
|
59
|
+
|
|
60
|
+
// Only call onChange if content actually changed
|
|
61
|
+
if (finalContent !== currentValueRef.current) {
|
|
62
|
+
currentValueRef.current = finalContent;
|
|
63
|
+
onChange(finalContent);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
[enableRichRender, onChange],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Flexbox gap={12}>
|
|
71
|
+
<Flexbox align="center" gap={6} horizontal>
|
|
72
|
+
<Icon icon={Clock} size={16} />
|
|
73
|
+
<Text style={{ fontWeight: 600 }}>{t('agentCronJobs.content')}</Text>
|
|
74
|
+
</Flexbox>
|
|
75
|
+
<Card
|
|
76
|
+
size="small"
|
|
77
|
+
style={{ borderRadius: 12, overflow: 'hidden' }}
|
|
78
|
+
styles={{ body: { padding: 0 } }}
|
|
79
|
+
>
|
|
80
|
+
<Flexbox padding={16} style={{ minHeight: 220 }}>
|
|
81
|
+
<Editor
|
|
82
|
+
content={''}
|
|
83
|
+
editor={editor}
|
|
84
|
+
lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
|
|
85
|
+
onTextChange={handleContentChange}
|
|
86
|
+
placeholder={t('agentCronJobs.form.content.placeholder')}
|
|
87
|
+
plugins={
|
|
88
|
+
enableRichRender
|
|
89
|
+
? [
|
|
90
|
+
ReactListPlugin,
|
|
91
|
+
ReactCodePlugin,
|
|
92
|
+
ReactCodemirrorPlugin,
|
|
93
|
+
ReactHRPlugin,
|
|
94
|
+
ReactLinkPlugin,
|
|
95
|
+
ReactTablePlugin,
|
|
96
|
+
ReactMathPlugin,
|
|
97
|
+
]
|
|
98
|
+
: undefined
|
|
99
|
+
}
|
|
100
|
+
style={{ paddingBottom: 48 }}
|
|
101
|
+
type={'text'}
|
|
102
|
+
variant={'chat'}
|
|
103
|
+
/>
|
|
104
|
+
</Flexbox>
|
|
105
|
+
</Card>
|
|
106
|
+
</Flexbox>
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
export default CronJobContentEditor;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Flexbox, Input } from '@lobehub/ui';
|
|
2
|
+
import { Switch } from 'antd';
|
|
3
|
+
import { memo } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
interface CronJobHeaderProps {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
isNewJob?: boolean;
|
|
9
|
+
name: string;
|
|
10
|
+
onNameChange: (name: string) => void;
|
|
11
|
+
onToggleEnabled?: (enabled: boolean) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const CronJobHeader = memo<CronJobHeaderProps>(
|
|
15
|
+
({ enabled, isNewJob, name, onNameChange, onToggleEnabled }) => {
|
|
16
|
+
const { t } = useTranslation(['setting', 'common']);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Flexbox gap={16}>
|
|
20
|
+
{/* Title Input */}
|
|
21
|
+
<Input
|
|
22
|
+
onChange={(e) => onNameChange(e.target.value)}
|
|
23
|
+
placeholder={t('agentCronJobs.form.name.placeholder')}
|
|
24
|
+
style={{
|
|
25
|
+
fontSize: 28,
|
|
26
|
+
fontWeight: 600,
|
|
27
|
+
padding: 0,
|
|
28
|
+
}}
|
|
29
|
+
value={name}
|
|
30
|
+
variant={'borderless'}
|
|
31
|
+
/>
|
|
32
|
+
|
|
33
|
+
{/* Controls Row */}
|
|
34
|
+
{!isNewJob && (
|
|
35
|
+
<Flexbox align="center" gap={12} horizontal>
|
|
36
|
+
{/* Enable/Disable Switch */}
|
|
37
|
+
<Switch checked={enabled ?? false} onChange={onToggleEnabled} />
|
|
38
|
+
</Flexbox>
|
|
39
|
+
)}
|
|
40
|
+
</Flexbox>
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export default CronJobHeader;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Button, Flexbox } from '@lobehub/ui';
|
|
2
|
+
import { Save } from 'lucide-react';
|
|
3
|
+
import { memo } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
interface CronJobSaveButtonProps {
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
onSave: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CronJobSaveButton = memo<CronJobSaveButtonProps>(({ disabled, loading, onSave }) => {
|
|
13
|
+
const { t } = useTranslation('setting');
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Flexbox paddingBlock={16}>
|
|
17
|
+
<Button
|
|
18
|
+
disabled={disabled}
|
|
19
|
+
icon={Save}
|
|
20
|
+
loading={loading}
|
|
21
|
+
onClick={onSave}
|
|
22
|
+
style={{ width: 200 }}
|
|
23
|
+
type="primary"
|
|
24
|
+
>
|
|
25
|
+
{t('agentCronJobs.saveAsNew', { defaultValue: 'Save as New Scheduled Task' })}
|
|
26
|
+
</Button>
|
|
27
|
+
</Flexbox>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export default CronJobSaveButton;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Flexbox, Tag, Text } from '@lobehub/ui';
|
|
2
|
+
import { Card, InputNumber, Select, TimePicker } from 'antd';
|
|
3
|
+
import type { Dayjs } from 'dayjs';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import { memo, useMemo } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
SCHEDULE_TYPE_OPTIONS,
|
|
10
|
+
type ScheduleType,
|
|
11
|
+
TIMEZONE_OPTIONS,
|
|
12
|
+
WEEKDAY_LABELS,
|
|
13
|
+
WEEKDAY_OPTIONS,
|
|
14
|
+
} from '../CronConfig';
|
|
15
|
+
|
|
16
|
+
interface CronJobScheduleConfigProps {
|
|
17
|
+
hourlyInterval?: number;
|
|
18
|
+
maxExecutions?: number | null;
|
|
19
|
+
onScheduleChange: (updates: {
|
|
20
|
+
hourlyInterval?: number;
|
|
21
|
+
maxExecutions?: number | null;
|
|
22
|
+
scheduleType?: ScheduleType;
|
|
23
|
+
timezone?: string;
|
|
24
|
+
triggerTime?: Dayjs;
|
|
25
|
+
weekdays?: number[];
|
|
26
|
+
}) => void;
|
|
27
|
+
scheduleType: ScheduleType;
|
|
28
|
+
timezone: string;
|
|
29
|
+
triggerTime: Dayjs;
|
|
30
|
+
weekdays: number[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const CronJobScheduleConfig = memo<CronJobScheduleConfigProps>(
|
|
34
|
+
({
|
|
35
|
+
hourlyInterval,
|
|
36
|
+
maxExecutions,
|
|
37
|
+
onScheduleChange,
|
|
38
|
+
scheduleType,
|
|
39
|
+
timezone,
|
|
40
|
+
triggerTime,
|
|
41
|
+
weekdays,
|
|
42
|
+
}) => {
|
|
43
|
+
const { t } = useTranslation('setting');
|
|
44
|
+
|
|
45
|
+
// Compute summary tags
|
|
46
|
+
const summaryTags = useMemo(() => {
|
|
47
|
+
const result: Array<{ key: string; label: string }> = [];
|
|
48
|
+
|
|
49
|
+
// Schedule type
|
|
50
|
+
const scheduleTypeLabel = SCHEDULE_TYPE_OPTIONS.find(
|
|
51
|
+
(opt) => opt.value === scheduleType,
|
|
52
|
+
)?.label;
|
|
53
|
+
if (scheduleTypeLabel) {
|
|
54
|
+
result.push({
|
|
55
|
+
key: 'scheduleType',
|
|
56
|
+
label: t(scheduleTypeLabel as any),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Trigger time
|
|
61
|
+
if (scheduleType === 'hourly') {
|
|
62
|
+
const minute = triggerTime.minute();
|
|
63
|
+
result.push({
|
|
64
|
+
key: 'interval',
|
|
65
|
+
label: `Every ${hourlyInterval || 1} hour(s) at :${minute.toString().padStart(2, '0')}`,
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
result.push({
|
|
69
|
+
key: 'triggerTime',
|
|
70
|
+
label: triggerTime.format('HH:mm'),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Timezone
|
|
75
|
+
result.push({
|
|
76
|
+
key: 'timezone',
|
|
77
|
+
label: timezone,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Weekdays for weekly schedule
|
|
81
|
+
if (scheduleType === 'weekly' && weekdays.length > 0) {
|
|
82
|
+
result.push({
|
|
83
|
+
key: 'weekdays',
|
|
84
|
+
label: weekdays.map((day) => WEEKDAY_LABELS[day]).join(', '),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}, [scheduleType, triggerTime, timezone, weekdays, hourlyInterval, t]);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<Card size="small" style={{ borderRadius: 12 }} styles={{ body: { padding: 12 } }}>
|
|
93
|
+
<Flexbox gap={12}>
|
|
94
|
+
{/* Summary Tags */}
|
|
95
|
+
{summaryTags.length > 0 && (
|
|
96
|
+
<Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
|
|
97
|
+
{summaryTags.map((tag) => (
|
|
98
|
+
<Tag key={tag.key} variant={'filled'}>
|
|
99
|
+
{tag.label}
|
|
100
|
+
</Tag>
|
|
101
|
+
))}
|
|
102
|
+
</Flexbox>
|
|
103
|
+
)}
|
|
104
|
+
{/* Schedule Configuration - All in one row */}
|
|
105
|
+
<Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
|
|
106
|
+
<Tag variant={'borderless'}>{t('agentCronJobs.schedule')}</Tag>
|
|
107
|
+
<Select
|
|
108
|
+
onChange={(value: ScheduleType) =>
|
|
109
|
+
onScheduleChange({
|
|
110
|
+
scheduleType: value,
|
|
111
|
+
weekdays: value === 'weekly' ? [0, 1, 2, 3, 4, 5, 6] : [],
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
options={SCHEDULE_TYPE_OPTIONS.map((opt) => ({
|
|
115
|
+
label: t(opt.label as any),
|
|
116
|
+
value: opt.value,
|
|
117
|
+
}))}
|
|
118
|
+
size="small"
|
|
119
|
+
style={{ minWidth: 120 }}
|
|
120
|
+
value={scheduleType}
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
{/* Weekdays - show only for weekly */}
|
|
124
|
+
{scheduleType === 'weekly' && (
|
|
125
|
+
<Select
|
|
126
|
+
maxTagCount="responsive"
|
|
127
|
+
mode="multiple"
|
|
128
|
+
onChange={(values: number[]) => onScheduleChange({ weekdays: values })}
|
|
129
|
+
options={WEEKDAY_OPTIONS}
|
|
130
|
+
placeholder="Select days"
|
|
131
|
+
size="small"
|
|
132
|
+
style={{ minWidth: 150 }}
|
|
133
|
+
value={weekdays}
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{/* Trigger Time - show for daily and weekly */}
|
|
138
|
+
{scheduleType !== 'hourly' && (
|
|
139
|
+
<TimePicker
|
|
140
|
+
format="HH:mm"
|
|
141
|
+
minuteStep={30}
|
|
142
|
+
onChange={(value) => {
|
|
143
|
+
if (value) onScheduleChange({ triggerTime: value });
|
|
144
|
+
}}
|
|
145
|
+
size="small"
|
|
146
|
+
style={{ minWidth: 120 }}
|
|
147
|
+
value={triggerTime ?? dayjs().hour(0).minute(0)}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{/* Hourly Interval - show only for hourly */}
|
|
152
|
+
{scheduleType === 'hourly' && (
|
|
153
|
+
<>
|
|
154
|
+
<Tag variant={'borderless'}>Every</Tag>
|
|
155
|
+
<InputNumber
|
|
156
|
+
max={24}
|
|
157
|
+
min={1}
|
|
158
|
+
onChange={(value: number | null) =>
|
|
159
|
+
onScheduleChange({ hourlyInterval: value ?? 1 })
|
|
160
|
+
}
|
|
161
|
+
size="small"
|
|
162
|
+
style={{ width: 80 }}
|
|
163
|
+
value={hourlyInterval ?? 1}
|
|
164
|
+
/>
|
|
165
|
+
<Text type="secondary">hour(s) at</Text>
|
|
166
|
+
<Select
|
|
167
|
+
onChange={(value: number) =>
|
|
168
|
+
onScheduleChange({ triggerTime: dayjs().hour(0).minute(value) })
|
|
169
|
+
}
|
|
170
|
+
options={[
|
|
171
|
+
{ label: ':00', value: 0 },
|
|
172
|
+
{ label: ':30', value: 30 },
|
|
173
|
+
]}
|
|
174
|
+
size="small"
|
|
175
|
+
style={{ width: 80 }}
|
|
176
|
+
value={triggerTime?.minute() ?? 0}
|
|
177
|
+
/>
|
|
178
|
+
</>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{/* Timezone */}
|
|
182
|
+
<Select
|
|
183
|
+
onChange={(value: string) => onScheduleChange({ timezone: value })}
|
|
184
|
+
options={TIMEZONE_OPTIONS}
|
|
185
|
+
showSearch
|
|
186
|
+
size="small"
|
|
187
|
+
style={{ maxWidth: 300, minWidth: 200 }}
|
|
188
|
+
value={timezone}
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
</Flexbox>
|
|
192
|
+
|
|
193
|
+
{/* Max Executions */}
|
|
194
|
+
<Flexbox align="center" gap={8} horizontal style={{ flexWrap: 'wrap' }}>
|
|
195
|
+
<Tag variant={'borderless'}>{t('agentCronJobs.maxExecutions')}</Tag>
|
|
196
|
+
<InputNumber
|
|
197
|
+
min={1}
|
|
198
|
+
onChange={(value: number | null) =>
|
|
199
|
+
onScheduleChange({ maxExecutions: value ?? null })
|
|
200
|
+
}
|
|
201
|
+
placeholder={t('agentCronJobs.form.maxExecutions.placeholder')}
|
|
202
|
+
size="small"
|
|
203
|
+
style={{ width: 160 }}
|
|
204
|
+
value={maxExecutions ?? null}
|
|
205
|
+
/>
|
|
206
|
+
</Flexbox>
|
|
207
|
+
</Flexbox>
|
|
208
|
+
</Card>
|
|
209
|
+
);
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
export default CronJobScheduleConfig;
|