data-primals-engine 1.4.3 → 1.5.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/README.md +878 -867
- package/client/package-lock.json +49 -0
- package/client/package.json +1 -0
- package/client/src/App.jsx +1 -1
- package/client/src/App.scss +13 -3
- package/client/src/AssistantChat.scss +3 -2
- package/client/src/DashboardView.jsx +569 -569
- package/client/src/DataEditor.jsx +2 -2
- package/client/src/DataLayout.jsx +1 -8
- package/client/src/DataTable.jsx +26 -4
- package/client/src/Field.jsx +1825 -1788
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/ModelCreator.jsx +1 -2
- package/client/src/ModelCreatorField.jsx +23 -27
- package/client/src/ModelList.jsx +1 -1
- package/client/src/constants.js +1 -1
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/translations.js +2 -0
- package/package.json +2 -1
- package/perf/README.md +147 -0
- package/perf/artillery-hooks.js +37 -0
- package/perf/perf-shot-hardwork.yml +84 -0
- package/perf/perf-shot-search.yml +45 -0
- package/perf/setup.yml +26 -0
- package/server.js +1 -1
- package/src/constants.js +2 -27
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +1544 -1540
- package/src/email.js +5 -2
- package/src/engine.js +5 -3
- package/src/filter.js +5 -3
- package/src/modules/assistant/assistant.js +3 -1
- package/src/modules/bucket.js +12 -15
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.js +6 -3
- package/src/modules/data/data.operations.js +3231 -2999
- package/src/modules/data/data.routes.js +1821 -1785
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +4 -1
- package/src/modules/file.js +4 -2
- package/src/modules/user.js +4 -1
- package/src/modules/workflow.js +9 -10
- package/src/openai.jobs.js +2 -0
- package/src/packs.js +22 -5
- package/src/providers.js +22 -7
- package/test/data.integration.test.js +1060 -981
- package/test/import_export.integration.test.js +1 -1
|
@@ -33,8 +33,10 @@ const formatDate = (value, type, t) => {
|
|
|
33
33
|
|
|
34
34
|
const FlexDataRenderer = ({ value, fieldDefinition,data }) => {
|
|
35
35
|
const { t, i18n } = useTranslation();
|
|
36
|
+
const lang = (i18n.resolvedLanguage || i18n.language).split(/[-_]/)?.[0];
|
|
36
37
|
const {models} = useModelContext()
|
|
37
38
|
const {me} = useAuthContext()
|
|
39
|
+
|
|
38
40
|
// 'translatedLabel' is added by getFieldDefinitionByPath
|
|
39
41
|
const field = fieldDefinition;
|
|
40
42
|
const translatedLabel = t('model_'+field.name, field.name);
|
package/client/src/KPIDialog.jsx
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Dialog } from './Dialog.jsx'; // Assure-toi que Dialog est importé
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import {FaAd, FaPlus} from "react-icons/fa";
|
|
5
|
+
import {getUserHash} from "../../src/data.js";
|
|
6
|
+
import {useNavigate} from "react-router-dom";
|
|
7
|
+
import {useAuthContext} from "./contexts/AuthContext.jsx";
|
|
4
8
|
|
|
5
9
|
function KPIDialog({ availableKpis = [], onAddKpi, onClose }) {
|
|
6
10
|
const { t } = useTranslation();
|
|
7
11
|
const [searchTerm, setSearchTerm] = useState('');
|
|
8
|
-
|
|
12
|
+
const nav = useNavigate()
|
|
13
|
+
const {me} = useAuthContext()
|
|
9
14
|
const filteredKpis = (availableKpis || []).filter(kpi =>
|
|
10
15
|
kpi.name.value.toLowerCase().includes(searchTerm.toLowerCase())
|
|
11
16
|
);
|
|
@@ -32,6 +37,11 @@ function KPIDialog({ availableKpis = [], onAddKpi, onClose }) {
|
|
|
32
37
|
<li>{t('dashboards.no_kpi_found', 'Aucun KPI disponible ou correspondant.')}</li>
|
|
33
38
|
)}
|
|
34
39
|
</ul>
|
|
40
|
+
<div className={"flex mg-2"}>
|
|
41
|
+
<button onClick={() => {
|
|
42
|
+
nav(`/user/${getUserHash(me)}/?model=kpi`);
|
|
43
|
+
}} className={"btn"}><FaPlus />{t('dashboards.addKpi', 'Nouvel indicateur KPI')}</button>
|
|
44
|
+
</div>
|
|
35
45
|
</Dialog>
|
|
36
46
|
);
|
|
37
47
|
}
|
|
@@ -660,11 +660,10 @@ const ModelCreator = forwardRef(({ initialPrompt = '', onModelGenerated, autoGen
|
|
|
660
660
|
{/* Boutons d'action, visibles si un modèle est affiché ou en mode manuel */}
|
|
661
661
|
{(showModel || !useAI || initialModel) && (
|
|
662
662
|
<div className="actions flex">
|
|
663
|
-
|
|
663
|
+
|
|
664
664
|
<Button type="button" onClick={handleAddField}>
|
|
665
665
|
<FaPlus /> <Trans i18nKey={"btns.addField"}>Ajouter un champ</Trans>
|
|
666
666
|
</Button>
|
|
667
|
-
)}
|
|
668
667
|
|
|
669
668
|
<Button type="submit">
|
|
670
669
|
<FaSave />
|
|
@@ -72,7 +72,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
72
72
|
newFields[index].name = e.target.value;
|
|
73
73
|
setFields(newFields);
|
|
74
74
|
}}
|
|
75
|
-
help={t('modelcreator.name.hint')}
|
|
76
75
|
required
|
|
77
76
|
after={!(!modelLocked && isLocalUser(me) && field.locked) && !field._isNewField && (
|
|
78
77
|
<Button type={"button"} className={"btn-form btn-last"}
|
|
@@ -212,10 +211,10 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
212
211
|
|
|
213
212
|
<div className="flex flex-no-wrap">
|
|
214
213
|
{hint('modelcreator.relationFilter.hint')}
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
214
|
+
<div className="checkbox-label flex flex-1">
|
|
215
|
+
<CheckboxField
|
|
216
|
+
label={<Trans
|
|
217
|
+
i18nKey={"modelcreator.relationFilter"}>Filtre</Trans>}
|
|
219
218
|
disabled={modelLocked || (isLocalUser(me) && field.locked)}
|
|
220
219
|
checked={field.relationFilter !== undefined}
|
|
221
220
|
onChange={(e) => {
|
|
@@ -228,7 +227,7 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
228
227
|
setFields(newFields);
|
|
229
228
|
}}
|
|
230
229
|
/>
|
|
231
|
-
</
|
|
230
|
+
</div>
|
|
232
231
|
</div>)}
|
|
233
232
|
|
|
234
233
|
|
|
@@ -286,7 +285,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
286
285
|
newFields[index].multiline = e;
|
|
287
286
|
setFields(newFields);
|
|
288
287
|
}}
|
|
289
|
-
help={field.multiline && t('modelcreator.multiline.hint')}
|
|
290
288
|
/>
|
|
291
289
|
</div>
|
|
292
290
|
</div>
|
|
@@ -295,28 +293,26 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
295
293
|
|
|
296
294
|
{['string', 'string_t', 'richtext', 'richtext_t', 'email', 'phone', 'url', 'password', 'code'].includes(field.itemsType || field.type) && (
|
|
297
295
|
<>
|
|
298
|
-
<div className={"flex flex-no-wrap"}>
|
|
296
|
+
<div className={"flex flex-no-wrap field-bg"}>
|
|
299
297
|
{hint('modelcreator.maxlength.hint')}
|
|
300
|
-
<
|
|
301
|
-
<Trans i18nKey={"modelcreator.maxlength"}>Longueur
|
|
302
|
-
maximale :</Trans>
|
|
303
|
-
<NumberField
|
|
298
|
+
<div className={"flex-1"}><NumberField
|
|
304
299
|
disabled={modelLocked || (isLocalUser(me) && field.locked)}
|
|
305
300
|
step={1}
|
|
306
301
|
min={0}
|
|
302
|
+
label={<Trans i18nKey={"modelcreator.maxlength"}>Longueur
|
|
303
|
+
maximale :</Trans>}
|
|
307
304
|
className={"flex-1"}
|
|
308
305
|
value={field.maxlength}
|
|
309
306
|
onChange={(e) => {
|
|
310
307
|
const newFields = [...fields];
|
|
311
|
-
const val = parseInt(e, 10);
|
|
312
|
-
if (
|
|
308
|
+
const val = parseInt(e.target.value, 10);
|
|
309
|
+
if (isNaN(val))
|
|
313
310
|
newFields[index].maxlength = undefined;
|
|
314
311
|
else
|
|
315
312
|
newFields[index].maxlength = val;
|
|
316
313
|
setFields(newFields);
|
|
317
314
|
}}
|
|
318
|
-
|
|
319
|
-
</label>
|
|
315
|
+
/></div>
|
|
320
316
|
</div>
|
|
321
317
|
</>
|
|
322
318
|
)}
|
|
@@ -438,7 +434,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
438
434
|
newFields[index].required = e;
|
|
439
435
|
setFields(newFields);
|
|
440
436
|
}}
|
|
441
|
-
help={field.required && t('modelcreator.required.hint')}
|
|
442
437
|
/>
|
|
443
438
|
</div>
|
|
444
439
|
</div>
|
|
@@ -456,7 +451,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
456
451
|
newFields[index].unique = e;
|
|
457
452
|
setFields(newFields);
|
|
458
453
|
}}
|
|
459
|
-
help={field.unique && t('modelcreator.unique.hint')}
|
|
460
454
|
/>
|
|
461
455
|
</div>
|
|
462
456
|
</div>
|
|
@@ -555,7 +549,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
555
549
|
disabled={modelLocked || (isLocalUser(me) && field.locked)}
|
|
556
550
|
onChange={(e) => {
|
|
557
551
|
const newFields = [...fields];
|
|
558
|
-
|
|
559
552
|
newFields[index].default = e.value;
|
|
560
553
|
setFields(newFields);
|
|
561
554
|
}}
|
|
@@ -626,10 +619,12 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
626
619
|
|
|
627
620
|
{(['datetime', 'date'].includes(field.itemsType || field.type)) && (
|
|
628
621
|
<>
|
|
629
|
-
<div
|
|
622
|
+
<div
|
|
623
|
+
className="flex flex-no-wrap mg-item">
|
|
630
624
|
{hint('modelcreator.min.hint')}
|
|
631
|
-
<
|
|
632
|
-
|
|
625
|
+
<div className="flex field-bg flex-1">
|
|
626
|
+
<label className={"flex-1"}><Trans i18nKey={"modelcreator.min"}>Valeur minimale :</Trans></label>
|
|
627
|
+
<input
|
|
633
628
|
className={"flex-1"}
|
|
634
629
|
disabled={modelLocked || (isLocalUser(me) && field.locked)}
|
|
635
630
|
type={field.type === 'datetime' ? 'datetime-local' : field.type}
|
|
@@ -643,13 +638,16 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
643
638
|
}
|
|
644
639
|
setFields(newFields);
|
|
645
640
|
}}
|
|
646
|
-
|
|
641
|
+
/>
|
|
642
|
+
</div>
|
|
647
643
|
</div>
|
|
648
644
|
|
|
649
645
|
<div className="flex flex-no-wrap mg-item">
|
|
650
646
|
{hint('modelcreator.max.hint')}
|
|
651
|
-
<
|
|
652
|
-
|
|
647
|
+
<div className="flex field-bg flex-1">
|
|
648
|
+
<label className={"flex-1"}><Trans i18nKey={"modelcreator.max"}>Valeur maximale :</Trans></label>
|
|
649
|
+
<input
|
|
650
|
+
className={"flex-1"}
|
|
653
651
|
disabled={modelLocked || (isLocalUser(me) && field.locked)}
|
|
654
652
|
type={field.type === 'datetime' ? 'datetime-local' : field.type}
|
|
655
653
|
value={field.type === "number" ? (field.max + '').replace('.', ',') : field.max}
|
|
@@ -807,7 +805,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
807
805
|
newFields[index].asMain = e;
|
|
808
806
|
setFields(newFields);
|
|
809
807
|
}}
|
|
810
|
-
help={field.asMain && t('modelcreator.asMain.hint')}
|
|
811
808
|
/>
|
|
812
809
|
</div>
|
|
813
810
|
</div>)}
|
|
@@ -858,7 +855,6 @@ const ModelCreatorField = ({model, handleRenameField, handleRemoveField, handleU
|
|
|
858
855
|
newFields[index].hiddenable = e;
|
|
859
856
|
setFields(newFields);
|
|
860
857
|
}}
|
|
861
|
-
help={field.unique && t('modelcreator.hiddenable.hint')}
|
|
862
858
|
/>
|
|
863
859
|
</div>
|
|
864
860
|
</div>
|
package/client/src/ModelList.jsx
CHANGED
|
@@ -190,7 +190,7 @@ export function ModelList({ onModelSelect, onCreateModel, onImportModel, onEditM
|
|
|
190
190
|
key={'modelist' + model.name} onClick={() => generatedModels.some(g => g.name === model.name) ? onEditModel(model) : onModelSelect(model)}>
|
|
191
191
|
<div className="flex flex-center flex-fw">
|
|
192
192
|
<div
|
|
193
|
-
className="flex flex-1 flex-no-wrap break-word">
|
|
193
|
+
className="flex flex-1 flex-no-wrap break-word gap-2">
|
|
194
194
|
<div className={"icon"}>{IconComponent ? IconComponent : <></>}</div>
|
|
195
195
|
<div>{t(`model_${model.name}`, model.name)} {generatedModels.some(f => f.name === model.name) ? '(tmp)' : (mods.some(f => f.mod === model.name) ? `(${mods.find(f => f.mod === model.name).count})` : '')}</div></div>
|
|
196
196
|
<div className="btns">
|
package/client/src/constants.js
CHANGED
|
@@ -57,7 +57,7 @@ export const profiles = {
|
|
|
57
57
|
}, // budget,
|
|
58
58
|
'developer': {
|
|
59
59
|
"packs": ["Multilingual starter pack", "Website Starter Pack"],
|
|
60
|
-
"models": ['alert','endpoint','request','webpage', 'content', 'taxonomy', 'resource', 'translation', 'contact', 'location', 'channel', 'lang', 'token', 'message', 'ticket', 'user', 'permission', 'role']
|
|
60
|
+
"models": ['alert','endpoint','request','kpi','webpage', 'content', 'taxonomy', 'resource', 'translation', 'contact', 'location', 'channel', 'lang', 'token', 'message', 'ticket', 'user', 'permission', 'role']
|
|
61
61
|
},
|
|
62
62
|
'company': {
|
|
63
63
|
"packs": ["Multilingual starter pack", "E-commerce Starter Kit"],
|
|
@@ -4,7 +4,7 @@ import { useUI } from '../contexts/UIContext.jsx';
|
|
|
4
4
|
import { useNotificationContext } from '../NotificationProvider.jsx';
|
|
5
5
|
import { tutorialsConfig } from '../tutorials.js';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
import useLocalStorage from "./useLocalStorage.js";
|
|
9
9
|
import {Event} from "../../../src/events.js";
|
|
10
10
|
|
|
@@ -20,9 +20,9 @@ export const useTutorials = () => {
|
|
|
20
20
|
const { allTourSteps, setCurrentTourSteps, setIsTourOpen } = useUI();
|
|
21
21
|
const { addNotification } = useNotificationContext();
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
const
|
|
23
|
+
// Ref pour "verrouiller" la vérification d'une étape spécifique et éviter les notifications en double
|
|
24
|
+
// à cause des re-renders rapides (race condition).
|
|
25
|
+
const completingStepRef = useRef(null);
|
|
26
26
|
|
|
27
27
|
const { data: tutorials, isLoading: isLoadingTutorials } = useQuery('tutorials', async () => {
|
|
28
28
|
return tutorialsConfig.map(t => ({ ...t, _id: t.id }));
|
|
@@ -119,102 +119,99 @@ export const useTutorials = () => {
|
|
|
119
119
|
updateActiveTutorial(newActiveState);
|
|
120
120
|
launchTour(firstStage.tourName);
|
|
121
121
|
};
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
const triggerTutorialCheck = useCallback(async () => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
// S'il n'y a pas de tutoriel actif, on ne fait rien.
|
|
125
|
+
if (!me?.activeTutorial || !tutorials) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { id: currentTutorialId, stage: currentStageNumber } = me.activeTutorial;
|
|
130
|
+
const stepIdentifier = `${currentTutorialId}-${currentStageNumber}`;
|
|
131
|
+
|
|
132
|
+
// --- VERROUILLAGE (LOCK) ---
|
|
133
|
+
// Si on est déjà en train de traiter cette étape exacte, on ignore les appels suivants
|
|
134
|
+
// pour éviter les notifications multiples.
|
|
135
|
+
if (completingStepRef.current === stepIdentifier) {
|
|
136
|
+
console.log(`[Tutoriels] Vérification pour l'étape ${stepIdentifier} déjà en cours. On ignore.`);
|
|
127
137
|
return;
|
|
128
138
|
}
|
|
129
|
-
if (!me?.activeTutorial || !tutorials) return;
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
const tutorial = tutorials.find(t => t._id === currentTutorialId);
|
|
141
|
+
const stageConfig = tutorial?.stages.find(s => s.stage === currentStageNumber);
|
|
142
|
+
|
|
143
|
+
if (!stageConfig) return; // Pas d'étape active à vérifier
|
|
133
144
|
|
|
134
145
|
try {
|
|
135
|
-
|
|
146
|
+
// On pose le verrou pour cette étape
|
|
147
|
+
completingStepRef.current = stepIdentifier;
|
|
148
|
+
console.log(`[Tutoriels] Verrouillage et vérification de l'étape ${stepIdentifier}`);
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
const tutorial = tutorials.find(t => t._id === currentActiveState.id);
|
|
139
|
-
if (!tutorial) break;
|
|
150
|
+
const { isCompleted } = await checkCompletionCondition(stageConfig.completionCondition);
|
|
140
151
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
console.log('[Tutoriels] Fin du tutoriel, attribution des récompenses.');
|
|
144
|
-
claimRewards(tutorial._id);
|
|
145
|
-
updateActiveTutorial(null);
|
|
146
|
-
setIsTourOpen(false);
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
152
|
+
if (isCompleted) {
|
|
153
|
+
console.log(`[Tutoriels] Étape ${currentStageNumber} complétée.`);
|
|
149
154
|
|
|
150
|
-
const
|
|
155
|
+
const titleKey = `tutorial.${tutorial.id}.stage.${currentStageNumber}.name`;
|
|
156
|
+
const descriptionKey = `tutorial.${tutorial.id}.stage.${currentStageNumber}.description`;
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
addNotification({
|
|
159
|
+
title: t(titleKey, stageConfig.name),
|
|
160
|
+
message: t(descriptionKey, stageConfig.description),
|
|
161
|
+
status: 'success',
|
|
162
|
+
id: `tuto-step-${stepIdentifier}` // ID unique pour la notification
|
|
163
|
+
});
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
// On passe à l'étape suivante (ou on termine le tutoriel)
|
|
166
|
+
const nextStageNumber = currentStageNumber + 1;
|
|
167
|
+
const nextStage = tutorial.stages.find(s => s.stage === nextStageNumber);
|
|
158
168
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
status: 'success'
|
|
163
|
-
});
|
|
164
|
-
currentActiveState = { id: currentActiveState.id, stage: currentActiveState.stage + 1 };
|
|
169
|
+
if (nextStage) {
|
|
170
|
+
updateActiveTutorial({ id: currentTutorialId, stage: nextStageNumber });
|
|
171
|
+
launchTour(nextStage.tourName);
|
|
165
172
|
} else {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
launchTour(stageConfig.tourName);
|
|
170
|
-
}
|
|
171
|
-
break;
|
|
173
|
+
claimRewards(tutorial._id);
|
|
174
|
+
updateActiveTutorial(null);
|
|
175
|
+
setIsTourOpen(false);
|
|
172
176
|
}
|
|
177
|
+
} else {
|
|
178
|
+
console.log(`[Tutoriels] Étape ${currentStageNumber} non complétée.`);
|
|
173
179
|
}
|
|
174
180
|
} catch (error) {
|
|
175
|
-
console.error("[Tutoriels] Erreur lors de la vérification
|
|
181
|
+
console.error("[Tutoriels] Erreur lors de la vérification de l'étape :", error);
|
|
176
182
|
} finally {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// Si une vérification a été mise en attente, on la lance maintenant.
|
|
181
|
-
if (isCheckQueuedRef.current) {
|
|
182
|
-
console.log('[Tutoriels] Lancement de la vérification mise en file d\'attente.');
|
|
183
|
-
isCheckQueuedRef.current = false;
|
|
184
|
-
triggerTutorialCheck();
|
|
185
|
-
}
|
|
183
|
+
// On libère le verrou
|
|
184
|
+
completingStepRef.current = null;
|
|
185
|
+
console.log(`[Tutoriels] Libération du verrou pour l'étape ${stepIdentifier}`);
|
|
186
186
|
}
|
|
187
|
-
}, [
|
|
188
|
-
tutorials, checkCompletionCondition, claimRewards, updateActiveTutorial,
|
|
189
|
-
addNotification, launchTour, t, setIsTourOpen, me
|
|
190
|
-
]);
|
|
187
|
+
}, [me, tutorials, checkCompletionCondition, updateActiveTutorial, claimRewards, addNotification, launchTour, t, setIsTourOpen]);
|
|
191
188
|
|
|
192
189
|
useEffect(() => {
|
|
193
190
|
setMe(prevMe => ({ ...prevMe, activeTutorial: activeTuto }));
|
|
194
191
|
}, [activeTuto]);
|
|
195
192
|
|
|
193
|
+
const handleDataChange = useCallback(async (payload) => {
|
|
194
|
+
if (me?.activeTutorial) {
|
|
195
|
+
console.log(`[Tutoriels] Événement de données reçu, déclenchement de la vérification.`, payload);
|
|
196
|
+
triggerTutorialCheck();
|
|
197
|
+
}
|
|
198
|
+
}, [me?.activeTutorial, triggerTutorialCheck]);
|
|
199
|
+
|
|
196
200
|
// Écoute les événements de modification de données pour vérifier la progression en arrière-plan.
|
|
197
201
|
useEffect(() => {
|
|
198
|
-
const handleDataChange = async (payload) => {
|
|
199
|
-
console.log(`[Tutoriels] Événement de données reçu.`, payload);
|
|
200
|
-
if (me?.activeTutorial) {
|
|
201
|
-
triggerTutorialCheck();
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
console.log(me.activeTutorial + new Date().getMilliseconds());
|
|
206
202
|
const eventTypes = ['API_ADD_DATA', 'API_EDIT_DATA', 'API_DELETE_DATA'];
|
|
207
203
|
eventTypes.forEach(type => Event.Listen(type, handleDataChange, "custom", "data"));
|
|
208
204
|
|
|
209
205
|
return () => {
|
|
210
206
|
eventTypes.forEach(type => Event.RemoveCallback(type, handleDataChange, "custom", "data"));
|
|
211
207
|
};
|
|
212
|
-
}, [
|
|
208
|
+
}, [handleDataChange]);
|
|
213
209
|
|
|
214
210
|
// Vérifie la progression lorsqu'un tutoriel devient actif ou que son étape change.
|
|
215
211
|
useEffect(() => {
|
|
216
212
|
triggerTutorialCheck();
|
|
217
|
-
},[]);
|
|
213
|
+
},[me?.activeTutorial]); // Se déclenche quand le tutoriel/étape change
|
|
214
|
+
|
|
218
215
|
return {
|
|
219
216
|
tutorials,
|
|
220
217
|
isLoadingTutorials,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "data-primals-engine is a package responsible from handling large amount of data using MongoDB in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation. It also has integrated AI assistant.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"stripe": "^18.4.0",
|
|
102
102
|
"swagger-ui-express": "^5.0.1",
|
|
103
103
|
"tar": "^7.4.3",
|
|
104
|
+
"tinycolor2": "^1.6.0",
|
|
104
105
|
"uniqid": "^5.4.0",
|
|
105
106
|
"vitest": "^3.2.4",
|
|
106
107
|
"yaml": "^2.8.0"
|
package/perf/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
## Performance Test Report 1
|
|
2
|
+
|
|
3
|
+
### Test Environment
|
|
4
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
5
|
+
- **Concurrency**: Ramp-up to 50 virtual users/sec
|
|
6
|
+
- Runs on `perf-shot-search.yml` ,that simulates user basic activity on API
|
|
7
|
+
|
|
8
|
+
### Summary of Results
|
|
9
|
+
|
|
10
|
+
| Metric | Value |
|
|
11
|
+
|----------------------------|------------|
|
|
12
|
+
| Total HTTP Requests | 1,475 |
|
|
13
|
+
| Successful Responses (2xx) | 1,475 |
|
|
14
|
+
| Average Request Rate | 19/sec |
|
|
15
|
+
| Data Downloaded | 13.5 MB |
|
|
16
|
+
| Failed Scenarios | 0 |
|
|
17
|
+
|
|
18
|
+
### Response Time Analysis (in milliseconds)
|
|
19
|
+
|
|
20
|
+
| Percentile | Response Time (ms) |
|
|
21
|
+
|------------|--------------------|
|
|
22
|
+
| min | 8 |
|
|
23
|
+
| median | **36.2** |
|
|
24
|
+
| mean | 253.9 |
|
|
25
|
+
| p95 | 561.2 |
|
|
26
|
+
| p99 | 608 |
|
|
27
|
+
| max | 805 |
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### Analysis & Recommendations
|
|
31
|
+
|
|
32
|
+
* **Outstanding Performance & Stability**: The final test results are excellent. The server handled **1,475 requests with 0 failures**, demonstrating exceptional stability under sustained load.
|
|
33
|
+
|
|
34
|
+
* **Bottleneck Resolved**:
|
|
35
|
+
* The **median response time is now just 36.2 milliseconds**. This is a dramatic improvement and indicates that the core API operations are extremely fast for the majority of users.
|
|
36
|
+
* The **p95 (561.2 ms)** and **p99 (608 ms)** response times remain well under one second. This shows that the application performs consistently, with very few outliers, even during peak load phases.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Performance Test Report 2
|
|
41
|
+
|
|
42
|
+
### Test Environment
|
|
43
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
44
|
+
- **Scenario**: `perf-shot-hardwork.yml` (Full user journey including model creation, data import, and pack installation)
|
|
45
|
+
- **Concurrency**: Sustained load of 1 virtual user/sec for 60 seconds.
|
|
46
|
+
|
|
47
|
+
### Summary of Results
|
|
48
|
+
|
|
49
|
+
| Metric | Value |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Total HTTP Requests | 480 |
|
|
52
|
+
| Successful Responses (2xx) | 480 |
|
|
53
|
+
| Average Request Rate | 4/sec |
|
|
54
|
+
| Data Downloaded | 0.88 MB |
|
|
55
|
+
| **Failed Scenarios** | **0** |
|
|
56
|
+
|
|
57
|
+
### Response Time Analysis (in milliseconds)
|
|
58
|
+
|
|
59
|
+
| Percentile | Response Time (ms) |
|
|
60
|
+
|------------|--------------------|
|
|
61
|
+
| min | 7 |
|
|
62
|
+
| median | **15** |
|
|
63
|
+
| mean | 96.1 |
|
|
64
|
+
| p95 | 368.8 |
|
|
65
|
+
| p99 | 407.5 |
|
|
66
|
+
| max | 474 |
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### Analysis & Recommendations
|
|
70
|
+
|
|
71
|
+
* **Total Success & Stability**: The test is a complete success. With a 100% success rate (**0 failed scenarios** and **0 request errors**), it confirms that the previous fixes for race conditions and file upload issues are effective and robust.
|
|
72
|
+
|
|
73
|
+
* **Outstanding Performance**: The API's responsiveness is excellent.
|
|
74
|
+
* A **median response time of just 15 milliseconds** shows that the core operations are extremely fast for the majority of users.
|
|
75
|
+
* The **p99 is 407.5 ms**, meaning 99% of requests, even within a complex multi-step scenario, completed in well under half a second. This demonstrates consistent and reliable performance.
|
|
76
|
+
|
|
77
|
+
* **Conclusion**: The application is perfectly stable and highly performant under the tested load. The entire user journey, including complex operations, is handled efficiently. This provides a solid baseline for future, more intensive load tests.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Performance Test Report 3 (Scaling)
|
|
81
|
+
|
|
82
|
+
### Test Environment
|
|
83
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
84
|
+
- **Scenario**: `perf-shot-hardwork.yml` (Full user journey)
|
|
85
|
+
- **Concurrency**: Ramped load from 1 to 2 virtual users/sec for 60 seconds.
|
|
86
|
+
|
|
87
|
+
### Summary of Results
|
|
88
|
+
|
|
89
|
+
| Metric | Value |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Total HTTP Requests | 720 |
|
|
92
|
+
| Successful Responses (2xx) | 720 |
|
|
93
|
+
| Average Request Rate | 9/sec |
|
|
94
|
+
| **Failed Scenarios** | **0** |
|
|
95
|
+
|
|
96
|
+
### Response Time Analysis (in milliseconds)
|
|
97
|
+
|
|
98
|
+
| Percentile | Response Time (ms) |
|
|
99
|
+
|------------|--------------------|
|
|
100
|
+
| min | 8 |
|
|
101
|
+
| median | **32.1** |
|
|
102
|
+
| mean | 141.4 |
|
|
103
|
+
| p95 | 772.9 |
|
|
104
|
+
| p99 | 854.2 |
|
|
105
|
+
| max | 922 |
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### Analysis & Recommendations
|
|
109
|
+
|
|
110
|
+
* **Excellent Linear Scaling**: This test confirms the application's ability to scale gracefully. When doubling the load from the previous test (1 user/sec to 2 users/sec), the median and p95 response times also roughly doubled. This linear behavior is ideal and indicates a robust architecture without major bottlenecks.
|
|
111
|
+
* **Continued Stability**: With **0 failed scenarios**, the application proves its stability even as concurrency increases.
|
|
112
|
+
* **Conclusion**: The application is ready for higher loads. The performance degradation is predictable and linear, which is a sign of a very healthy system.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## Performance Test Report 4 (Bottleneck Identification)
|
|
116
|
+
|
|
117
|
+
### Test Environment
|
|
118
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
119
|
+
- **Scenario**: `perf-shot-hardwork.yml` (Full user journey)
|
|
120
|
+
- **Concurrency**: Ramped load from 1 to 4 virtual users/sec for 60 seconds.
|
|
121
|
+
|
|
122
|
+
### Summary of Results
|
|
123
|
+
|
|
124
|
+
| Metric | Value |
|
|
125
|
+
|---|---|
|
|
126
|
+
| Total HTTP Requests | 1,200 |
|
|
127
|
+
| Successful Responses (2xx) | 1,200 |
|
|
128
|
+
| Average Request Rate | 12/sec |
|
|
129
|
+
| **Failed Scenarios** | **0** |
|
|
130
|
+
|
|
131
|
+
### Response Time Analysis (in milliseconds)
|
|
132
|
+
|
|
133
|
+
| Percentile | Response Time (ms) |
|
|
134
|
+
|------------|--------------------|
|
|
135
|
+
| min | 3 |
|
|
136
|
+
| median | **70.1** |
|
|
137
|
+
| mean | 483.9 |
|
|
138
|
+
| p95 | **2893.5** |
|
|
139
|
+
| p99 | **8024.5** |
|
|
140
|
+
| max | 8348 |
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### Analysis & Recommendations
|
|
144
|
+
|
|
145
|
+
* **Bottleneck Identified**: This test successfully identified the application's first major performance bottleneck. While stability remains perfect (**0 failures**), the response times show non-linear degradation. The **p99 jumping to over 8 seconds** indicates that the system is struggling under this write-heavy load.
|
|
146
|
+
* **Database Contention**: The cause is almost certainly database contention (MongoDB). The scenario involves concurrent model creation, data import, and pack installation, which are highly resource-intensive operations.
|
|
147
|
+
* **Next Steps**: The immediate next step is to run a detailed analysis to pinpoint the exact slow queries. Monitoring server CPU/Disk I/O during the test and using MongoDB's profiler will be essential for optimization.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {uuidv4} from "../src/core.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export function setScenarioVariables(context, events, done) {
|
|
5
|
+
// context.vars is where we store variables for the virtual user's session
|
|
6
|
+
context.vars.vuId = uuidv4().replace(/-/g, '');
|
|
7
|
+
context.vars.demoUser = `perf${Math.floor(Math.random() * 9000000) + 1000000}`;
|
|
8
|
+
return done();
|
|
9
|
+
}
|
|
10
|
+
// Hook to name requests to aggregate metrics by endpoint
|
|
11
|
+
export function metricsByEndpoint_beforeRequest(requestParams, context, ee, next) {
|
|
12
|
+
// Remove query parameters for the metric name
|
|
13
|
+
const urlPath = requestParams.url.split('?')[0];
|
|
14
|
+
|
|
15
|
+
// Replace unique IDs (ObjectId or UUID) in the URL with a placeholder for aggregation
|
|
16
|
+
// Example: /api/data/60d0fe4f5311236168a109ca -> /api/data/:id
|
|
17
|
+
const name = urlPath.replace(/([a-fA-F0-9]{24}|[a-fA-F0-9-]{36})/g, ':id');
|
|
18
|
+
|
|
19
|
+
// Emit an internal Artillery event to name the request
|
|
20
|
+
ee.emit('request:set_name', name);
|
|
21
|
+
|
|
22
|
+
return next();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Hook pour logger les erreurs de manière détaillée
|
|
26
|
+
export function metricsByEndpoint_afterResponse(requestParams, response, context, ee, next) {
|
|
27
|
+
// On vérifie si le statut est un code d'erreur (4xx ou 5xx)
|
|
28
|
+
if (response.statusCode >= 400) {
|
|
29
|
+
console.error(`\n[ERREUR] Requête ${requestParams.method} ${requestParams.url} a échoué avec le statut ${response.statusCode}`);
|
|
30
|
+
// On log le corps de la réponse, qui contient souvent le message d'erreur du serveur
|
|
31
|
+
if (response.body) {
|
|
32
|
+
console.error(` -> Réponse: ${response.body}\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// On passe à la suite du scénario
|
|
36
|
+
return next();
|
|
37
|
+
}
|