data-primals-engine 1.7.1 → 1.7.3
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/README.md +9 -9
- package/client/package-lock.json +8430 -8121
- package/client/package.json +7 -4
- package/client/src/APIInfo.jsx +1 -1
- package/client/src/App.jsx +2 -1
- package/client/src/App.scss +1635 -1626
- package/client/src/AssistantChat.jsx +2 -3
- package/client/src/CalendarView.jsx +1 -0
- package/client/src/ContentView.jsx +3 -3
- package/client/src/Dashboard.jsx +5 -2
- package/client/src/DashboardChart.jsx +1 -0
- package/client/src/DashboardFlexViewItem.jsx +1 -0
- package/client/src/DashboardHtmlViewItem.jsx +1 -0
- package/client/src/DashboardView.jsx +2 -0
- package/client/src/DataEditor.jsx +0 -1
- package/client/src/DataImporter.jsx +489 -468
- package/client/src/DataLayout.jsx +25 -23
- package/client/src/DataTable.jsx +6 -5
- package/client/src/Dialog.jsx +92 -90
- package/client/src/Dialog.scss +122 -116
- package/client/src/DisplayFlexNodeRenderer.jsx +1 -0
- package/client/src/DocumentationPageLayout.scss +1 -1
- package/client/src/FlexBuilderControls.jsx +1 -0
- package/client/src/FlexBuilderPreview.jsx +1 -1
- package/client/src/FlexNode.jsx +1 -1
- package/client/src/HistoryDialog.jsx +3 -2
- package/client/src/KPIWidget.jsx +1 -1
- package/client/src/KanbanView.jsx +1 -0
- package/client/src/ModelCreator.jsx +4 -0
- package/client/src/ModelList.jsx +1 -0
- package/client/src/PackGallery.jsx +5 -4
- package/client/src/RTETrans.jsx +1 -1
- package/client/src/RelationField.jsx +2 -0
- package/client/src/RelationSelectorWidget.jsx +2 -0
- package/client/src/RelationValue.jsx +1 -0
- package/client/src/RestoreDialog.jsx +1 -0
- package/client/src/ViewSwitcher.jsx +1 -1
- package/client/src/WorkflowEditor.jsx +3 -0
- package/client/src/contexts/CommandContext.jsx +2 -1
- package/client/src/contexts/ModelContext.jsx +3 -3
- package/client/src/hooks/data.js +1 -0
- package/client/src/hooks/useValidation.js +1 -0
- package/client/src/translations.js +24 -24
- package/client/vite.config.js +31 -30
- package/doc/AI-assistance.md +87 -63
- package/doc/Concepts.md +122 -0
- package/doc/Custom-Endpoints.md +31 -0
- package/doc/Event-system.md +13 -14
- package/doc/Home.md +33 -0
- package/doc/Modules.md +83 -0
- package/doc/Packs-gallery.md +8 -23
- package/doc/Workflows.md +32 -0
- package/doc/automation-workflows.md +141 -102
- package/doc/dashboards-kpis-charts.md +47 -49
- package/doc/data-management.md +126 -120
- package/doc/data-models.md +68 -75
- package/doc/roles-permissions.md +144 -43
- package/doc/sharding-replication.md +158 -0
- package/doc/users.md +54 -30
- package/package.json +27 -17
- package/server.js +37 -37
- package/src/ai.jobs.js +135 -0
- package/src/constants.js +560 -545
- package/src/data.js +521 -518
- package/src/email.js +157 -154
- package/src/engine.js +167 -49
- package/src/gameObject.js +6 -0
- package/src/i18n.js +0 -1
- package/src/modules/assistant/assistant.js +782 -763
- package/src/modules/assistant/constants.js +23 -16
- package/src/modules/assistant/providers.js +77 -37
- package/src/modules/auth-google/index.js +53 -50
- package/src/modules/bucket.js +346 -335
- package/src/modules/data/data.backup.js +400 -376
- package/src/modules/data/data.cluster.js +559 -0
- package/src/modules/data/data.core.js +11 -8
- package/src/modules/data/data.js +311 -311
- package/src/modules/data/data.operations.js +3666 -3555
- package/src/modules/data/data.relations.js +1 -0
- package/src/modules/data/data.replication.js +125 -0
- package/src/modules/data/data.routes.js +2206 -1879
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/file.js +248 -247
- package/src/modules/mongodb.js +76 -73
- package/src/modules/user.js +18 -2
- package/src/modules/worker-script-runner.js +97 -0
- package/src/modules/workflow.js +177 -52
- package/src/packs.js +5701 -5701
- package/src/providers.js +298 -297
- package/src/sso.js +91 -25
- package/test/assistant.test.js +207 -206
- package/test/cluster.test.js +221 -0
- package/test/core.test.js +0 -2
- package/test/data.integration.test.js +1425 -1416
- package/test/import_export.integration.test.js +210 -210
- package/test/replication.test.js +163 -0
- package/test/workflow.actions.integration.test.js +487 -475
- package/test/workflow.integration.test.js +332 -329
- package/doc/core-concepts.md +0 -33
|
@@ -1,469 +1,490 @@
|
|
|
1
|
-
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
2
|
-
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
3
|
-
import {useNotificationContext} from "./NotificationProvider.jsx";
|
|
4
|
-
import {useEffect, useRef, useState} from "react";
|
|
5
|
-
import {Trans, useTranslation} from "react-i18next";
|
|
6
|
-
import {useMutation, useQueryClient} from "react-query";
|
|
7
|
-
import useLocalStorage from "./hooks/useLocalStorage.js";
|
|
8
|
-
import {getUserId} from "../../src/data.js";
|
|
9
|
-
import {kilobytes, maxBytesPerSecondThrottleData, maxFileSize} from "../../src/constants";
|
|
10
|
-
import {FileField, ModelField} from "./Field.jsx";
|
|
11
|
-
import Button from "./Button.jsx";
|
|
12
|
-
import {FaInfo, FaTrash} from "react-icons/fa";
|
|
13
|
-
import {Dialog} from "./Dialog.jsx";
|
|
14
|
-
import readXlsxFile from 'read-excel-file'
|
|
15
|
-
// Ajoutez cette constante pour la clé de sessionStorage
|
|
16
|
-
const SESSION_STORAGE_IMPORT_JOBS_KEY = 'activeImportJobs';
|
|
17
|
-
|
|
18
|
-
export function DataImporter({onClose}) {
|
|
19
|
-
const [previewData, setPreviewData] = useState(null);
|
|
20
|
-
const [file, setFile] = useState(null);
|
|
21
|
-
const {selectedModel, page} = useModelContext();
|
|
22
|
-
|
|
23
|
-
const isCsvFile = file && (file.name.endsWith('.csv') || file.type === 'text/csv');
|
|
24
|
-
const isExcelFile = file && ((file.name.endsWith('.xlsx') ||
|
|
25
|
-
['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
26
|
-
'application/vnd.ms-excel'].includes(file.type)));
|
|
27
|
-
|
|
28
|
-
const {me} = useAuthContext();
|
|
29
|
-
const {t, i18n} = useTranslation();
|
|
30
|
-
|
|
31
|
-
const queryClient = useQueryClient();
|
|
32
|
-
const {addNotification} = useNotificationContext();
|
|
33
|
-
|
|
34
|
-
const [hasHeaders, setHasHeaders] = useState(true);
|
|
35
|
-
const [csvHeaders, setCSVHeaders] = useState(selectedModel.fields.map(field => field.name));
|
|
36
|
-
|
|
37
|
-
// --- MODIFIÉ : État pour gérer plusieurs tâches d'importation ---
|
|
38
|
-
// Cet objet stockera les données de progression de chaque tâche, indexées par leur jobId
|
|
39
|
-
const [importJobs, setImportJobs] = useState({});
|
|
40
|
-
// Cette liste stockera les IDs des tâches qui sont actuellement suivies via SSE
|
|
41
|
-
const [activeJobIds, setActiveJobIds] = useState([]);
|
|
42
|
-
|
|
43
|
-
// Référence pour stocker les instances EventSource, indexées par jobId
|
|
44
|
-
const eventSourceRefs = useRef({});
|
|
45
|
-
|
|
46
|
-
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
47
|
-
|
|
48
|
-
const [storedJobIds, setStoredJobIds] = useLocalStorage(SESSION_STORAGE_IMPORT_JOBS_KEY, []);
|
|
49
|
-
// --- NOUVEAU : Charger/Sauvegarder les IDs des tâches actives depuis sessionStorage ---
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
>
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
<
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1
|
+
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
2
|
+
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
3
|
+
import {useNotificationContext} from "./NotificationProvider.jsx";
|
|
4
|
+
import {useEffect, useRef, useState} from "react";
|
|
5
|
+
import {Trans, useTranslation} from "react-i18next";
|
|
6
|
+
import {useMutation, useQueryClient} from "react-query";
|
|
7
|
+
import useLocalStorage from "./hooks/useLocalStorage.js";
|
|
8
|
+
import {getUserId} from "../../src/data.js";
|
|
9
|
+
import {kilobytes, maxBytesPerSecondThrottleData, maxFileSize} from "../../src/constants";
|
|
10
|
+
import {FileField, ModelField} from "./Field.jsx";
|
|
11
|
+
import Button from "./Button.jsx";
|
|
12
|
+
import {FaInfo, FaTrash} from "react-icons/fa";
|
|
13
|
+
import {Dialog, DialogProvider} from "./Dialog.jsx";
|
|
14
|
+
import readXlsxFile from 'read-excel-file'
|
|
15
|
+
// Ajoutez cette constante pour la clé de sessionStorage
|
|
16
|
+
const SESSION_STORAGE_IMPORT_JOBS_KEY = 'activeImportJobs';
|
|
17
|
+
|
|
18
|
+
export function DataImporter({onClose}) {
|
|
19
|
+
const [previewData, setPreviewData] = useState(null);
|
|
20
|
+
const [file, setFile] = useState(null);
|
|
21
|
+
const {selectedModel, page} = useModelContext();
|
|
22
|
+
|
|
23
|
+
const isCsvFile = file && (file.name.endsWith('.csv') || file.type === 'text/csv');
|
|
24
|
+
const isExcelFile = file && ((file.name.endsWith('.xlsx') ||
|
|
25
|
+
['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
26
|
+
'application/vnd.ms-excel'].includes(file.type)));
|
|
27
|
+
|
|
28
|
+
const {me} = useAuthContext();
|
|
29
|
+
const {t, i18n} = useTranslation();
|
|
30
|
+
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
const {addNotification} = useNotificationContext();
|
|
33
|
+
|
|
34
|
+
const [hasHeaders, setHasHeaders] = useState(true);
|
|
35
|
+
const [csvHeaders, setCSVHeaders] = useState(selectedModel.fields.map(field => field.name));
|
|
36
|
+
|
|
37
|
+
// --- MODIFIÉ : État pour gérer plusieurs tâches d'importation ---
|
|
38
|
+
// Cet objet stockera les données de progression de chaque tâche, indexées par leur jobId
|
|
39
|
+
const [importJobs, setImportJobs] = useState({});
|
|
40
|
+
// Cette liste stockera les IDs des tâches qui sont actuellement suivies via SSE
|
|
41
|
+
const [activeJobIds, setActiveJobIds] = useState([]);
|
|
42
|
+
|
|
43
|
+
// Référence pour stocker les instances EventSource, indexées par jobId
|
|
44
|
+
const eventSourceRefs = useRef({});
|
|
45
|
+
|
|
46
|
+
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
47
|
+
|
|
48
|
+
const [storedJobIds, setStoredJobIds] = useLocalStorage(SESSION_STORAGE_IMPORT_JOBS_KEY, []);
|
|
49
|
+
// --- NOUVEAU : Charger/Sauvegarder les IDs des tâches actives depuis sessionStorage ---
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const validJobIds = [];
|
|
52
|
+
const checkJobsPromises = storedJobIds.map(jobId =>
|
|
53
|
+
fetch(`/api/import/progress/${jobId}`)
|
|
54
|
+
.then(res => res.json())
|
|
55
|
+
.then(jobData => {
|
|
56
|
+
if (jobData.status !== 'not_found') {
|
|
57
|
+
validJobIds.push(jobId);
|
|
58
|
+
// Mettre à jour l'état initial avec les données fraîches
|
|
59
|
+
setImportJobs(prevJobs => ({
|
|
60
|
+
...prevJobs,
|
|
61
|
+
[jobId]: { ...jobData, jobId: jobId } // Assurer que jobId est dans l'objet
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
.catch(() => {
|
|
66
|
+
// Ignorer les erreurs de réseau, le job sera simplement retiré
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
Promise.all(checkJobsPromises).then(() => {
|
|
71
|
+
setActiveJobIds(validJobIds);
|
|
72
|
+
// Démarrer le suivi SSE uniquement pour les tâches valides
|
|
73
|
+
validJobIds.forEach(jobId => startProgressTracking(jobId));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Fonction de nettoyage : fermer toutes les connexions EventSource lors du démontage du composant
|
|
77
|
+
return () => {
|
|
78
|
+
Object.values(eventSourceRefs.current).forEach(es => es.close());
|
|
79
|
+
eventSourceRefs.current = {}; // Effacer les références
|
|
80
|
+
};
|
|
81
|
+
}, []); // S'exécute une seule fois au montage
|
|
82
|
+
|
|
83
|
+
// --- NOUVEAU : Effet pour mettre à jour sessionStorage lorsque activeJobIds change ---
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
setStoredJobIds(activeJobIds);
|
|
86
|
+
}, [activeJobIds]);
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
// Mutation pour initier l'importation (envoi du fichier au serveur)
|
|
90
|
+
const {isLoading, mutate: importMutation} = useMutation(async () => {
|
|
91
|
+
console.log('Initiating data import...');
|
|
92
|
+
const params = new FormData();
|
|
93
|
+
params.append('model', selectedModel?.name);
|
|
94
|
+
params.append("_user", getUserId(me));
|
|
95
|
+
params.append("hasHeaders", !!hasHeaders);
|
|
96
|
+
params.append("csvHeaders", csvHeaders.join(','));
|
|
97
|
+
if (file) {
|
|
98
|
+
params.append("file", file);
|
|
99
|
+
} else {
|
|
100
|
+
addNotification({ title: t('dataimporter.noFileSelected', 'Veuillez sélectionner un fichier à importer.'), status: 'warning' });
|
|
101
|
+
return Promise.reject(new Error("No file selected"));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(`/api/data/import?lang=${lang}`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
credentials: "include",
|
|
108
|
+
body: params
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (response.status === 200) {
|
|
112
|
+
const { jobId } = await response.json();
|
|
113
|
+
|
|
114
|
+
// --- MODIFIÉ : Ajouter le nouvel jobId à activeJobIds et à l'état importJobs ---
|
|
115
|
+
setActiveJobIds(prevIds => [...prevIds, jobId]);
|
|
116
|
+
setImportJobs(prevJobs => ({
|
|
117
|
+
...prevJobs,
|
|
118
|
+
[jobId]: {
|
|
119
|
+
jobId,
|
|
120
|
+
status: 'pending',
|
|
121
|
+
totalRecords: 0,
|
|
122
|
+
processedRecords: 0,
|
|
123
|
+
errors: [],
|
|
124
|
+
// Ajoutez d'autres champs initiaux que vous souhaitez afficher immédiatement
|
|
125
|
+
}
|
|
126
|
+
}));
|
|
127
|
+
startProgressTracking(jobId); // Commencer le suivi de cette nouvelle tâche
|
|
128
|
+
addNotification({
|
|
129
|
+
title: t('dataimporter.initiated', 'Importation initiée. Suivi de la progression...'),
|
|
130
|
+
icon: <FaInfo/>,
|
|
131
|
+
status: 'info'
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
const errorData = await response.json();
|
|
135
|
+
addNotification({
|
|
136
|
+
title: errorData.error || t('dataimporter.error', 'Erreur lors de l\'importation.'),
|
|
137
|
+
status: 'error'
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {
|
|
141
|
+
addNotification({
|
|
142
|
+
title: e.message || t('dataimporter.networkError', 'Erreur réseau lors de l\'importation.'),
|
|
143
|
+
status: 'error'
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Fonction pour démarrer le suivi de la progression via Server-Sent Events (SSE) pour un jobId spécifique
|
|
149
|
+
|
|
150
|
+
// Fonction pour démarrer le suivi de la progression via Server-Sent Events (SSE) pour un jobId spécifique
|
|
151
|
+
const startProgressTracking = (jobId) => {
|
|
152
|
+
// Fermer toute connexion EventSource existante pour ce jobId pour éviter les doublons
|
|
153
|
+
if (eventSourceRefs.current[jobId]) {
|
|
154
|
+
eventSourceRefs.current[jobId].close();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const eventSource = new EventSource(`/api/import/progress/${jobId}`);
|
|
158
|
+
eventSourceRefs.current[jobId] = eventSource; // Stocker l'instance pour le nettoyage
|
|
159
|
+
|
|
160
|
+
eventSource.onmessage = (event) => {
|
|
161
|
+
const data = JSON.parse(event.data);
|
|
162
|
+
// --- MODIFIÉ : Mettre à jour la progression de la tâche spécifique ---
|
|
163
|
+
setImportJobs(prevJobs => ({
|
|
164
|
+
...prevJobs,
|
|
165
|
+
[jobId]: {
|
|
166
|
+
...data,
|
|
167
|
+
jobId: jobId // Assurer que le jobId est toujours présent dans l'objet de la tâche
|
|
168
|
+
}
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
// Si la tâche est terminée (succès ou échec), fermer la connexion SSE.
|
|
172
|
+
// NE PAS la retirer de activeJobIds ici, pour qu'elle persiste au rafraîchissement.
|
|
173
|
+
// Elle sera retirar le bouton "Effacer".
|
|
174
|
+
if (data.status === 'completed' || data.status === 'failed' || data.status === 'not_found') {
|
|
175
|
+
queryClient.invalidateQueries(['api/data', selectedModel.name, 'page', page]); // Rafraîchir les données du tableau
|
|
176
|
+
queryClient.invalidateQueries(['api/data', selectedModel.name]); // Invalider toutes les requêtes pour ce modèle
|
|
177
|
+
|
|
178
|
+
if (data.status === 'completed') {
|
|
179
|
+
addNotification({
|
|
180
|
+
title: t('dataimporter.success', 'Importation des données réussie.'),
|
|
181
|
+
icon: <FaInfo/>,
|
|
182
|
+
status: 'completed'
|
|
183
|
+
});
|
|
184
|
+
} else if (data.status === 'failed') {
|
|
185
|
+
addNotification({
|
|
186
|
+
title: t('dataimporter.failed', 'Importation échouée. Voir les détails pour les erreurs.'),
|
|
187
|
+
status: 'error'
|
|
188
|
+
});
|
|
189
|
+
} else if (data.status === 'not_found') {
|
|
190
|
+
addNotification({
|
|
191
|
+
title: t('dataimporter.jobNotFound', 'Tâche d\'importation non trouvée ou déjà terminée.'),
|
|
192
|
+
status: 'warning'
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// On ferme la connexion SSE et on retire la référence
|
|
196
|
+
eventSource.close();
|
|
197
|
+
delete eventSourceRefs.current[jobId];
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
eventSource.onerror = (error) => {
|
|
202
|
+
console.error(`EventSource error for job ${jobId}:`, error);
|
|
203
|
+
eventSource.close();
|
|
204
|
+
delete eventSourceRefs.current[jobId];
|
|
205
|
+
setStoredJobIds(prevIds => prevIds.filter(id => id !== jobId));
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const handleImportClick = () => {
|
|
210
|
+
importMutation();
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const handleCloseModal = () => {
|
|
214
|
+
// Fermer toutes les connexions EventSource avant de fermer la modale
|
|
215
|
+
Object.values(eventSourceRefs.current).forEach(es => es.close());
|
|
216
|
+
eventSourceRefs.current = {}; // Effacer les références
|
|
217
|
+
onClose();
|
|
218
|
+
};
|
|
219
|
+
const handleFilePreview = async (file) => {
|
|
220
|
+
console.log('handleFilePreview');
|
|
221
|
+
if (!file) {
|
|
222
|
+
setPreviewData(null);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const isExcelFile = file && ((file.name.endsWith('.xlsx') ||
|
|
227
|
+
['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
228
|
+
'application/vnd.ms-excel'].includes(file.type)));
|
|
229
|
+
|
|
230
|
+
if (isExcelFile) {
|
|
231
|
+
console.log('excel');
|
|
232
|
+
try {
|
|
233
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
234
|
+
const rows = await readXlsxFile(arrayBuffer);
|
|
235
|
+
setPreviewData(rows);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error('Error reading Excel file:', error);
|
|
238
|
+
addNotification({
|
|
239
|
+
title: t('dataimporter.excelReadError', 'Erreur lors de la lecture du fichier Excel'),
|
|
240
|
+
status: 'error'
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
setPreviewData(null);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Déterminer si une importation est actuellement en cours (pour désactiver les boutons)
|
|
249
|
+
const isAnyImportInProgress = Object.values(importJobs).some(job => job.status === 'pending' || job.status === 'processing');
|
|
250
|
+
|
|
251
|
+
// Filtrer et trier les tâches à afficher (par exemple, les tâches en cours en premier)
|
|
252
|
+
const jobsToDisplay = Object.values(importJobs).sort((a, b) => {
|
|
253
|
+
// Trier par statut (en attente/en cours en premier, puis échoué, puis terminé)
|
|
254
|
+
const statusOrder = { 'pending': 1, 'processing': 2, 'failed': 3, 'completed': 4, 'not_found': 5 };
|
|
255
|
+
return statusOrder[a.status] - statusOrder[b.status];
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const { models } = useModelContext();
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<Dialog isClosable={true} isModal={true} onClose={handleCloseModal}>
|
|
262
|
+
<>
|
|
263
|
+
<h2>
|
|
264
|
+
<Trans i18nKey="dataimporter.title" values={{model: t('model_' + selectedModel?.name, selectedModel?.name)}}>
|
|
265
|
+
Importer des données dans {t('model_' + selectedModel?.name, selectedModel?.name)}
|
|
266
|
+
</Trans>
|
|
267
|
+
</h2>
|
|
268
|
+
<p className="msg msg-info">
|
|
269
|
+
<Trans i18nKey="dataimporter.info" values={{constante: (maxBytesPerSecondThrottleData / kilobytes) + 'ko/s'}}></Trans>
|
|
270
|
+
</p>
|
|
271
|
+
|
|
272
|
+
{/* Toujours afficher le formulaire de sélection de fichier et le bouton d'importation */}
|
|
273
|
+
<FileField
|
|
274
|
+
name="file"
|
|
275
|
+
maxSize={maxFileSize}
|
|
276
|
+
mimeTypes={[
|
|
277
|
+
'application/json',
|
|
278
|
+
'text/csv',
|
|
279
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
280
|
+
'application/vnd.ms-excel'
|
|
281
|
+
]}
|
|
282
|
+
type="file"
|
|
283
|
+
multiple={false}
|
|
284
|
+
onChange={async (files) => {
|
|
285
|
+
const newFile = files && files.length > 0 ? files[files.length - 1].file : null;
|
|
286
|
+
setFile(newFile);
|
|
287
|
+
await handleFilePreview(newFile);
|
|
288
|
+
}}
|
|
289
|
+
/>
|
|
290
|
+
|
|
291
|
+
{file && (isExcelFile || isCsvFile) && (
|
|
292
|
+
<div className="checkbox-label flex flex-row">
|
|
293
|
+
{isCsvFile && (<label htmlFor="hasHeadersCheckbox">
|
|
294
|
+
<input
|
|
295
|
+
type="checkbox"
|
|
296
|
+
id="hasHeadersCheckbox"
|
|
297
|
+
checked={hasHeaders}
|
|
298
|
+
onChange={(e) => setHasHeaders(e.target.checked)}
|
|
299
|
+
/>
|
|
300
|
+
<Trans i18nKey="dataimporter.hasCsvHeaders"></Trans>
|
|
301
|
+
</label>)}
|
|
302
|
+
{(!hasHeaders || isExcelFile) && (
|
|
303
|
+
<table>
|
|
304
|
+
<thead>
|
|
305
|
+
<tr>
|
|
306
|
+
<th><Trans i18nKey={"dataimporter.columnType"} values={[isExcelFile?'Excel':'CSV']}>Numéro de colonne</Trans></th>
|
|
307
|
+
<th><Trans i18nKey="dataimporter.field">Champ du modèle</Trans></th>
|
|
308
|
+
</tr>
|
|
309
|
+
</thead>
|
|
310
|
+
<tbody>
|
|
311
|
+
{selectedModel?.fields.map((field, index) => {
|
|
312
|
+
const currentFieldValue = csvHeaders[index] || '';
|
|
313
|
+
const fieldObject = selectedModel.fields.find(f => f.name === currentFieldValue);
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<tr key={`${selectedModel.name}-csvmap-${index}`}>
|
|
317
|
+
<td><Trans i18nKey="dataimporter.column" values={[index + 1]}>colonne {index + 1}</Trans></td>
|
|
318
|
+
<td>
|
|
319
|
+
<div className="flex">
|
|
320
|
+
<ModelField
|
|
321
|
+
disableable={true}
|
|
322
|
+
showModel={false}
|
|
323
|
+
value={selectedModel.name}
|
|
324
|
+
fieldValue={currentFieldValue}
|
|
325
|
+
onChange={({name: propName, value: selectedValue}) => {
|
|
326
|
+
const newCsvHeaders = [...csvHeaders];
|
|
327
|
+
newCsvHeaders[index] = selectedValue?.field ?? '';
|
|
328
|
+
setCSVHeaders(newCsvHeaders);
|
|
329
|
+
}}
|
|
330
|
+
fields={true}
|
|
331
|
+
model={selectedModel}
|
|
332
|
+
field={fieldObject}
|
|
333
|
+
/>
|
|
334
|
+
<Button className="flex" onClick={() => {
|
|
335
|
+
const newHeaders = [...csvHeaders];
|
|
336
|
+
newHeaders.splice(index, 1);
|
|
337
|
+
setCSVHeaders(newHeaders);
|
|
338
|
+
}}><FaTrash/></Button>
|
|
339
|
+
</div>
|
|
340
|
+
</td>
|
|
341
|
+
</tr>
|
|
342
|
+
);
|
|
343
|
+
})}
|
|
344
|
+
<tr>
|
|
345
|
+
<td colSpan={2}>
|
|
346
|
+
<Button onClick={() => {
|
|
347
|
+
const csvH = [...csvHeaders];
|
|
348
|
+
csvH.push('');
|
|
349
|
+
setCSVHeaders(csvH)
|
|
350
|
+
}}><Trans i18nKey="dataimporter.addColumn">Ajouter une colonne</Trans></Button>
|
|
351
|
+
</td>
|
|
352
|
+
</tr>
|
|
353
|
+
</tbody>
|
|
354
|
+
</table>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
|
|
359
|
+
{previewData && (
|
|
360
|
+
<div className="excel-preview mt-4">
|
|
361
|
+
<h3><Trans i18nKey="dataimporter.excelPreview">Aperçu des données Excel</Trans></h3>
|
|
362
|
+
<div className="msg msg-tiny">
|
|
363
|
+
<Trans i18nKey="dataimporter.previewNote">
|
|
364
|
+
Note: Ceci est un aperçu des premières lignes. Les cellules vides sont affichées comme "(vide)".
|
|
365
|
+
</Trans>
|
|
366
|
+
</div>
|
|
367
|
+
<div className="preview-table-container" style={{ maxHeight: '300px', overflow: 'auto' }}>
|
|
368
|
+
<table className="preview-table">
|
|
369
|
+
<thead>
|
|
370
|
+
<tr>
|
|
371
|
+
{previewData[0].map((_, colIndex) => {
|
|
372
|
+
// Récupérer le nom du champ mappé pour cette colonne depuis l'état `csvHeaders`
|
|
373
|
+
const mappedFieldName = csvHeaders[colIndex];
|
|
374
|
+
|
|
375
|
+
// Si un champ est mappé, on affiche son nom traduit.
|
|
376
|
+
// Sinon, on affiche un nom générique comme "Colonne X".
|
|
377
|
+
const headerLabel = mappedFieldName
|
|
378
|
+
? t(`field_${mappedFieldName}`, mappedFieldName)
|
|
379
|
+
: t('dataimporter.column', 'Colonne {{count}}', { count: colIndex + 1 });
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<th key={`header-${colIndex}`}>
|
|
383
|
+
{headerLabel}
|
|
384
|
+
</th>
|
|
385
|
+
);
|
|
386
|
+
})}
|
|
387
|
+
</tr>
|
|
388
|
+
</thead>
|
|
389
|
+
<tbody>
|
|
390
|
+
{previewData.map((row, rowIndex) => (
|
|
391
|
+
<tr key={`row-${rowIndex}`}>
|
|
392
|
+
{row.map((cell, cellIndex) => (
|
|
393
|
+
<td
|
|
394
|
+
key={`cell-${rowIndex}-${cellIndex}`}
|
|
395
|
+
style={{
|
|
396
|
+
border: '1px solid #ddd',
|
|
397
|
+
padding: '4px',
|
|
398
|
+
backgroundColor: rowIndex === 0 && hasHeaders ? '#f0f0f0' : 'transparent'
|
|
399
|
+
}}
|
|
400
|
+
>
|
|
401
|
+
{cell !== null ? String(cell) : <span style={{ color: '#999' }}><Trans i18nKey="dataimporter.nullValue">(vide)</Trans></span>}
|
|
402
|
+
</td>
|
|
403
|
+
))}
|
|
404
|
+
</tr>
|
|
405
|
+
))}
|
|
406
|
+
</tbody>
|
|
407
|
+
</table>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
<div>
|
|
412
|
+
<Button onClick={handleImportClick} disabled={isLoading || !file}>
|
|
413
|
+
<Trans i18nKey="btns.import">Importer</Trans>
|
|
414
|
+
</Button>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
{/* Afficher la progression pour toutes les tâches d'importation actives/suivies */}
|
|
418
|
+
{jobsToDisplay.length > 0 && (
|
|
419
|
+
<div className="import-jobs-list">
|
|
420
|
+
<h3><Trans i18nKey="dataimporter.activeImports">Importations en cours / terminées</Trans></h3>
|
|
421
|
+
{jobsToDisplay.map(job => {
|
|
422
|
+
const progressPercentage = job.totalRecords > 0
|
|
423
|
+
? (job.processedRecords / job.totalRecords) * 100
|
|
424
|
+
: 0;
|
|
425
|
+
const isJobFinished = job.status === 'completed' || job.status === 'failed' || job.status === 'not_found';
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<div key={job.jobId} className="import-progress-container">
|
|
429
|
+
<h4><Trans i18nKey="dataimporter.jobId">Tâche ID:</Trans> {job.jobId?.substring(0, 8)}...</h4>
|
|
430
|
+
<p>
|
|
431
|
+
<Trans i18nKey="dataimporter.status">Statut:</Trans>{' '}
|
|
432
|
+
<strong>{t(`dataimporter.status.${job.status}`, job.status)}</strong>
|
|
433
|
+
</p>
|
|
434
|
+
{job.totalRecords > 0 && (
|
|
435
|
+
<p>
|
|
436
|
+
<Trans i18nKey="dataimporter.recordsProcessed">Enregistrements traités:</Trans>{' '}
|
|
437
|
+
{job.processedRecords} / {job.totalRecords}
|
|
438
|
+
</p>
|
|
439
|
+
)}
|
|
440
|
+
<div className="progress-bar-wrapper">
|
|
441
|
+
<div
|
|
442
|
+
className="progress-bar"
|
|
443
|
+
style={{ width: `${progressPercentage}%` }}
|
|
444
|
+
>
|
|
445
|
+
{progressPercentage.toFixed(0)}%
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
{job.errors && job.errors.length > 0 && (
|
|
450
|
+
<div className="import-errors">
|
|
451
|
+
<h4><Trans i18nKey="dataimporter.errors">Erreurs:</Trans></h4>
|
|
452
|
+
<ul>
|
|
453
|
+
{job.errors.map((error, index) => (
|
|
454
|
+
<li key={index}>{error}</li>
|
|
455
|
+
))}
|
|
456
|
+
</ul>
|
|
457
|
+
</div>
|
|
458
|
+
)}
|
|
459
|
+
{isJobFinished && (
|
|
460
|
+
<div className="flex justify-end mt-2">
|
|
461
|
+
<Button onClick={async () => {
|
|
462
|
+
// 1. Appeler l'API pour supprimer le job côté serveur
|
|
463
|
+
await fetch(`/api/import/job/${job.jobId}`, { method: 'DELETE', credentials: "include" });
|
|
464
|
+
|
|
465
|
+
// 2. Mettre à jour l'état local côté client
|
|
466
|
+
setActiveJobIds(prev => prev.filter(id => id !== job.jobId));
|
|
467
|
+
setImportJobs(prev => {
|
|
468
|
+
const newState = { ...prev };
|
|
469
|
+
delete newState[job.jobId];
|
|
470
|
+
return newState;
|
|
471
|
+
});
|
|
472
|
+
addNotification({ title: t('dataimporter.jobCleared', 'Tâche effacée.'), status: 'info' });
|
|
473
|
+
}}><Trans i18nKey="btns.clear">Effacer</Trans></Button>
|
|
474
|
+
</div>
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
477
|
+
);
|
|
478
|
+
})}
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
|
|
482
|
+
<div className="flex justify-end mt-4">
|
|
483
|
+
<Button onClick={handleCloseModal} disabled={isAnyImportInProgress}>
|
|
484
|
+
<Trans i18nKey="btns.close">Fermer</Trans>
|
|
485
|
+
</Button>
|
|
486
|
+
</div>
|
|
487
|
+
</>
|
|
488
|
+
</Dialog>
|
|
489
|
+
);
|
|
469
490
|
}
|