strapi-content-sync-pro 1.0.0
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/LICENSE +21 -0
- package/README.md +206 -0
- package/admin/src/components/ConfigTab.jsx +1038 -0
- package/admin/src/components/ContentTypesTab.jsx +160 -0
- package/admin/src/components/HelpTab.jsx +945 -0
- package/admin/src/components/LogsTab.jsx +136 -0
- package/admin/src/components/MediaTab.jsx +557 -0
- package/admin/src/components/SyncProfilesTab.jsx +715 -0
- package/admin/src/components/SyncTab.jsx +988 -0
- package/admin/src/index.js +31 -0
- package/admin/src/pages/App/index.jsx +129 -0
- package/admin/src/pluginId.js +3 -0
- package/package.json +84 -0
- package/server/src/bootstrap.js +151 -0
- package/server/src/config/index.js +5 -0
- package/server/src/content-types/index.js +7 -0
- package/server/src/content-types/sync-log/schema.json +24 -0
- package/server/src/controllers/alerts.js +59 -0
- package/server/src/controllers/config.js +292 -0
- package/server/src/controllers/content-type-discovery.js +9 -0
- package/server/src/controllers/dependencies.js +109 -0
- package/server/src/controllers/index.js +29 -0
- package/server/src/controllers/ping.js +7 -0
- package/server/src/controllers/sync-config.js +26 -0
- package/server/src/controllers/sync-enforcement.js +323 -0
- package/server/src/controllers/sync-execution.js +134 -0
- package/server/src/controllers/sync-log.js +18 -0
- package/server/src/controllers/sync-media.js +158 -0
- package/server/src/controllers/sync-profiles.js +182 -0
- package/server/src/controllers/sync.js +31 -0
- package/server/src/destroy.js +7 -0
- package/server/src/index.js +21 -0
- package/server/src/middlewares/verify-signature.js +32 -0
- package/server/src/register.js +7 -0
- package/server/src/routes/index.js +111 -0
- package/server/src/services/alerts.js +437 -0
- package/server/src/services/config.js +68 -0
- package/server/src/services/content-type-discovery.js +41 -0
- package/server/src/services/dependency-resolver.js +284 -0
- package/server/src/services/index.js +30 -0
- package/server/src/services/ping.js +7 -0
- package/server/src/services/sync-config.js +45 -0
- package/server/src/services/sync-enforcement.js +362 -0
- package/server/src/services/sync-execution.js +541 -0
- package/server/src/services/sync-log.js +56 -0
- package/server/src/services/sync-media.js +963 -0
- package/server/src/services/sync-profiles.js +380 -0
- package/server/src/services/sync.js +248 -0
- package/server/src/utils/applier.js +89 -0
- package/server/src/utils/comparator.js +83 -0
- package/server/src/utils/fetcher.js +142 -0
- package/server/src/utils/hmac.js +37 -0
- package/server/src/utils/pagination.js +51 -0
- package/server/src/utils/sync-guard.js +29 -0
- package/server/src/utils/sync-id.js +16 -0
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Flex,
|
|
5
|
+
Typography,
|
|
6
|
+
Button,
|
|
7
|
+
Alert,
|
|
8
|
+
SingleSelect,
|
|
9
|
+
SingleSelectOption,
|
|
10
|
+
Field,
|
|
11
|
+
Switch,
|
|
12
|
+
Badge,
|
|
13
|
+
Table,
|
|
14
|
+
Thead,
|
|
15
|
+
Tbody,
|
|
16
|
+
Tr,
|
|
17
|
+
Th,
|
|
18
|
+
Td,
|
|
19
|
+
Tabs,
|
|
20
|
+
NumberInput,
|
|
21
|
+
Modal,
|
|
22
|
+
IconButton,
|
|
23
|
+
Checkbox,
|
|
24
|
+
TextInput,
|
|
25
|
+
} from '@strapi/design-system';
|
|
26
|
+
import { Play, Clock, Cog, ArrowUp, ArrowDown } from '@strapi/icons';
|
|
27
|
+
import { useFetchClient } from '@strapi/strapi/admin';
|
|
28
|
+
|
|
29
|
+
const PLUGIN_ID = 'strapi-content-sync-pro';
|
|
30
|
+
|
|
31
|
+
const EXECUTION_MODE_OPTIONS = [
|
|
32
|
+
{ value: 'on_demand', label: 'On Demand (Manual)' },
|
|
33
|
+
{ value: 'scheduled', label: 'Scheduled (Batch)' },
|
|
34
|
+
{ value: 'live', label: 'Live (Real-time)' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const FILTER_OPTIONS = [
|
|
38
|
+
{ value: 'all', label: 'All Profiles' },
|
|
39
|
+
{ value: 'active', label: 'Active Only' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const SyncTab = () => {
|
|
43
|
+
const { get, post, put } = useFetchClient();
|
|
44
|
+
|
|
45
|
+
const [syncing, setSyncing] = useState(false);
|
|
46
|
+
const [executingProfiles, setExecutingProfiles] = useState(new Set());
|
|
47
|
+
const [result, setResult] = useState(null);
|
|
48
|
+
const [error, setError] = useState(null);
|
|
49
|
+
const [message, setMessage] = useState(null);
|
|
50
|
+
|
|
51
|
+
// Profiles and execution status
|
|
52
|
+
const [profiles, setProfiles] = useState([]);
|
|
53
|
+
const [executionStatus, setExecutionStatus] = useState([]);
|
|
54
|
+
const [globalSettings, setGlobalSettings] = useState({});
|
|
55
|
+
const [loading, setLoading] = useState(true);
|
|
56
|
+
|
|
57
|
+
// Filter and ordering
|
|
58
|
+
const [profileFilter, setProfileFilter] = useState('all');
|
|
59
|
+
const [executionOrder, setExecutionOrder] = useState({}); // { profileId: order }
|
|
60
|
+
const [orderModified, setOrderModified] = useState(false);
|
|
61
|
+
|
|
62
|
+
// Selection for batch execution
|
|
63
|
+
const [selectedProfiles, setSelectedProfiles] = useState([]);
|
|
64
|
+
|
|
65
|
+
// Execution settings modal
|
|
66
|
+
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
|
67
|
+
const [selectedProfile, setSelectedProfile] = useState(null);
|
|
68
|
+
const [executionSettings, setExecutionSettings] = useState({
|
|
69
|
+
executionMode: 'on_demand',
|
|
70
|
+
scheduleType: 'interval',
|
|
71
|
+
scheduleInterval: 60,
|
|
72
|
+
cronExpression: '0 * * * *',
|
|
73
|
+
enabled: false,
|
|
74
|
+
syncDependencies: false,
|
|
75
|
+
dependencyDepth: 1,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Dependencies data
|
|
79
|
+
const [dependencies, setDependencies] = useState({});
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
loadData();
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const loadData = async () => {
|
|
86
|
+
try {
|
|
87
|
+
const [profilesRes, statusRes, globalRes, depsRes] = await Promise.all([
|
|
88
|
+
get(`/${PLUGIN_ID}/sync-profiles`),
|
|
89
|
+
get(`/${PLUGIN_ID}/sync-execution/status`),
|
|
90
|
+
get(`/${PLUGIN_ID}/sync-execution/global-settings`),
|
|
91
|
+
get(`/${PLUGIN_ID}/dependencies/all`).catch(() => ({ data: { data: {} } })),
|
|
92
|
+
]);
|
|
93
|
+
const loadedProfiles = profilesRes.data.data || [];
|
|
94
|
+
setProfiles(loadedProfiles);
|
|
95
|
+
setExecutionStatus(statusRes.data.data || []);
|
|
96
|
+
setGlobalSettings(globalRes.data.data || {});
|
|
97
|
+
setDependencies(depsRes.data.data || {});
|
|
98
|
+
|
|
99
|
+
// Load saved execution order or calculate from dependencies
|
|
100
|
+
const savedOrder = globalRes.data.data?.executionOrder || {};
|
|
101
|
+
if (Object.keys(savedOrder).length > 0) {
|
|
102
|
+
setExecutionOrder(savedOrder);
|
|
103
|
+
} else {
|
|
104
|
+
// Calculate initial order from dependencies
|
|
105
|
+
const calculatedOrder = calculateExecutionOrder(loadedProfiles, depsRes.data.data || {});
|
|
106
|
+
setExecutionOrder(calculatedOrder);
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('Failed to load sync data', err);
|
|
110
|
+
setMessage({ type: 'danger', text: err?.response?.data?.error?.message || err.message || 'Failed to load sync data' });
|
|
111
|
+
} finally {
|
|
112
|
+
setLoading(false);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Calculate execution order based on dependencies.
|
|
118
|
+
* Profiles that others depend on get higher priority (lower order number).
|
|
119
|
+
* More dependencies = lower priority (higher order number).
|
|
120
|
+
*/
|
|
121
|
+
const calculateExecutionOrder = (profileList, deps) => {
|
|
122
|
+
const order = {};
|
|
123
|
+
const contentTypeToProfile = {};
|
|
124
|
+
|
|
125
|
+
// Map content types to profiles
|
|
126
|
+
profileList.forEach(p => {
|
|
127
|
+
contentTypeToProfile[p.contentType] = p.id;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Count how many profiles depend on each profile
|
|
131
|
+
const dependedOnCount = {};
|
|
132
|
+
const dependsOnCount = {};
|
|
133
|
+
|
|
134
|
+
profileList.forEach(p => {
|
|
135
|
+
dependedOnCount[p.id] = 0;
|
|
136
|
+
dependsOnCount[p.id] = 0;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Analyze dependencies
|
|
140
|
+
profileList.forEach(profile => {
|
|
141
|
+
const contentTypeDeps = deps[profile.contentType] || [];
|
|
142
|
+
contentTypeDeps.forEach(dep => {
|
|
143
|
+
const depProfileId = contentTypeToProfile[dep.target];
|
|
144
|
+
if (depProfileId && depProfileId !== profile.id) {
|
|
145
|
+
// This profile depends on depProfileId
|
|
146
|
+
dependsOnCount[profile.id] = (dependsOnCount[profile.id] || 0) + 1;
|
|
147
|
+
// depProfileId is depended on by this profile
|
|
148
|
+
dependedOnCount[depProfileId] = (dependedOnCount[depProfileId] || 0) + 1;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Sort profiles: higher dependedOnCount = lower order (executes first)
|
|
154
|
+
// Lower dependsOnCount = lower order (executes first)
|
|
155
|
+
const sortedProfiles = [...profileList].sort((a, b) => {
|
|
156
|
+
// Primary: profiles that are depended on more should execute first
|
|
157
|
+
const depOnDiff = (dependedOnCount[b.id] || 0) - (dependedOnCount[a.id] || 0);
|
|
158
|
+
if (depOnDiff !== 0) return depOnDiff;
|
|
159
|
+
|
|
160
|
+
// Secondary: profiles with fewer dependencies should execute first
|
|
161
|
+
const depsOnDiff = (dependsOnCount[a.id] || 0) - (dependsOnCount[b.id] || 0);
|
|
162
|
+
if (depsOnDiff !== 0) return depsOnDiff;
|
|
163
|
+
|
|
164
|
+
// Tertiary: alphabetical by name
|
|
165
|
+
return a.name.localeCompare(b.name);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
sortedProfiles.forEach((p, index) => {
|
|
169
|
+
order[p.id] = index + 1;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return order;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const handleResetOrder = () => {
|
|
176
|
+
const calculatedOrder = calculateExecutionOrder(profiles, dependencies);
|
|
177
|
+
setExecutionOrder(calculatedOrder);
|
|
178
|
+
setOrderModified(false);
|
|
179
|
+
setMessage({ type: 'success', text: 'Execution order reset to dependency-based calculation' });
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const handleSaveOrder = async () => {
|
|
183
|
+
try {
|
|
184
|
+
await put(`/${PLUGIN_ID}/sync-execution/global-settings`, {
|
|
185
|
+
...globalSettings,
|
|
186
|
+
executionOrder,
|
|
187
|
+
});
|
|
188
|
+
setOrderModified(false);
|
|
189
|
+
setMessage({ type: 'success', text: 'Execution order saved' });
|
|
190
|
+
} catch (err) {
|
|
191
|
+
setMessage({ type: 'danger', text: err?.response?.data?.error?.message || err.message || 'Failed to save execution order' });
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const handleOrderChange = (profileId, newOrder) => {
|
|
196
|
+
const parsed = parseInt(newOrder, 10);
|
|
197
|
+
if (isNaN(parsed) || parsed < 1) return;
|
|
198
|
+
|
|
199
|
+
setExecutionOrder(prev => ({
|
|
200
|
+
...prev,
|
|
201
|
+
[profileId]: parsed,
|
|
202
|
+
}));
|
|
203
|
+
setOrderModified(true);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleMoveUp = (profileId) => {
|
|
207
|
+
const currentOrder = executionOrder[profileId] || 999;
|
|
208
|
+
if (currentOrder <= 1) return;
|
|
209
|
+
|
|
210
|
+
// Find profile with the order one less
|
|
211
|
+
const targetOrder = currentOrder - 1;
|
|
212
|
+
const swapProfileId = Object.keys(executionOrder).find(
|
|
213
|
+
id => executionOrder[id] === targetOrder
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
setExecutionOrder(prev => {
|
|
217
|
+
const newOrder = { ...prev };
|
|
218
|
+
newOrder[profileId] = targetOrder;
|
|
219
|
+
if (swapProfileId) {
|
|
220
|
+
newOrder[swapProfileId] = currentOrder;
|
|
221
|
+
}
|
|
222
|
+
return newOrder;
|
|
223
|
+
});
|
|
224
|
+
setOrderModified(true);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handleMoveDown = (profileId) => {
|
|
228
|
+
const currentOrder = executionOrder[profileId] || 1;
|
|
229
|
+
const maxOrder = profiles.length;
|
|
230
|
+
if (currentOrder >= maxOrder) return;
|
|
231
|
+
|
|
232
|
+
// Find profile with the order one more
|
|
233
|
+
const targetOrder = currentOrder + 1;
|
|
234
|
+
const swapProfileId = Object.keys(executionOrder).find(
|
|
235
|
+
id => executionOrder[id] === targetOrder
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
setExecutionOrder(prev => {
|
|
239
|
+
const newOrder = { ...prev };
|
|
240
|
+
newOrder[profileId] = targetOrder;
|
|
241
|
+
if (swapProfileId) {
|
|
242
|
+
newOrder[swapProfileId] = currentOrder;
|
|
243
|
+
}
|
|
244
|
+
return newOrder;
|
|
245
|
+
});
|
|
246
|
+
setOrderModified(true);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const handleSyncAll = async () => {
|
|
250
|
+
setSyncing(true);
|
|
251
|
+
setResult(null);
|
|
252
|
+
setError(null);
|
|
253
|
+
try {
|
|
254
|
+
const { data } = await post(`/${PLUGIN_ID}/sync-now`);
|
|
255
|
+
setResult(data.data);
|
|
256
|
+
setMessage({ type: 'success', text: 'All active profiles synced successfully' });
|
|
257
|
+
loadData();
|
|
258
|
+
} catch (err) {
|
|
259
|
+
setError(err.response?.data?.error?.message || err.message || 'Sync failed');
|
|
260
|
+
} finally {
|
|
261
|
+
setSyncing(false);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const handleExecuteProfile = async (profileId) => {
|
|
266
|
+
setExecutingProfiles(prev => new Set([...prev, profileId]));
|
|
267
|
+
try {
|
|
268
|
+
await post(`/${PLUGIN_ID}/sync-execution/execute/${profileId}`);
|
|
269
|
+
const profile = profiles.find(p => p.id === profileId);
|
|
270
|
+
setMessage({ type: 'success', text: `Sync completed: ${profile?.name || profileId}` });
|
|
271
|
+
loadData();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
setMessage({ type: 'danger', text: err.response?.data?.error?.message || 'Execution failed' });
|
|
274
|
+
} finally {
|
|
275
|
+
setExecutingProfiles(prev => {
|
|
276
|
+
const next = new Set(prev);
|
|
277
|
+
next.delete(profileId);
|
|
278
|
+
return next;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const handleExecuteSelected = async () => {
|
|
284
|
+
if (selectedProfiles.length === 0) return;
|
|
285
|
+
setSyncing(true);
|
|
286
|
+
try {
|
|
287
|
+
const { data } = await post(`/${PLUGIN_ID}/sync-execution/execute-batch`, { profileIds: selectedProfiles });
|
|
288
|
+
const successCount = data.data?.results?.length || 0;
|
|
289
|
+
const errorCount = data.data?.errors?.length || 0;
|
|
290
|
+
setMessage({
|
|
291
|
+
type: errorCount > 0 ? 'warning' : 'success',
|
|
292
|
+
text: `Batch sync: ${successCount} succeeded, ${errorCount} failed`
|
|
293
|
+
});
|
|
294
|
+
setSelectedProfiles([]);
|
|
295
|
+
loadData();
|
|
296
|
+
} catch (err) {
|
|
297
|
+
setMessage({ type: 'danger', text: err.response?.data?.error?.message || 'Batch execution failed' });
|
|
298
|
+
} finally {
|
|
299
|
+
setSyncing(false);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const handleSelectProfile = (profileId) => {
|
|
304
|
+
// Only allow selecting active profiles
|
|
305
|
+
const profile = profiles.find(p => p.id === profileId);
|
|
306
|
+
if (!profile?.isActive) return;
|
|
307
|
+
|
|
308
|
+
setSelectedProfiles(prev =>
|
|
309
|
+
prev.includes(profileId)
|
|
310
|
+
? prev.filter(id => id !== profileId)
|
|
311
|
+
: [...prev, profileId]
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const handleSelectAllActive = () => {
|
|
316
|
+
const activeIds = filteredProfiles.filter(p => p.isActive).map(p => p.id);
|
|
317
|
+
const allSelected = activeIds.every(id => selectedProfiles.includes(id));
|
|
318
|
+
if (allSelected) {
|
|
319
|
+
setSelectedProfiles(prev => prev.filter(id => !activeIds.includes(id)));
|
|
320
|
+
} else {
|
|
321
|
+
setSelectedProfiles(prev => [...new Set([...prev, ...activeIds])]);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const openSettingsModal = async (profileId) => {
|
|
326
|
+
try {
|
|
327
|
+
const res = await get(`/${PLUGIN_ID}/sync-execution/settings/${profileId}`);
|
|
328
|
+
setExecutionSettings(res.data.data || {
|
|
329
|
+
executionMode: 'on_demand',
|
|
330
|
+
scheduleType: 'interval',
|
|
331
|
+
scheduleInterval: 60,
|
|
332
|
+
cronExpression: '0 * * * *',
|
|
333
|
+
enabled: false,
|
|
334
|
+
syncDependencies: false,
|
|
335
|
+
dependencyDepth: 1,
|
|
336
|
+
});
|
|
337
|
+
setSelectedProfile(profileId);
|
|
338
|
+
setSettingsModalOpen(true);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
setMessage({ type: 'danger', text: err?.response?.data?.error?.message || err.message || 'Failed to load execution settings' });
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const handleSaveExecutionSettings = async () => {
|
|
345
|
+
try {
|
|
346
|
+
await put(`/${PLUGIN_ID}/sync-execution/settings/${selectedProfile}`, executionSettings);
|
|
347
|
+
setMessage({ type: 'success', text: 'Execution settings saved' });
|
|
348
|
+
setSettingsModalOpen(false);
|
|
349
|
+
loadData();
|
|
350
|
+
} catch (err) {
|
|
351
|
+
setMessage({ type: 'danger', text: err.response?.data?.error?.message || 'Failed to save settings' });
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const getProfileById = (profileId) => {
|
|
356
|
+
return profiles.find(p => p.id === profileId);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const getStatusForProfile = (profileId) => {
|
|
360
|
+
return executionStatus.find(s => s.profileId === profileId) || {};
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const formatDate = (dateStr) => {
|
|
364
|
+
if (!dateStr) return 'Never';
|
|
365
|
+
return new Date(dateStr).toLocaleString();
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Get dependency count for a profile
|
|
369
|
+
const getDependencyInfo = (profile) => {
|
|
370
|
+
const contentTypeDeps = dependencies[profile.contentType] || [];
|
|
371
|
+
const dependsOn = contentTypeDeps.length;
|
|
372
|
+
|
|
373
|
+
// Count how many profiles depend on this one
|
|
374
|
+
let dependedBy = 0;
|
|
375
|
+
profiles.forEach(p => {
|
|
376
|
+
const pDeps = dependencies[p.contentType] || [];
|
|
377
|
+
if (pDeps.some(d => d.target === profile.contentType)) {
|
|
378
|
+
dependedBy++;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return { dependsOn, dependedBy };
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Filter and sort profiles
|
|
386
|
+
const filteredProfiles = useMemo(() => {
|
|
387
|
+
let result = [...profiles];
|
|
388
|
+
|
|
389
|
+
// Apply filter
|
|
390
|
+
if (profileFilter === 'active') {
|
|
391
|
+
result = result.filter(p => p.isActive);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Sort by execution order
|
|
395
|
+
result.sort((a, b) => {
|
|
396
|
+
const orderA = executionOrder[a.id] || 999;
|
|
397
|
+
const orderB = executionOrder[b.id] || 999;
|
|
398
|
+
return orderA - orderB;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return result;
|
|
402
|
+
}, [profiles, profileFilter, executionOrder]);
|
|
403
|
+
|
|
404
|
+
const activeProfilesInFilter = filteredProfiles.filter(p => p.isActive);
|
|
405
|
+
|
|
406
|
+
if (loading) return <Typography>Loading…</Typography>;
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<Box>
|
|
410
|
+
<Tabs.Root defaultValue="execute">
|
|
411
|
+
<Tabs.List>
|
|
412
|
+
<Tabs.Trigger value="execute">Execute Sync</Tabs.Trigger>
|
|
413
|
+
<Tabs.Trigger value="status">Execution Status</Tabs.Trigger>
|
|
414
|
+
<Tabs.Trigger value="info">How It Works</Tabs.Trigger>
|
|
415
|
+
</Tabs.List>
|
|
416
|
+
|
|
417
|
+
<Box paddingTop={4}>
|
|
418
|
+
<Tabs.Content value="execute">
|
|
419
|
+
<Box>
|
|
420
|
+
<Flex justifyContent="space-between" alignItems="center">
|
|
421
|
+
<Box>
|
|
422
|
+
<Typography variant="beta" tag="h2">Execute Sync</Typography>
|
|
423
|
+
<Typography variant="omega" textColor="neutral600">
|
|
424
|
+
Run sync operations on-demand. Select profiles or sync all at once.
|
|
425
|
+
</Typography>
|
|
426
|
+
</Box>
|
|
427
|
+
<Flex gap={2}>
|
|
428
|
+
{selectedProfiles.length > 0 && (
|
|
429
|
+
<Button
|
|
430
|
+
variant="secondary"
|
|
431
|
+
onClick={handleExecuteSelected}
|
|
432
|
+
loading={syncing}
|
|
433
|
+
disabled={syncing}
|
|
434
|
+
>
|
|
435
|
+
Run {selectedProfiles.length} Selected
|
|
436
|
+
</Button>
|
|
437
|
+
)}
|
|
438
|
+
<Button onClick={handleSyncAll} loading={syncing} disabled={syncing}>
|
|
439
|
+
{syncing ? 'Syncing…' : 'Sync All Active'}
|
|
440
|
+
</Button>
|
|
441
|
+
</Flex>
|
|
442
|
+
</Flex>
|
|
443
|
+
|
|
444
|
+
{message && (
|
|
445
|
+
<Box paddingTop={4}>
|
|
446
|
+
<Alert variant={message.type} closeLabel="Close" onClose={() => setMessage(null)}>
|
|
447
|
+
{message.text}
|
|
448
|
+
</Alert>
|
|
449
|
+
</Box>
|
|
450
|
+
)}
|
|
451
|
+
|
|
452
|
+
{error && (
|
|
453
|
+
<Box paddingTop={4}>
|
|
454
|
+
<Alert variant="danger" closeLabel="Close" onClose={() => setError(null)}>
|
|
455
|
+
{error}
|
|
456
|
+
</Alert>
|
|
457
|
+
</Box>
|
|
458
|
+
)}
|
|
459
|
+
|
|
460
|
+
{result && (
|
|
461
|
+
<Box paddingTop={4}>
|
|
462
|
+
<Alert variant="success" closeLabel="Close" onClose={() => setResult(null)}>
|
|
463
|
+
Sync completed at {result.syncedAt}
|
|
464
|
+
</Alert>
|
|
465
|
+
</Box>
|
|
466
|
+
)}
|
|
467
|
+
|
|
468
|
+
<Box paddingTop={4}>
|
|
469
|
+
{/* Filter and Order Controls */}
|
|
470
|
+
<Flex justifyContent="space-between" alignItems="center" marginBottom={4}>
|
|
471
|
+
<Flex gap={4} alignItems="center">
|
|
472
|
+
<Typography variant="delta">Profiles</Typography>
|
|
473
|
+
<SingleSelect
|
|
474
|
+
value={profileFilter}
|
|
475
|
+
onChange={setProfileFilter}
|
|
476
|
+
size="S"
|
|
477
|
+
style={{ width: 150 }}
|
|
478
|
+
>
|
|
479
|
+
{FILTER_OPTIONS.map(opt => (
|
|
480
|
+
<SingleSelectOption key={opt.value} value={opt.value}>
|
|
481
|
+
{opt.label}
|
|
482
|
+
</SingleSelectOption>
|
|
483
|
+
))}
|
|
484
|
+
</SingleSelect>
|
|
485
|
+
</Flex>
|
|
486
|
+
<Flex gap={2}>
|
|
487
|
+
{orderModified && (
|
|
488
|
+
<Button variant="success" size="S" onClick={handleSaveOrder}>
|
|
489
|
+
Save Order
|
|
490
|
+
</Button>
|
|
491
|
+
)}
|
|
492
|
+
<Button
|
|
493
|
+
variant="tertiary"
|
|
494
|
+
size="S"
|
|
495
|
+
onClick={handleResetOrder}
|
|
496
|
+
>
|
|
497
|
+
↻ Reset Order
|
|
498
|
+
</Button>
|
|
499
|
+
</Flex>
|
|
500
|
+
</Flex>
|
|
501
|
+
|
|
502
|
+
{filteredProfiles.length === 0 ? (
|
|
503
|
+
<Box padding={4} background="neutral0" hasRadius>
|
|
504
|
+
<Typography textColor="neutral600">
|
|
505
|
+
{profileFilter === 'active'
|
|
506
|
+
? 'No active profiles. Activate a profile in the Sync Profiles tab first.'
|
|
507
|
+
: 'No profiles found. Create a profile in the Sync Profiles tab.'}
|
|
508
|
+
</Typography>
|
|
509
|
+
</Box>
|
|
510
|
+
) : (
|
|
511
|
+
<Table>
|
|
512
|
+
<Thead>
|
|
513
|
+
<Tr>
|
|
514
|
+
<Th style={{ width: 50 }}>
|
|
515
|
+
<Checkbox
|
|
516
|
+
checked={activeProfilesInFilter.length > 0 && activeProfilesInFilter.every(p => selectedProfiles.includes(p.id))}
|
|
517
|
+
indeterminate={activeProfilesInFilter.some(p => selectedProfiles.includes(p.id)) && !activeProfilesInFilter.every(p => selectedProfiles.includes(p.id))}
|
|
518
|
+
onCheckedChange={handleSelectAllActive}
|
|
519
|
+
aria-label="Select all active profiles"
|
|
520
|
+
/>
|
|
521
|
+
</Th>
|
|
522
|
+
<Th style={{ width: 80 }}><Typography variant="sigma">Order</Typography></Th>
|
|
523
|
+
<Th><Typography variant="sigma">Profile</Typography></Th>
|
|
524
|
+
<Th><Typography variant="sigma">Content Type</Typography></Th>
|
|
525
|
+
<Th><Typography variant="sigma">Dependencies</Typography></Th>
|
|
526
|
+
<Th><Typography variant="sigma">Status</Typography></Th>
|
|
527
|
+
<Th><Typography variant="sigma">Execution Mode</Typography></Th>
|
|
528
|
+
<Th><Typography variant="sigma">Actions</Typography></Th>
|
|
529
|
+
</Tr>
|
|
530
|
+
</Thead>
|
|
531
|
+
<Tbody>
|
|
532
|
+
{filteredProfiles.map((profile) => {
|
|
533
|
+
const status = getStatusForProfile(profile.id);
|
|
534
|
+
const isExecuting = executingProfiles.has(profile.id);
|
|
535
|
+
const depInfo = getDependencyInfo(profile);
|
|
536
|
+
const order = executionOrder[profile.id] || 999;
|
|
537
|
+
|
|
538
|
+
return (
|
|
539
|
+
<Tr key={profile.id} style={{ opacity: profile.isActive ? 1 : 0.6 }}>
|
|
540
|
+
<Td>
|
|
541
|
+
{profile.isActive ? (
|
|
542
|
+
<Checkbox
|
|
543
|
+
checked={selectedProfiles.includes(profile.id)}
|
|
544
|
+
onCheckedChange={() => handleSelectProfile(profile.id)}
|
|
545
|
+
aria-label={`Select ${profile.name}`}
|
|
546
|
+
/>
|
|
547
|
+
) : (
|
|
548
|
+
<Typography textColor="neutral400">—</Typography>
|
|
549
|
+
)}
|
|
550
|
+
</Td>
|
|
551
|
+
<Td>
|
|
552
|
+
<Flex gap={1} alignItems="center">
|
|
553
|
+
<Flex direction="column" gap={0}>
|
|
554
|
+
<IconButton
|
|
555
|
+
label="Move up"
|
|
556
|
+
size="S"
|
|
557
|
+
variant="ghost"
|
|
558
|
+
onClick={() => handleMoveUp(profile.id)}
|
|
559
|
+
disabled={order <= 1}
|
|
560
|
+
>
|
|
561
|
+
<ArrowUp />
|
|
562
|
+
</IconButton>
|
|
563
|
+
<IconButton
|
|
564
|
+
label="Move down"
|
|
565
|
+
size="S"
|
|
566
|
+
variant="ghost"
|
|
567
|
+
onClick={() => handleMoveDown(profile.id)}
|
|
568
|
+
disabled={order >= profiles.length}
|
|
569
|
+
>
|
|
570
|
+
<ArrowDown />
|
|
571
|
+
</IconButton>
|
|
572
|
+
</Flex>
|
|
573
|
+
<TextInput
|
|
574
|
+
value={order}
|
|
575
|
+
onChange={(e) => handleOrderChange(profile.id, e.target.value)}
|
|
576
|
+
style={{ width: 50, textAlign: 'center' }}
|
|
577
|
+
size="S"
|
|
578
|
+
type="number"
|
|
579
|
+
min={1}
|
|
580
|
+
/>
|
|
581
|
+
</Flex>
|
|
582
|
+
</Td>
|
|
583
|
+
<Td>
|
|
584
|
+
<Typography fontWeight="bold">{profile.name}</Typography>
|
|
585
|
+
</Td>
|
|
586
|
+
<Td>
|
|
587
|
+
<Typography textColor="neutral600" style={{ fontSize: '0.85em' }}>
|
|
588
|
+
{profile.contentType}
|
|
589
|
+
</Typography>
|
|
590
|
+
</Td>
|
|
591
|
+
<Td>
|
|
592
|
+
<Flex gap={1}>
|
|
593
|
+
{depInfo.dependedBy > 0 && (
|
|
594
|
+
<Badge active title="Other profiles depend on this">
|
|
595
|
+
↑{depInfo.dependedBy}
|
|
596
|
+
</Badge>
|
|
597
|
+
)}
|
|
598
|
+
{depInfo.dependsOn > 0 && (
|
|
599
|
+
<Badge title="This profile depends on others">
|
|
600
|
+
↓{depInfo.dependsOn}
|
|
601
|
+
</Badge>
|
|
602
|
+
)}
|
|
603
|
+
{depInfo.dependedBy === 0 && depInfo.dependsOn === 0 && (
|
|
604
|
+
<Typography textColor="neutral400">—</Typography>
|
|
605
|
+
)}
|
|
606
|
+
</Flex>
|
|
607
|
+
</Td>
|
|
608
|
+
<Td>
|
|
609
|
+
{profile.isActive ? (
|
|
610
|
+
<Badge active>Active</Badge>
|
|
611
|
+
) : (
|
|
612
|
+
<Badge>Inactive</Badge>
|
|
613
|
+
)}
|
|
614
|
+
</Td>
|
|
615
|
+
<Td>
|
|
616
|
+
<Badge active={status.executionMode !== 'on_demand'}>
|
|
617
|
+
{EXECUTION_MODE_OPTIONS.find(o => o.value === status.executionMode)?.label || 'On Demand'}
|
|
618
|
+
</Badge>
|
|
619
|
+
{status.executionMode === 'live' && status.enabled && (
|
|
620
|
+
<Badge active style={{ marginLeft: 4 }}>Live</Badge>
|
|
621
|
+
)}
|
|
622
|
+
{status.isSchedulerRunning && (
|
|
623
|
+
<Badge active style={{ marginLeft: 4 }}>Scheduled</Badge>
|
|
624
|
+
)}
|
|
625
|
+
</Td>
|
|
626
|
+
<Td>
|
|
627
|
+
<Flex gap={1}>
|
|
628
|
+
{profile.isActive ? (
|
|
629
|
+
<Button
|
|
630
|
+
variant="secondary"
|
|
631
|
+
size="S"
|
|
632
|
+
startIcon={<Play />}
|
|
633
|
+
onClick={() => handleExecuteProfile(profile.id)}
|
|
634
|
+
loading={isExecuting}
|
|
635
|
+
disabled={isExecuting}
|
|
636
|
+
>
|
|
637
|
+
{isExecuting ? 'Running…' : 'Run Now'}
|
|
638
|
+
</Button>
|
|
639
|
+
) : (
|
|
640
|
+
<Typography textColor="neutral400" variant="pi">
|
|
641
|
+
Activate to run
|
|
642
|
+
</Typography>
|
|
643
|
+
)}
|
|
644
|
+
<IconButton
|
|
645
|
+
label="Execution Settings"
|
|
646
|
+
onClick={() => openSettingsModal(profile.id)}
|
|
647
|
+
>
|
|
648
|
+
<Cog />
|
|
649
|
+
</IconButton>
|
|
650
|
+
</Flex>
|
|
651
|
+
</Td>
|
|
652
|
+
</Tr>
|
|
653
|
+
);
|
|
654
|
+
})}
|
|
655
|
+
</Tbody>
|
|
656
|
+
</Table>
|
|
657
|
+
)}
|
|
658
|
+
|
|
659
|
+
{/* Dependency Legend */}
|
|
660
|
+
<Box paddingTop={3}>
|
|
661
|
+
<Flex gap={4}>
|
|
662
|
+
<Typography variant="pi" textColor="neutral500">
|
|
663
|
+
<Badge active>↑N</Badge> = N profiles depend on this (executes earlier)
|
|
664
|
+
</Typography>
|
|
665
|
+
<Typography variant="pi" textColor="neutral500">
|
|
666
|
+
<Badge>↓N</Badge> = Depends on N other profiles (executes later)
|
|
667
|
+
</Typography>
|
|
668
|
+
</Flex>
|
|
669
|
+
</Box>
|
|
670
|
+
</Box>
|
|
671
|
+
</Box>
|
|
672
|
+
</Tabs.Content>
|
|
673
|
+
|
|
674
|
+
<Tabs.Content value="status">
|
|
675
|
+
<Box>
|
|
676
|
+
<Typography variant="beta" tag="h2">Execution Status</Typography>
|
|
677
|
+
<Typography variant="omega" textColor="neutral600">
|
|
678
|
+
Monitor scheduled and live sync jobs.
|
|
679
|
+
</Typography>
|
|
680
|
+
|
|
681
|
+
<Box paddingTop={4}>
|
|
682
|
+
<Table>
|
|
683
|
+
<Thead>
|
|
684
|
+
<Tr>
|
|
685
|
+
<Th><Typography variant="sigma">Profile</Typography></Th>
|
|
686
|
+
<Th><Typography variant="sigma">Mode</Typography></Th>
|
|
687
|
+
<Th><Typography variant="sigma">Enabled</Typography></Th>
|
|
688
|
+
<Th><Typography variant="sigma">Last Run</Typography></Th>
|
|
689
|
+
<Th><Typography variant="sigma">Next Run</Typography></Th>
|
|
690
|
+
<Th><Typography variant="sigma">Status</Typography></Th>
|
|
691
|
+
</Tr>
|
|
692
|
+
</Thead>
|
|
693
|
+
<Tbody>
|
|
694
|
+
{executionStatus.map((status) => (
|
|
695
|
+
<Tr key={status.profileId}>
|
|
696
|
+
<Td>
|
|
697
|
+
<Typography fontWeight="bold">{status.profileName}</Typography>
|
|
698
|
+
</Td>
|
|
699
|
+
<Td>
|
|
700
|
+
<Badge>
|
|
701
|
+
{EXECUTION_MODE_OPTIONS.find(o => o.value === status.executionMode)?.label || status.executionMode}
|
|
702
|
+
</Badge>
|
|
703
|
+
</Td>
|
|
704
|
+
<Td>
|
|
705
|
+
<Badge active={status.enabled}>
|
|
706
|
+
{status.enabled ? 'Enabled' : 'Disabled'}
|
|
707
|
+
</Badge>
|
|
708
|
+
</Td>
|
|
709
|
+
<Td>
|
|
710
|
+
<Typography textColor="neutral500">
|
|
711
|
+
{formatDate(status.lastExecutedAt)}
|
|
712
|
+
</Typography>
|
|
713
|
+
</Td>
|
|
714
|
+
<Td>
|
|
715
|
+
<Typography textColor="neutral500">
|
|
716
|
+
{status.executionMode === 'scheduled' && status.enabled
|
|
717
|
+
? formatDate(status.nextExecutionAt)
|
|
718
|
+
: '—'}
|
|
719
|
+
</Typography>
|
|
720
|
+
</Td>
|
|
721
|
+
<Td>
|
|
722
|
+
{status.isSchedulerRunning ? (
|
|
723
|
+
<Badge active><Clock /> Running</Badge>
|
|
724
|
+
) : status.executionMode === 'live' && status.enabled ? (
|
|
725
|
+
<Badge active>Live Active</Badge>
|
|
726
|
+
) : (
|
|
727
|
+
<Badge>Idle</Badge>
|
|
728
|
+
)}
|
|
729
|
+
</Td>
|
|
730
|
+
</Tr>
|
|
731
|
+
))}
|
|
732
|
+
</Tbody>
|
|
733
|
+
</Table>
|
|
734
|
+
</Box>
|
|
735
|
+
</Box>
|
|
736
|
+
</Tabs.Content>
|
|
737
|
+
|
|
738
|
+
{/* How It Works Tab */}
|
|
739
|
+
<Tabs.Content value="info">
|
|
740
|
+
<Box>
|
|
741
|
+
<Typography variant="beta" tag="h2">How Sync Execution Works</Typography>
|
|
742
|
+
<Typography variant="omega" textColor="neutral600" paddingBottom={4}>
|
|
743
|
+
Understanding the different execution modes and how they are triggered.
|
|
744
|
+
</Typography>
|
|
745
|
+
|
|
746
|
+
{/* On-Demand Section */}
|
|
747
|
+
<Box padding={4} background="neutral0" hasRadius marginBottom={4}>
|
|
748
|
+
<Typography variant="delta" textColor="primary600">On-Demand (Manual)</Typography>
|
|
749
|
+
<Box paddingTop={2}>
|
|
750
|
+
<Typography>
|
|
751
|
+
<strong>Trigger:</strong> User clicks "Run Now" or "Sync All Active" buttons in this tab.
|
|
752
|
+
</Typography>
|
|
753
|
+
<Typography paddingTop={1}>
|
|
754
|
+
<strong>Use Case:</strong> Testing, initial data migration, or when you want full control over when sync happens.
|
|
755
|
+
</Typography>
|
|
756
|
+
<Typography paddingTop={1}>
|
|
757
|
+
<strong>How It Works:</strong>
|
|
758
|
+
</Typography>
|
|
759
|
+
<Box as="ul" paddingLeft={4} paddingTop={1}>
|
|
760
|
+
<li>Select individual profiles using checkboxes and click "Run Selected"</li>
|
|
761
|
+
<li>Or click "Run Now" on a specific profile row</li>
|
|
762
|
+
<li>Or click "Sync All Active" to run all active profiles at once</li>
|
|
763
|
+
</Box>
|
|
764
|
+
</Box>
|
|
765
|
+
</Box>
|
|
766
|
+
|
|
767
|
+
{/* Scheduled Section */}
|
|
768
|
+
<Box padding={4} background="neutral0" hasRadius marginBottom={4}>
|
|
769
|
+
<Typography variant="delta" textColor="warning600">Scheduled (Batch)</Typography>
|
|
770
|
+
<Box paddingTop={2}>
|
|
771
|
+
<Typography>
|
|
772
|
+
<strong>Trigger:</strong> Automatic timer runs at the configured interval (e.g., every 60 minutes).
|
|
773
|
+
</Typography>
|
|
774
|
+
<Typography paddingTop={1}>
|
|
775
|
+
<strong>Use Case:</strong> Regular background synchronization without manual intervention. Best for production environments.
|
|
776
|
+
</Typography>
|
|
777
|
+
<Typography paddingTop={1}>
|
|
778
|
+
<strong>How It Works:</strong>
|
|
779
|
+
</Typography>
|
|
780
|
+
<Box as="ul" paddingLeft={4} paddingTop={1}>
|
|
781
|
+
<li>Set execution mode to "Scheduled" in profile settings (⚙️ icon)</li>
|
|
782
|
+
<li>Configure the interval (1-1440 minutes)</li>
|
|
783
|
+
<li>Enable automatic execution with the toggle</li>
|
|
784
|
+
<li>The scheduler starts automatically and runs in the background</li>
|
|
785
|
+
<li>Next execution time is displayed in the Execution Status tab</li>
|
|
786
|
+
</Box>
|
|
787
|
+
</Box>
|
|
788
|
+
</Box>
|
|
789
|
+
|
|
790
|
+
{/* Live Section */}
|
|
791
|
+
<Box padding={4} background="neutral0" hasRadius marginBottom={4}>
|
|
792
|
+
<Typography variant="delta" textColor="success600">Live (Real-time)</Typography>
|
|
793
|
+
<Box paddingTop={2}>
|
|
794
|
+
<Typography>
|
|
795
|
+
<strong>Trigger:</strong> Database lifecycle hooks (afterCreate, afterUpdate, afterDelete) on content.
|
|
796
|
+
</Typography>
|
|
797
|
+
<Typography paddingTop={1}>
|
|
798
|
+
<strong>Use Case:</strong> Instant synchronization when content changes. Best for critical data that must stay in sync immediately.
|
|
799
|
+
</Typography>
|
|
800
|
+
<Typography paddingTop={1}>
|
|
801
|
+
<strong>How It Works:</strong>
|
|
802
|
+
</Typography>
|
|
803
|
+
<Box as="ul" paddingLeft={4} paddingTop={1}>
|
|
804
|
+
<li>Set execution mode to "Live" in profile settings (⚙️ icon)</li>
|
|
805
|
+
<li>Enable automatic execution with the toggle</li>
|
|
806
|
+
<li>When any record of that content type is created/updated/deleted:</li>
|
|
807
|
+
<li style={{ marginLeft: 16 }}>→ The plugin checks if live sync is enabled for that content type</li>
|
|
808
|
+
<li style={{ marginLeft: 16 }}>→ If enabled, it immediately pushes/pulls the change to the remote instance</li>
|
|
809
|
+
<li>Changes are synced within seconds of occurring</li>
|
|
810
|
+
</Box>
|
|
811
|
+
</Box>
|
|
812
|
+
</Box>
|
|
813
|
+
|
|
814
|
+
{/* Dependencies Section */}
|
|
815
|
+
<Box padding={4} background="neutral100" hasRadius>
|
|
816
|
+
<Typography variant="delta">Dependency Syncing</Typography>
|
|
817
|
+
<Box paddingTop={2}>
|
|
818
|
+
<Typography>
|
|
819
|
+
When enabled in profile settings, the sync will also include related entities:
|
|
820
|
+
</Typography>
|
|
821
|
+
<Box as="ul" paddingLeft={4} paddingTop={1}>
|
|
822
|
+
<li><strong>Relations:</strong> Linked content from other content types</li>
|
|
823
|
+
<li><strong>Components:</strong> Embedded component data</li>
|
|
824
|
+
<li><strong>Dynamic Zones:</strong> Multi-type component areas</li>
|
|
825
|
+
</Box>
|
|
826
|
+
<Typography paddingTop={2} textColor="neutral500">
|
|
827
|
+
Dependency depth (1-5) controls how many levels of nested relations are followed.
|
|
828
|
+
</Typography>
|
|
829
|
+
</Box>
|
|
830
|
+
</Box>
|
|
831
|
+
</Box>
|
|
832
|
+
</Tabs.Content>
|
|
833
|
+
</Box>
|
|
834
|
+
</Tabs.Root>
|
|
835
|
+
|
|
836
|
+
{/* Execution Settings Modal */}
|
|
837
|
+
{settingsModalOpen && (
|
|
838
|
+
<Modal.Root open={settingsModalOpen} onOpenChange={setSettingsModalOpen}>
|
|
839
|
+
<Modal.Content>
|
|
840
|
+
<Modal.Header>
|
|
841
|
+
<Modal.Title>Execution Settings</Modal.Title>
|
|
842
|
+
</Modal.Header>
|
|
843
|
+
<Modal.Body>
|
|
844
|
+
<Box paddingBottom={4}>
|
|
845
|
+
<Field.Root>
|
|
846
|
+
<Field.Label>Execution Mode</Field.Label>
|
|
847
|
+
<SingleSelect
|
|
848
|
+
value={executionSettings.executionMode}
|
|
849
|
+
onChange={(value) => setExecutionSettings((p) => ({ ...p, executionMode: value }))}
|
|
850
|
+
>
|
|
851
|
+
{EXECUTION_MODE_OPTIONS.map((opt) => (
|
|
852
|
+
<SingleSelectOption key={opt.value} value={opt.value}>
|
|
853
|
+
{opt.label}
|
|
854
|
+
</SingleSelectOption>
|
|
855
|
+
))}
|
|
856
|
+
</SingleSelect>
|
|
857
|
+
<Field.Hint>
|
|
858
|
+
{executionSettings.executionMode === 'on_demand' && 'Sync only when manually triggered'}
|
|
859
|
+
{executionSettings.executionMode === 'scheduled' && 'Sync automatically at regular intervals'}
|
|
860
|
+
{executionSettings.executionMode === 'live' && 'Sync immediately when changes occur'}
|
|
861
|
+
</Field.Hint>
|
|
862
|
+
</Field.Root>
|
|
863
|
+
</Box>
|
|
864
|
+
|
|
865
|
+
{executionSettings.executionMode === 'scheduled' && (
|
|
866
|
+
<Box paddingBottom={4}>
|
|
867
|
+
<Field.Root>
|
|
868
|
+
<Field.Label>Schedule Type</Field.Label>
|
|
869
|
+
<SingleSelect
|
|
870
|
+
value={executionSettings.scheduleType || 'interval'}
|
|
871
|
+
onChange={(value) => setExecutionSettings((p) => ({ ...p, scheduleType: value }))}
|
|
872
|
+
>
|
|
873
|
+
<SingleSelectOption value="interval">Interval (setInterval)</SingleSelectOption>
|
|
874
|
+
<SingleSelectOption value="timeout">Timeout (chained, no overlap)</SingleSelectOption>
|
|
875
|
+
<SingleSelectOption value="cron">Cron (wall-clock)</SingleSelectOption>
|
|
876
|
+
<SingleSelectOption value="external">External scheduler</SingleSelectOption>
|
|
877
|
+
</SingleSelect>
|
|
878
|
+
<Field.Hint>
|
|
879
|
+
{(!executionSettings.scheduleType || executionSettings.scheduleType === 'interval') && 'Fires every N minutes via setInterval. Light and simple; may overlap if a run is slow.'}
|
|
880
|
+
{executionSettings.scheduleType === 'timeout' && 'Chained setTimeout: waits for each run to finish before scheduling the next. Best for long-running syncs.'}
|
|
881
|
+
{executionSettings.scheduleType === 'cron' && 'Uses Strapi\'s built-in cron. Recommended for production and larger datasets.'}
|
|
882
|
+
{executionSettings.scheduleType === 'external' && 'No in-process timer. Trigger the execute endpoint from an external scheduler (cron, Task Scheduler, K8s CronJob, etc.). See the Help tab.'}
|
|
883
|
+
</Field.Hint>
|
|
884
|
+
</Field.Root>
|
|
885
|
+
</Box>
|
|
886
|
+
)}
|
|
887
|
+
|
|
888
|
+
{executionSettings.executionMode === 'scheduled' &&
|
|
889
|
+
(executionSettings.scheduleType === 'interval' ||
|
|
890
|
+
executionSettings.scheduleType === 'timeout' ||
|
|
891
|
+
!executionSettings.scheduleType) && (
|
|
892
|
+
<Box paddingBottom={4}>
|
|
893
|
+
<Field.Root>
|
|
894
|
+
<Field.Label>Schedule Interval (minutes)</Field.Label>
|
|
895
|
+
<NumberInput
|
|
896
|
+
value={executionSettings.scheduleInterval}
|
|
897
|
+
onValueChange={(value) => setExecutionSettings((p) => ({ ...p, scheduleInterval: value }))}
|
|
898
|
+
min={1}
|
|
899
|
+
max={1440}
|
|
900
|
+
/>
|
|
901
|
+
<Field.Hint>How often to run the sync (1-1440 minutes)</Field.Hint>
|
|
902
|
+
</Field.Root>
|
|
903
|
+
</Box>
|
|
904
|
+
)}
|
|
905
|
+
|
|
906
|
+
{executionSettings.executionMode === 'scheduled' && executionSettings.scheduleType === 'cron' && (
|
|
907
|
+
<Box paddingBottom={4}>
|
|
908
|
+
<Field.Root>
|
|
909
|
+
<Field.Label>Cron Expression</Field.Label>
|
|
910
|
+
<TextInput
|
|
911
|
+
value={executionSettings.cronExpression || ''}
|
|
912
|
+
onChange={(e) => setExecutionSettings((p) => ({ ...p, cronExpression: e.target.value }))}
|
|
913
|
+
placeholder="0 */2 * * *"
|
|
914
|
+
/>
|
|
915
|
+
<Field.Hint>
|
|
916
|
+
Standard 5- or 6-field cron. Examples: "0 * * * *" (hourly), "*/15 * * * *" (every 15 min), "0 2 * * *" (daily at 02:00).
|
|
917
|
+
</Field.Hint>
|
|
918
|
+
</Field.Root>
|
|
919
|
+
</Box>
|
|
920
|
+
)}
|
|
921
|
+
|
|
922
|
+
{executionSettings.executionMode === 'scheduled' && executionSettings.scheduleType === 'external' && (
|
|
923
|
+
<Box paddingBottom={4}>
|
|
924
|
+
<Typography variant="pi" textColor="neutral600">
|
|
925
|
+
External mode: the plugin will NOT run an in-process timer. Your external scheduler must POST to
|
|
926
|
+
{' '}<code>/api/strapi-content-sync-pro/sync-execution/execute/<profileId></code>{' '}
|
|
927
|
+
with a valid API token. See the Help tab for concrete examples (cron, Windows Task Scheduler, systemd, Kubernetes CronJob, GitHub Actions).
|
|
928
|
+
</Typography>
|
|
929
|
+
</Box>
|
|
930
|
+
)}
|
|
931
|
+
|
|
932
|
+
{(executionSettings.executionMode === 'scheduled' || executionSettings.executionMode === 'live') && (
|
|
933
|
+
<Box paddingBottom={4}>
|
|
934
|
+
<Flex alignItems="center" gap={2}>
|
|
935
|
+
<Switch
|
|
936
|
+
checked={executionSettings.enabled}
|
|
937
|
+
onCheckedChange={(checked) => setExecutionSettings((p) => ({ ...p, enabled: checked }))}
|
|
938
|
+
/>
|
|
939
|
+
<Typography>Enable automatic execution</Typography>
|
|
940
|
+
</Flex>
|
|
941
|
+
</Box>
|
|
942
|
+
)}
|
|
943
|
+
|
|
944
|
+
<Box paddingBottom={4}>
|
|
945
|
+
<Flex alignItems="center" gap={2}>
|
|
946
|
+
<Switch
|
|
947
|
+
checked={executionSettings.syncDependencies}
|
|
948
|
+
onCheckedChange={(checked) => setExecutionSettings((p) => ({ ...p, syncDependencies: checked }))}
|
|
949
|
+
/>
|
|
950
|
+
<Typography>Sync related dependencies</Typography>
|
|
951
|
+
</Flex>
|
|
952
|
+
<Box paddingTop={1}>
|
|
953
|
+
<Typography variant="pi" textColor="neutral500">
|
|
954
|
+
Also sync related entities (relations, components) when syncing this content type.
|
|
955
|
+
</Typography>
|
|
956
|
+
</Box>
|
|
957
|
+
</Box>
|
|
958
|
+
|
|
959
|
+
{executionSettings.syncDependencies && (
|
|
960
|
+
<Box paddingBottom={4}>
|
|
961
|
+
<Field.Root>
|
|
962
|
+
<Field.Label>Dependency Depth</Field.Label>
|
|
963
|
+
<NumberInput
|
|
964
|
+
value={executionSettings.dependencyDepth}
|
|
965
|
+
onValueChange={(value) => setExecutionSettings((p) => ({ ...p, dependencyDepth: value }))}
|
|
966
|
+
min={1}
|
|
967
|
+
max={5}
|
|
968
|
+
/>
|
|
969
|
+
<Field.Hint>How many levels of relations to follow (1-5)</Field.Hint>
|
|
970
|
+
</Field.Root>
|
|
971
|
+
</Box>
|
|
972
|
+
)}
|
|
973
|
+
</Modal.Body>
|
|
974
|
+
<Modal.Footer>
|
|
975
|
+
<Modal.Close>
|
|
976
|
+
<Button variant="tertiary">Cancel</Button>
|
|
977
|
+
</Modal.Close>
|
|
978
|
+
<Button onClick={handleSaveExecutionSettings}>Save Settings</Button>
|
|
979
|
+
</Modal.Footer>
|
|
980
|
+
</Modal.Content>
|
|
981
|
+
</Modal.Root>
|
|
982
|
+
)}
|
|
983
|
+
</Box>
|
|
984
|
+
);
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
export { SyncTab };
|
|
988
|
+
export default SyncTab;
|