data-primals-engine 1.4.1 → 1.4.2

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 CHANGED
@@ -1,4 +1,4 @@
1
- # Data Primals Engine
1
+ # data-primals-engine
2
2
  [![Node.js CI](https://github.com/anonympins/data-primals-engine/actions/workflows/node.js.yml/badge.svg?branch=main)](https://github.com/anonympins/data-primals-engine/actions/workflows/node.js.yml)
3
3
  ![](https://img.shields.io/npm/dw/data-primals-engine)
4
4
  ![](https://img.shields.io/npm/last-update/data-primals-engine)
@@ -33,15 +33,6 @@
33
33
  - **📦 Starter Packs**: Quickly bootstrap applications with pre-built data packs for CRM, e-commerce, and more.
34
34
  - **📄Auto-Generated API Documentation**: Interactive API documentation available via the interface or at `/api-docs`.
35
35
 
36
- ## ⚛️ Frontend Integration (React)
37
-
38
- This engine is designed to work seamlessly with its dedicated client library, **`data-primals-engine/client`**. This library provides a complete set of React hooks and UI components to build a rich, data-centric user interface on top of the backend.
39
-
40
- While this README focuses on the backend engine and its API, you can find detailed instructions on how to integrate the client part in your React application here:
41
-
42
- ➡️ **[View the Frontend Integration Guide](https://github.com/anonympins/data-primals-engine/tree/develop/client)**
43
-
44
-
45
36
  ## 🌟 Why Choose data-primals-engine?
46
37
 
47
38
  - **Zero Boilerplate**: Focus on your business logic, not infrastructure
@@ -129,10 +120,11 @@ By default, the app runs on port **7633** : http://localhost:7633
129
120
 
130
121
  The engine includes a pluggable system for user management. For development and testing, it uses a `DefaultUserProvider` which creates a volatile **`demo`** user. This is perfect for getting started quickly without setting up a user database.
131
122
 
132
- For production environments, you should extend the base `UserProvider` class to connect to your actual user database (e.g., another MongoDB collection, a SQL database, or an external authentication service). This allows you to implement your own logic for finding users and validating passwords.
123
+ For production environments, you should use SSO providers as seen in the page below, or extend the base `UserProvider` class to connect to your actual user database (e.g., another MongoDB collection, a SQL database, or an external authentication service). This allows you to implement your own logic for finding and creating users.
124
+
125
+ ### Model generation
126
+ Models are the way to handle structured data. They organize data and they can be declared in JSON.
133
127
 
134
- ### Models
135
- Define schemas using JSON:
136
128
  ```json
137
129
  {
138
130
  "name": "product",
@@ -150,12 +142,8 @@ Define schemas using JSON:
150
142
  ]
151
143
  }
152
144
  ```
153
- ### Smart Relations
154
- - Handles up to 2,000 direct relations by default (can be customized)
155
- - For larger datasets, use intermediate collections
156
- - Automatic indexing on key fields
157
- - Custom indexing on fields
158
- - Custom fields :
145
+
146
+ ### Custom fields types
159
147
 
160
148
  | Type | Description | Properties/Notes |
161
149
  |:------------|:------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|
@@ -180,6 +168,12 @@ Define schemas using JSON:
180
168
  | model | Stores a model by name | – |
181
169
  | modelField | Stores a model field path | – |
182
170
 
171
+ ### Other model features
172
+ - Handles up to 2,000 direct relations by default (can be customized). For larger datasets, use intermediate collections
173
+ - Automatic indexing on key fields
174
+ - Custom indexing on fields
175
+ - Anonymizable fields (encrypted for API users)
176
+
183
177
  ### Model constraints
184
178
  ```javascript
185
179
  {
@@ -826,6 +820,16 @@ Results are merged together if multiple events are triggered.
826
820
  - arrays are concatenated
827
821
  - objects are merged using spread operator
828
822
 
823
+ ---
824
+
825
+ ## ⚛️ Frontend Integration (React)
826
+
827
+ This engine is designed to work seamlessly with its dedicated client library, **`data-primals-engine/client`**. This library provides a complete set of React hooks and UI components to build a rich, data-centric user interface on top of the backend.
828
+
829
+ While this README focuses on the backend engine and its API, you can find detailed instructions on how to integrate the client part in your React application here:
830
+
831
+ ➡️ **[View the Frontend Integration Guide](https://github.com/anonympins/data-primals-engine/tree/develop/client)**
832
+
829
833
 
830
834
  ---
831
835
 
@@ -1,16 +1,26 @@
1
1
  // client/src/components/AssistantChat.jsx
2
2
 
3
3
  import React, { useState, useMemo, useEffect, useRef } from 'react';
4
- import { FaRobot, FaPaperPlane, FaTimes, FaExpand, FaCompress } from 'react-icons/fa';
4
+ import { FaRobot, FaPaperPlane, FaTimes, FaExpand, FaCompress, FaPlus } from 'react-icons/fa';
5
5
  import './AssistantChat.scss';
6
6
  import { useModelContext } from "./contexts/ModelContext.jsx";
7
7
  import { Trans, useTranslation } from "react-i18next";
8
8
  import Markdown from 'react-markdown';
9
9
  import {useQueryClient} from "react-query";
10
10
  import {providers} from "../../src/modules/assistant/constants.js";
11
+ import DashboardChart from "./DashboardChart.jsx";
12
+ import {useUI} from "./contexts/UIContext.jsx";
13
+ import Button from "./Button.jsx";
14
+ import {getUserHash, getUserId} from "../../src/data.js";
15
+ import {useNavigation} from "react-router";
16
+ import {useAuthContext} from "./contexts/AuthContext.jsx";
17
+ import {useNavigate} from "react-router-dom";
18
+ import {DataTable} from "./DataTable.jsx";
11
19
 
12
20
  const AssistantChat = ({ config }) => {
13
- const { selectedModel } = useModelContext();
21
+ const { selectedModel, models } = useModelContext();
22
+ const { me } = useAuthContext();
23
+ const nav = useNavigate();
14
24
  const { t } = useTranslation();
15
25
  const [isOpen, setIsOpen] = useState(false);
16
26
  const [isMaximized, setIsMaximized] = useState(false);
@@ -19,6 +29,7 @@ const AssistantChat = ({ config }) => {
19
29
  ]);
20
30
  const [input, setInput] = useState('');
21
31
  const [isLoading, setIsLoading] = useState(false);
32
+ const { setChartToAdd } = useUI();
22
33
 
23
34
  // NOUVEL ÉTAT : Stocke une action en attente de confirmation de l'utilisateur
24
35
  const [pendingConfirmation, setPendingConfirmation] = useState(null);
@@ -74,7 +85,8 @@ const AssistantChat = ({ config }) => {
74
85
  const botMessage = {
75
86
  from: 'bot',
76
87
  text: null,
77
- actionDetails: null // Pour stocker les détails de l'action à afficher
88
+ actionDetails: null,
89
+ chartConfig: null// Pour stocker les détails de l'action à afficher
78
90
  };
79
91
 
80
92
  // Gérer le texte à afficher
@@ -104,7 +116,15 @@ const AssistantChat = ({ config }) => {
104
116
  }
105
117
 
106
118
  // On ajoute le message à la liste uniquement s'il a du contenu textuel
107
- if (botMessage.text) {
119
+ if (result.chartConfig) {
120
+ botMessage.chartConfig = result.chartConfig;
121
+ }
122
+ // Si des données tabulaires sont retournées
123
+ if (result.dataResult) {
124
+ botMessage.dataResult = result.dataResult;
125
+ }
126
+ // On ajoute le message à la liste uniquement s'il a du contenu textuel ou un graphique
127
+ if (botMessage.text || botMessage.chartConfig || botMessage.dataResult) {
108
128
  setMessages(prev => [...prev, botMessage]);
109
129
  }
110
130
 
@@ -225,7 +245,30 @@ const AssistantChat = ({ config }) => {
225
245
  <div className="chat-messages">
226
246
  {messages.map((msg, index) => (
227
247
  <div key={index} className={`message ${msg.from}`}>
228
- <Markdown>{msg.text}</Markdown>
248
+ {msg.text && <Markdown>{msg.text}</Markdown>}
249
+ {msg.chartConfig && (
250
+ <div className="chart-container">
251
+ <DashboardChart config={msg.chartConfig} />
252
+ <div className="chart-actions" style={{ marginTop: '8px', textAlign: 'right' }}>
253
+ <Button onClick={() => {
254
+ nav('/user/'+getUserHash(me)+'/dashboards');
255
+ setChartToAdd(msg.chartConfig);
256
+ }} title={t('assistant.addToDashboard', 'Ajouter au tableau de bord')}>
257
+ <FaPlus />
258
+ <span style={{ marginLeft: '8px' }}>
259
+ {t('assistant.addToDashboard', 'Ajouter au tableau de bord')}
260
+ </span>
261
+ </Button>
262
+ </div>
263
+ </div>
264
+ )}
265
+
266
+ {/* NOUVEAU : Affichage des données tabulaires */}
267
+ {msg.dataResult && (
268
+ <div className="data-table-container">
269
+ <DataTable model={models.find(f => f.name === msg.dataResult.model)} advanced={false} data={msg.dataResult.data} />
270
+ </div>
271
+ )}
229
272
  {msg.actionDetails && (
230
273
  <div className="action-details">
231
274
  {msg.actionDetails.model && (
@@ -125,7 +125,6 @@ $input-height: 50px;
125
125
  gap: 12px;
126
126
 
127
127
  .message {
128
- max-width: 80%;
129
128
  p {
130
129
  padding: 4px 8px;
131
130
  border-radius: 18px;
@@ -55,6 +55,7 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
55
55
  const [chartType, setChartType] = useState('bar');
56
56
  const [chartAggregationType, setChartAggregationType] = useState('count');
57
57
  const [chartTitle, setChartTitle] = useState('');
58
+ const [timeUnit, setTimeUnit] = useState('day');
58
59
 
59
60
  const currentModel = models.find(f => f.name === selectedModel && f._user === me.username);
60
61
 
@@ -74,6 +75,7 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
74
75
  setColorField(initialConfig.chartBackgroundColor || null);
75
76
  setGroupByLabelField(initialConfig.groupByLabelField || '');
76
77
  setChartAggregationType(initialConfig.aggregationType || 'count');
78
+ setTimeUnit(initialConfig.timeUnit || 'day');
77
79
  // Note: modelFields et relatedModelFields seront chargés par les autres useEffects
78
80
  // déclenchés par le changement de selectedModel et groupByField.
79
81
  } else {
@@ -87,6 +89,7 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
87
89
  setGroupByField('');
88
90
  setGroupByLabelField('');
89
91
  setChartAggregationType('count');
92
+ setTimeUnit('day');
90
93
  setModelFields([]);
91
94
  setRelatedModelFields([]);
92
95
  }
@@ -155,6 +158,9 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
155
158
  const isRelationGroupBy = isGroupingChart && modelFields.find(f => f.name === groupByField)?.type === 'relation';
156
159
  const isYAxisRequiredForValidation = chartAggregationType && !['count'].includes(chartAggregationType);
157
160
 
161
+ const xAxisFieldDefinition = modelFields.find(f => f.name === xAxisField);
162
+ const isTemporal = !isGroupingChart && xAxisFieldDefinition && ['date', 'datetime'].includes(xAxisFieldDefinition.type);
163
+
158
164
  // handleSave (inchangé - envoie l'état actuel)
159
165
  const handleSave = () => {
160
166
  const isValid = selectedModel && chartType && chartTitle &&
@@ -178,6 +184,7 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
178
184
  aggregationType: chartAggregationType,
179
185
  chartBackgroundColor: colorField,
180
186
  filter,
187
+ timeUnit: isTemporal ? timeUnit : undefined,
181
188
  });
182
189
  } else {
183
190
  let errorMsg = t('chartConfigModal.fillFields', "Veuillez remplir tous les champs requis.");
@@ -321,15 +328,33 @@ const ChartConfigModal = ({ isOpen, onClose, onSave, initialConfig = null }) =>
321
328
  )}
322
329
  </>
323
330
  ) : (
324
- <SelectField
325
- label={t('chartConfigModal.xAxis', 'Axe X')}
326
- value={xAxisField}
327
- onChange={(item) => setXAxisField(item.value)}
328
- items={[{label: t('selectPlaceholder', 'Choisir...'), value: ''}, ...xAxisOptions]}
329
- required={!isGroupingChart}
330
- disabled={xAxisOptions.length === 0}
331
- hint={xAxisOptions.length === 0 ? t('chartConfigModal.noXAxisFields', 'Aucun champ utilisable pour l\'axe X') : ''}
332
- />
331
+ <>
332
+ <SelectField
333
+ label={t('chartConfigModal.xAxis', 'Axe X')}
334
+ value={xAxisField}
335
+ onChange={(item) => setXAxisField(item.value)}
336
+ items={[{label: t('selectPlaceholder', 'Choisir...'), value: ''}, ...xAxisOptions]}
337
+ required={!isGroupingChart}
338
+ disabled={xAxisOptions.length === 0}
339
+ hint={xAxisOptions.length === 0 ? t('chartConfigModal.noXAxisFields', 'Aucun champ utilisable pour l\'axe X') : ''}
340
+ />
341
+ {isTemporal && (
342
+ <SelectField
343
+ label={t('charts.timeUnit', 'Échelle de temps')}
344
+ value={timeUnit}
345
+ onChange={(item) => setTimeUnit(item.value)}
346
+ items={[
347
+ { value: 'minute', label: t('time.minute', 'Minute') },
348
+ { value: 'hour', label: t('time.hour', 'Heure') },
349
+ { value: 'day', label: t('time.day', 'Jour') },
350
+ { value: 'week', label: t('time.week', 'Semaine') },
351
+ { value: 'month', label: t('time.month', 'Mois') },
352
+ { value: 'year', label: t('time.year', 'Année') },
353
+ ]}
354
+ hint={t('charts.timeUnitHelp', "Définit l'unité de regroupement pour l'axe des dates.")}
355
+ />
356
+ )}
357
+ </>
333
358
  )}
334
359
 
335
360
  {/* Axe Y (inchangé) */}