data-primals-engine 1.5.2 → 1.6.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 +924 -915
- package/client/README.md +136 -136
- package/client/index.js +0 -1
- package/client/public/doc/API.postman_collection.json +213 -213
- package/client/public/react.svg +9 -9
- package/client/src/AddWidgetTypeModal.jsx +47 -47
- package/client/src/App.css +42 -42
- package/client/src/App.jsx +92 -150
- package/client/src/App.scss +6 -0
- package/client/src/AssistantChat.jsx +362 -363
- package/client/src/Button.jsx +12 -12
- package/client/src/Dashboard.jsx +480 -480
- package/client/src/DashboardHtmlViewItem.jsx +146 -146
- package/client/src/DashboardView.jsx +654 -654
- package/client/src/DataEditor.jsx +383 -383
- package/client/src/DataLayout.jsx +779 -808
- package/client/src/DataTable.jsx +782 -822
- package/client/src/DataTable.scss +186 -186
- package/client/src/GeolocationField.jsx +93 -93
- package/client/src/HistoryDialog.jsx +307 -307
- package/client/src/HistoryDialog.scss +319 -319
- package/client/src/HtmlViewBuilderModal.jsx +90 -90
- package/client/src/HtmlViewBuilderModal.scss +17 -17
- package/client/src/HtmlViewCard.jsx +43 -43
- package/client/src/ModelCreatorField.jsx +950 -950
- package/client/src/ModelList.jsx +280 -280
- package/client/src/Notification.jsx +136 -136
- package/client/src/PackGallery.jsx +391 -391
- package/client/src/PackGallery.scss +231 -231
- package/client/src/Pagination.jsx +143 -143
- package/client/src/RelationField.jsx +354 -354
- package/client/src/RelationSelectorWidget.jsx +172 -172
- package/client/src/RelationValue.jsx +2 -1
- package/client/src/contexts/CommandContext.jsx +260 -0
- package/client/src/contexts/ModelContext.jsx +4 -1
- package/client/src/contexts/UIContext.jsx +72 -72
- package/client/src/filter.js +263 -263
- package/client/src/index.css +90 -90
- package/package.json +6 -5
- package/perf/artillery-hooks.js +37 -37
- package/perf/setup.yml +25 -25
- package/src/constants.js +4 -0
- package/src/engine.js +335 -335
- package/src/events.js +232 -137
- package/src/filter.js +274 -274
- package/src/modules/assistant/assistant.js +225 -83
- package/src/modules/auth-google/index.js +50 -50
- package/src/modules/auth-microsoft/index.js +81 -81
- package/src/modules/data/data.core.js +118 -118
- package/src/modules/data/data.history.js +555 -555
- package/src/modules/data/data.operations.js +3401 -3381
- package/src/modules/data/data.relations.js +686 -686
- package/src/modules/data/data.routes.js +1879 -1879
- package/src/modules/data/data.validation.js +2 -2
- package/src/modules/file.js +247 -247
- package/src/packs.js +2 -2
- package/src/providers.js +2 -2
- package/src/services/stripe.js +196 -196
- package/src/sso.js +193 -193
- package/test/data.history.integration.test.js +264 -264
- package/test/data.integration.test.js +1206 -1206
- package/test/events.test.js +108 -10
- package/test/model.integration.test.js +377 -377
package/client/src/Dashboard.jsx
CHANGED
|
@@ -1,481 +1,481 @@
|
|
|
1
|
-
import React, {useState, useMemo, useEffect} from 'react';
|
|
2
|
-
import { Trans, useTranslation } from 'react-i18next';
|
|
3
|
-
import {FaPlus, FaSpinner, FaCog} from "react-icons/fa"; // Ajout FaTrash
|
|
4
|
-
|
|
5
|
-
import "./Dashboard.scss"
|
|
6
|
-
import {useMutation, useQuery, useQueryClient} from "react-query";
|
|
7
|
-
import { useAuthContext } from "./contexts/AuthContext.jsx";
|
|
8
|
-
import { SelectField, DurationField, TextField } from "./Field.jsx";
|
|
9
|
-
import {DashboardView} from "./DashboardView.jsx";
|
|
10
|
-
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
11
|
-
import {useNavigate, useParams, useSearchParams} from "react-router-dom";
|
|
12
|
-
import {getObjectHash} from "../../src/core.js";
|
|
13
|
-
import {getUserHash, getUserId} from "../../src/data.js";
|
|
14
|
-
import Button from "./Button.jsx";
|
|
15
|
-
import {FaNoteSticky} from "react-icons/fa6";
|
|
16
|
-
import ChartConfigModal from "./ChartConfigModal.jsx";
|
|
17
|
-
import {useUI} from "./contexts/UIContext.jsx";
|
|
18
|
-
import {DialogProvider} from "./Dialog.jsx";
|
|
19
|
-
|
|
20
|
-
// --- DashboardsPage (Reste inchangé) ---
|
|
21
|
-
export function DashboardsPage() {
|
|
22
|
-
const { setSelectedModel, selectedModel } = useModelContext()
|
|
23
|
-
const { t } = useTranslation();
|
|
24
|
-
const { me } = useAuthContext();
|
|
25
|
-
const [selectedDashboardId, setSelectedDashboardId] = useState(null); // This seems to be the main state
|
|
26
|
-
|
|
27
|
-
const { chartToAdd, setChartToAdd, flexViewToAdd, setFlexViewToAdd, htmlViewToAdd, setHtmlViewToAdd } = useUI();
|
|
28
|
-
const [searchParams, setSearchParams] = useSearchParams();
|
|
29
|
-
const { hash } = useParams(); // 2. Récupérer le hash de l'URL
|
|
30
|
-
|
|
31
|
-
// ... après les autres hooks useState, useQuery, etc.
|
|
32
|
-
const queryClient = useQueryClient();
|
|
33
|
-
const [newDashboardName, setNewDashboardName] = useState('');
|
|
34
|
-
const [isCreating, setIsCreating] = useState(false);
|
|
35
|
-
const [isEditing, setIsEditing] = useState(false);
|
|
36
|
-
const [dashboardToEdit, setDashboardToEdit] = useState(null);
|
|
37
|
-
|
|
38
|
-
// États pour le modal de configuration des graphiques
|
|
39
|
-
const [isChartModalOpen, setIsChartModalOpen] = useState(false);
|
|
40
|
-
const [chartToConfigure, setChartToConfigure] = useState(null);
|
|
41
|
-
|
|
42
|
-
const { data: dashboardsData, isLoading: isLoadingDashboards, error: errorDashboards } = useQuery(
|
|
43
|
-
['userDashboards', me?.username],
|
|
44
|
-
async () => {
|
|
45
|
-
if (!me?.username) return null;
|
|
46
|
-
const response = await fetch(
|
|
47
|
-
`/api/data/search?model=dashboard&_user=${me.username}`, {
|
|
48
|
-
method: 'POST',
|
|
49
|
-
headers: { 'Content-Type': 'application/json' },
|
|
50
|
-
// body: JSON.stringify({ sort: { name: 1 } }) // Optionnel: trier
|
|
51
|
-
});
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
const res = await response.json();
|
|
54
|
-
throw new Error(res.error || t('dashboards.errorList', 'Erreur chargement des tableaux de bord'));
|
|
55
|
-
}
|
|
56
|
-
return await response.json();
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
enabled: !!me?.username
|
|
60
|
-
}
|
|
61
|
-
);
|
|
62
|
-
const selectedDashboard = useMemo(() => {
|
|
63
|
-
if (!selectedDashboardId || !dashboardsData?.data) return null;
|
|
64
|
-
return dashboardsData.data.find(db => db._id === selectedDashboardId);
|
|
65
|
-
}, [selectedDashboardId, dashboardsData]);
|
|
66
|
-
|
|
67
|
-
const createDashboardMutation = useMutation(
|
|
68
|
-
async (dashboardName) => {
|
|
69
|
-
const isFirstDashboard = !dashboardsData?.data || dashboardsData.data.length === 0;
|
|
70
|
-
const response = await fetch('/api/data', {
|
|
71
|
-
method: 'POST',
|
|
72
|
-
headers: { 'Content-Type': 'application/json' },
|
|
73
|
-
body: JSON.stringify({
|
|
74
|
-
model: 'dashboard',
|
|
75
|
-
data: { name: dashboardName, isDefault: isFirstDashboard } // Le premier est défini par défaut
|
|
76
|
-
})
|
|
77
|
-
});
|
|
78
|
-
if (!response.ok) {
|
|
79
|
-
const errorData = await response.json();
|
|
80
|
-
throw new Error(errorData.error || t('dashboards.errorCreate', 'Impossible de créer le tableau de bord'));
|
|
81
|
-
}
|
|
82
|
-
return response.json();
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
onSuccess: () => {
|
|
86
|
-
// Rafraîchit la liste des dashboards après la création
|
|
87
|
-
queryClient.invalidateQueries(['userDashboards', me?.username]);
|
|
88
|
-
setNewDashboardName('');
|
|
89
|
-
setIsCreating(false);
|
|
90
|
-
},
|
|
91
|
-
onError: (error) => {
|
|
92
|
-
// Idéalement, afficher une notification à l'utilisateur
|
|
93
|
-
console.error("Erreur lors de la création du dashboard:", error);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
const updateDashboardMutation = useMutation(
|
|
99
|
-
async (dashboardData) => {
|
|
100
|
-
const { _id, _hash, _user, _createdAt, _updatedAt, ...data } = dashboardData;
|
|
101
|
-
const response = await fetch(`/api/data`, {
|
|
102
|
-
method: 'PUT',
|
|
103
|
-
headers: { 'Content-Type': 'application/json' },
|
|
104
|
-
body: JSON.stringify({
|
|
105
|
-
model: 'dashboard',
|
|
106
|
-
_id: _id,
|
|
107
|
-
data: data
|
|
108
|
-
})
|
|
109
|
-
});
|
|
110
|
-
if (!response.ok) {
|
|
111
|
-
const errorData = await response.json();
|
|
112
|
-
throw new Error(errorData.error || t('dashboards.errorUpdate', 'Impossible de mettre à jour le tableau de bord'));
|
|
113
|
-
}
|
|
114
|
-
return response.json();
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
onSuccess: () => {
|
|
118
|
-
queryClient.invalidateQueries(['userDashboards', me?.username]);
|
|
119
|
-
setIsEditing(false);
|
|
120
|
-
},
|
|
121
|
-
onError: (error) => {
|
|
122
|
-
console.error("Erreur lors de la mise à jour du dashboard:", error);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
const handleCreateDashboard = (e) => {
|
|
128
|
-
e.preventDefault();
|
|
129
|
-
if (newDashboardName.trim()) {
|
|
130
|
-
createDashboardMutation.mutate(newDashboardName.trim());
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const handleUpdateDashboard = (e) => {
|
|
135
|
-
e.preventDefault();
|
|
136
|
-
if (dashboardToEdit.name.trim()) {
|
|
137
|
-
updateDashboardMutation.mutate(dashboardToEdit);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const handleEditFieldChange = (e) => {
|
|
142
|
-
// e can be a standard event or an object { name, value } from custom fields
|
|
143
|
-
const name = e.target ? e.target.name : e.name;
|
|
144
|
-
const value = e.target ? e.target.value : e.value;
|
|
145
|
-
setDashboardToEdit(prev => ({ ...prev, [name]: value }));
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const handleSaveChart = (chartConfig) => {
|
|
149
|
-
if (!selectedDashboard) return;
|
|
150
|
-
|
|
151
|
-
const newLayout = [...(selectedDashboard.layout || [])];
|
|
152
|
-
|
|
153
|
-
if (chartConfig.id) { // Mode édition
|
|
154
|
-
const index = newLayout.findIndex(c => c.id === chartConfig.id);
|
|
155
|
-
if (index > -1) {
|
|
156
|
-
newLayout[index] = chartConfig;
|
|
157
|
-
}
|
|
158
|
-
} else { // Mode ajout
|
|
159
|
-
newLayout.push({ ...chartConfig, id: new Date().getTime().toString() });
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
updateDashboardMutation.mutate({
|
|
163
|
-
...selectedDashboard,
|
|
164
|
-
layout: newLayout
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
setIsChartModalOpen(false);
|
|
168
|
-
setChartToConfigure(null);
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const handleSaveFlexView = (flexViewConfig) => {
|
|
172
|
-
if (!selectedDashboard) return;
|
|
173
|
-
|
|
174
|
-
// Safely get the layout as a mutable array, creating a default section if needed.
|
|
175
|
-
const newLayout = JSON.parse(JSON.stringify(Array.isArray(selectedDashboard.layout) ? selectedDashboard.layout : []));
|
|
176
|
-
|
|
177
|
-
if (newLayout.length === 0) {
|
|
178
|
-
newLayout.push({
|
|
179
|
-
name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
|
|
180
|
-
kpis: [],
|
|
181
|
-
chartConfigs: [],
|
|
182
|
-
flexViews: [],
|
|
183
|
-
htmlViews: []
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Add the widget to the first section
|
|
188
|
-
const targetSection = newLayout[0];
|
|
189
|
-
if (!targetSection.flexViews) {
|
|
190
|
-
targetSection.flexViews = [];
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
targetSection.flexViews.push({
|
|
194
|
-
...flexViewConfig,
|
|
195
|
-
type: 'flexView', // Add a type to distinguish from charts
|
|
196
|
-
id: new Date().getTime().toString()
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
updateDashboardMutation.mutate({ ...selectedDashboard, layout: newLayout });
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const handleSaveHtmlView = (htmlViewConfig) => {
|
|
203
|
-
if (!selectedDashboard) return;
|
|
204
|
-
|
|
205
|
-
// Safely get the layout as a mutable array, creating a default section if needed.
|
|
206
|
-
const newLayout = JSON.parse(JSON.stringify(Array.isArray(selectedDashboard.layout) ? selectedDashboard.layout : []));
|
|
207
|
-
|
|
208
|
-
if (newLayout.length === 0) {
|
|
209
|
-
newLayout.push({
|
|
210
|
-
name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
|
|
211
|
-
kpis: [],
|
|
212
|
-
chartConfigs: [],
|
|
213
|
-
flexViews: [],
|
|
214
|
-
htmlViews: []
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Add the widget to the first section
|
|
219
|
-
const targetSection = newLayout[0];
|
|
220
|
-
if (!targetSection.htmlViews) {
|
|
221
|
-
targetSection.htmlViews = [];
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// We only need to save the template and data-fetching info, not the data itself.
|
|
225
|
-
const { data, ...configToSave } = htmlViewConfig;
|
|
226
|
-
|
|
227
|
-
targetSection.htmlViews.push({
|
|
228
|
-
...configToSave,
|
|
229
|
-
type: 'htmlView', // Add a type to distinguish
|
|
230
|
-
id: `html-${Date.now()}`
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
updateDashboardMutation.mutate({ ...selectedDashboard, layout: newLayout });
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
237
|
-
|
|
238
|
-
const refreshPresets = useMemo(() => [
|
|
239
|
-
{ label: t('dashboards.refreshPresets.none', 'Désactivé'), value: null },
|
|
240
|
-
{ label: t('dashboards.refreshPresets.10s', '10 secondes'), value: 10 },
|
|
241
|
-
{ label: t('dashboards.refreshPresets.30s', '30 secondes'), value: 30 },
|
|
242
|
-
{ label: t('dashboards.refreshPresets.1m', '1 minute'), value: 60 },
|
|
243
|
-
{ label: t('dashboards.refreshPresets.5m', '5 minutes'), value: 300 },
|
|
244
|
-
{ label: t('dashboards.refreshPresets.15m', '15 minutes'), value: 900 },
|
|
245
|
-
], [t]);
|
|
246
|
-
|
|
247
|
-
useEffect(() => {
|
|
248
|
-
if( chartToAdd ){
|
|
249
|
-
// Ouvre le modal de configuration avec les données du graphique de l'IA
|
|
250
|
-
setChartToConfigure(chartToAdd);
|
|
251
|
-
setIsChartModalOpen(true);
|
|
252
|
-
setChartToAdd(null); // Réinitialise le contexte pour éviter de rouvrir le modal
|
|
253
|
-
}
|
|
254
|
-
}, [chartToAdd, setChartToAdd, selectedDashboard]);
|
|
255
|
-
|
|
256
|
-
// NOUVEAU: Gérer l'ajout d'une vue flexible depuis le contexte UI
|
|
257
|
-
useEffect(() => {
|
|
258
|
-
if (flexViewToAdd && selectedDashboard) {
|
|
259
|
-
handleSaveFlexView(flexViewToAdd);
|
|
260
|
-
setFlexViewToAdd(null); // Réinitialiser le contexte
|
|
261
|
-
}
|
|
262
|
-
}, [flexViewToAdd, setFlexViewToAdd, selectedDashboard]);
|
|
263
|
-
|
|
264
|
-
// Gérer l'ajout d'une vue HTML depuis le contexte UI
|
|
265
|
-
useEffect(() => {
|
|
266
|
-
if (htmlViewToAdd && selectedDashboard) {
|
|
267
|
-
handleSaveHtmlView(htmlViewToAdd);
|
|
268
|
-
setHtmlViewToAdd(null); // Réinitialiser le contexte
|
|
269
|
-
}
|
|
270
|
-
}, [htmlViewToAdd, setHtmlViewToAdd, selectedDashboard]);
|
|
271
|
-
|
|
272
|
-
useEffect(() => {
|
|
273
|
-
// When a new dashboard is selected, close the editing form
|
|
274
|
-
setIsEditing(false);
|
|
275
|
-
}, [selectedDashboardId]);
|
|
276
|
-
|
|
277
|
-
useEffect(() => {
|
|
278
|
-
// When the edit form is opened, initialize it with the selected dashboard's data
|
|
279
|
-
if (isEditing && selectedDashboard) {
|
|
280
|
-
setDashboardToEdit({ ...selectedDashboard });
|
|
281
|
-
}
|
|
282
|
-
}, [isEditing]);
|
|
283
|
-
|
|
284
|
-
// 3. Ce useEffect trouve le bon dashboard quand le hash ou la liste change
|
|
285
|
-
useEffect(() => {
|
|
286
|
-
if (dashboardsData?.data.length > 0) {
|
|
287
|
-
if( hash ) {
|
|
288
|
-
const foundDashboard = dashboardsData?.data.find(d => d._hash+'' === hash);
|
|
289
|
-
setSelectedDashboardId(foundDashboard?._id || null); // Met à jour avec le dashboard trouvé ou null
|
|
290
|
-
}else{
|
|
291
|
-
const defaultDashboard = dashboardsData?.data.find(db => db.isDefault === true);
|
|
292
|
-
if( !defaultDashboard) {
|
|
293
|
-
setSelectedDashboardId(dashboardsData.data[0]._id);
|
|
294
|
-
}else
|
|
295
|
-
setSelectedDashboardId(defaultDashboard._id || null);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
setIsLoading(false);
|
|
299
|
-
}, [hash,dashboardsData]);
|
|
300
|
-
|
|
301
|
-
const dashboardOptions = useMemo(() => {
|
|
302
|
-
return (dashboardsData?.data || []).map(db => ({
|
|
303
|
-
label: db.name + (db.isDefault ? ` (${t('dashboards.default', 'Défaut')})` : ''),
|
|
304
|
-
value: db._id
|
|
305
|
-
}));
|
|
306
|
-
}, [dashboardsData, t]);
|
|
307
|
-
|
|
308
|
-
const nav = useNavigate();
|
|
309
|
-
|
|
310
|
-
useEffect(() => {
|
|
311
|
-
setSelectedModel(null)
|
|
312
|
-
}, []);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (isLoading) {
|
|
316
|
-
return <div>{t('loading', 'Chargement...')}</div>;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (!selectedDashboardId && hash) {
|
|
320
|
-
return (
|
|
321
|
-
<div className="dashboard-not-found">
|
|
322
|
-
<h2>{t('dashboards.notFound', 'Dashboard non trouvé')}</h2>
|
|
323
|
-
<p>{t('dashboards.notFoundHelp', 'Le dashboard avec l\'identifiant "{{0}}" n\'existe pas ou vous n\'y avez pas accès.',[hash] )}</p>
|
|
324
|
-
</div>
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const creationForm = (
|
|
329
|
-
<form onSubmit={handleCreateDashboard} className="create-dashboard-form flex flex-start mg-v-1">
|
|
330
|
-
<input
|
|
331
|
-
type="text"
|
|
332
|
-
className="input-fit"
|
|
333
|
-
value={newDashboardName}
|
|
334
|
-
onChange={(e) => setNewDashboardName(e.target.value)}
|
|
335
|
-
placeholder={t('dashboards.newName', 'Nom du nouveau tableau de bord')}
|
|
336
|
-
disabled={createDashboardMutation.isLoading}
|
|
337
|
-
autoFocus
|
|
338
|
-
/>
|
|
339
|
-
<button type="submit" className="btn" disabled={createDashboardMutation.isLoading || !newDashboardName.trim()}>
|
|
340
|
-
{createDashboardMutation.isLoading
|
|
341
|
-
? <><FaSpinner className="spin" /> <Trans i18nKey="creating">Création...</Trans></>
|
|
342
|
-
: <Trans i18nKey="btns.create">Créer</Trans>
|
|
343
|
-
}
|
|
344
|
-
</button>
|
|
345
|
-
{/* Bouton pour annuler quand on a cliqué sur le "+" */}
|
|
346
|
-
{isCreating && (
|
|
347
|
-
<button type="button" className="btn btn-secondary" onClick={() => setIsCreating(false)}>
|
|
348
|
-
<Trans i18nKey="btns.cancel">Annuler</Trans>
|
|
349
|
-
</button>
|
|
350
|
-
)}
|
|
351
|
-
</form>
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
return (
|
|
355
|
-
<div className="dashboards-page">
|
|
356
|
-
<div className={"flex right actions"}>
|
|
357
|
-
<Button onClick={() => {
|
|
358
|
-
nav("/user/"+getUserHash(me)+"/?model="+(selectedModel?.name||'dashboard'));
|
|
359
|
-
}}><FaNoteSticky /> <Trans i18nKey={"models"}>Modèles</Trans></Button>
|
|
360
|
-
</div>
|
|
361
|
-
<h1>{t('dashboards.title', 'Mes Tableaux de Bord')}</h1>
|
|
362
|
-
|
|
363
|
-
{isLoadingDashboards && (
|
|
364
|
-
<p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingList">Chargement des tableaux de bord...</Trans></p>
|
|
365
|
-
)}
|
|
366
|
-
|
|
367
|
-
{errorDashboards && (
|
|
368
|
-
<p className="error">{errorDashboards.message}</p>
|
|
369
|
-
)}
|
|
370
|
-
|
|
371
|
-
{!isLoadingDashboards && !errorDashboards && (
|
|
372
|
-
<>
|
|
373
|
-
{dashboardsData?.data && dashboardsData.data.length > 0 ? (
|
|
374
|
-
<div className="dashboard-selector-container">
|
|
375
|
-
<div className="dashboard-selector flex" style={{ alignItems: 'flex-end', gap: '8px' }}>
|
|
376
|
-
<SelectField
|
|
377
|
-
name="dashboardSelection"
|
|
378
|
-
label={t('dashboards.select', 'Choisir un tableau de bord :')}
|
|
379
|
-
value={selectedDashboardId}
|
|
380
|
-
onChange={(selectedOption) => {
|
|
381
|
-
setSelectedDashboardId(selectedOption.value);
|
|
382
|
-
const d = dashboardsData.data.find(f => f._id === selectedOption.value);
|
|
383
|
-
history.pushState({}, null, '/user/'+getUserHash(me)+'/dashboards/'+d._hash);
|
|
384
|
-
}}
|
|
385
|
-
items={dashboardOptions}
|
|
386
|
-
/>
|
|
387
|
-
<button onClick={() => { setIsCreating(true); setIsEditing(false); }} className="btn" title={t('dashboards.add', 'Ajouter un tableau de bord')}>
|
|
388
|
-
<FaPlus />
|
|
389
|
-
</button>
|
|
390
|
-
{selectedDashboard && (
|
|
391
|
-
<button onClick={() => { setIsEditing(true); setIsCreating(false); }} className="btn" title={t('dashboards.configure', 'Configurer le tableau de bord')}>
|
|
392
|
-
<FaCog />
|
|
393
|
-
</button>
|
|
394
|
-
)}
|
|
395
|
-
</div>
|
|
396
|
-
{isCreating && creationForm}
|
|
397
|
-
{isEditing && dashboardToEdit && (
|
|
398
|
-
<form onSubmit={handleUpdateDashboard} className="edit-dashboard-form flex flex-col flex-start mg-v-1 pd-1" style={{ border: '1px solid #ccc', borderRadius: '4px' }}>
|
|
399
|
-
<h3 className="mg-b-1">{t('dashboards.configure', 'Configurer le tableau de bord')}</h3>
|
|
400
|
-
<TextField
|
|
401
|
-
name="name"
|
|
402
|
-
label={t('dashboards.dashboardName', 'Nom du tableau de bord')}
|
|
403
|
-
value={dashboardToEdit.name}
|
|
404
|
-
onChange={handleEditFieldChange}
|
|
405
|
-
required
|
|
406
|
-
autoFocus
|
|
407
|
-
/>
|
|
408
|
-
|
|
409
|
-
<div className="flex" style={{ gap: '1rem', alignItems: 'flex-end', width: '100%' }}>
|
|
410
|
-
<div style={{ flexGrow: 1 }}>
|
|
411
|
-
<DurationField
|
|
412
|
-
name="refreshInterval"
|
|
413
|
-
label={t('dashboards.refreshInterval', 'Intervalle de rafraîchissement')}
|
|
414
|
-
value={dashboardToEdit.refreshInterval}
|
|
415
|
-
onChange={handleEditFieldChange}
|
|
416
|
-
help={t('dashboards.refreshIntervalHelp', 'Laisser vide pour désactiver le rafraîchissement automatique. La valeur est en secondes.')}
|
|
417
|
-
/>
|
|
418
|
-
</div>
|
|
419
|
-
<SelectField
|
|
420
|
-
name="refreshIntervalPreset"
|
|
421
|
-
label={t('dashboards.refreshPresets.label', 'Préréglages')}
|
|
422
|
-
value={dashboardToEdit.refreshInterval}
|
|
423
|
-
onChange={(option) => {
|
|
424
|
-
console.log({option});
|
|
425
|
-
if (option?.value)
|
|
426
|
-
handleEditFieldChange({ name: 'refreshInterval', value: option?.value })
|
|
427
|
-
}}
|
|
428
|
-
items={refreshPresets}
|
|
429
|
-
style={{ minWidth: '150px' }}
|
|
430
|
-
/>
|
|
431
|
-
</div>
|
|
432
|
-
<div className="flex mg-t-1" style={{ gap: '8px' }}>
|
|
433
|
-
<button type="submit" className="btn" disabled={updateDashboardMutation.isLoading}>
|
|
434
|
-
{updateDashboardMutation.isLoading ? <><FaSpinner className="spin" /> <Trans i18nKey="btns.saving">Enregistrement...</Trans></> : <Trans i18nKey="btns.save">Enregistrer</Trans>}
|
|
435
|
-
</button>
|
|
436
|
-
<button type="button" className="btn btn-secondary" onClick={() => setIsEditing(false)}>
|
|
437
|
-
<Trans i18nKey="btns.cancel">Annuler</Trans>
|
|
438
|
-
</button>
|
|
439
|
-
</div>
|
|
440
|
-
</form>
|
|
441
|
-
)}
|
|
442
|
-
</div>
|
|
443
|
-
) : (
|
|
444
|
-
<>
|
|
445
|
-
<p><Trans i18nKey="dashboards.noDashboards">Aucun tableau de bord trouvé. Vous pouvez en créer un.</Trans></p>
|
|
446
|
-
{creationForm}
|
|
447
|
-
</>
|
|
448
|
-
)}
|
|
449
|
-
|
|
450
|
-
{/* Affiche la vue du dashboard sélectionné */}
|
|
451
|
-
{/* Passe l'objet dashboard complet */}
|
|
452
|
-
{/*
|
|
453
|
-
TODO in DashboardView.jsx:
|
|
454
|
-
Utiliser la nouvelle propriété `dashboard.refreshInterval` (en secondes).
|
|
455
|
-
Si `refreshInterval` est défini et supérieur à 0, les données du dashboard (KPIs, graphiques)
|
|
456
|
-
doivent être rafraîchies automatiquement à cet intervalle.
|
|
457
|
-
Exemple avec react-query: `useQuery(queryKey, queryFn, { refetchInterval: dashboard.refreshInterval * 1000 })`
|
|
458
|
-
ou avec useEffect/setInterval:
|
|
459
|
-
useEffect(() => {
|
|
460
|
-
if (dashboard?.refreshInterval > 0) {
|
|
461
|
-
const intervalId = setInterval(() => {
|
|
462
|
-
// logique de rafraîchissement, ex: queryClient.invalidateQueries(...)
|
|
463
|
-
}, dashboard.refreshInterval * 1000);
|
|
464
|
-
return () => clearInterval(intervalId);
|
|
465
|
-
}
|
|
466
|
-
}, [dashboard?.refreshInterval, queryClient]);
|
|
467
|
-
*/}
|
|
468
|
-
<DashboardView dashboard={selectedDashboard} />
|
|
469
|
-
|
|
470
|
-
{/* Modal pour ajouter/éditer un graphique */}
|
|
471
|
-
<DialogProvider><ChartConfigModal
|
|
472
|
-
isOpen={isChartModalOpen}
|
|
473
|
-
onClose={() => setIsChartModalOpen(false)}
|
|
474
|
-
onSave={handleSaveChart}
|
|
475
|
-
initialConfig={chartToConfigure}
|
|
476
|
-
/></DialogProvider>
|
|
477
|
-
</>
|
|
478
|
-
)}
|
|
479
|
-
</div>
|
|
480
|
-
);
|
|
1
|
+
import React, {useState, useMemo, useEffect} from 'react';
|
|
2
|
+
import { Trans, useTranslation } from 'react-i18next';
|
|
3
|
+
import {FaPlus, FaSpinner, FaCog} from "react-icons/fa"; // Ajout FaTrash
|
|
4
|
+
|
|
5
|
+
import "./Dashboard.scss"
|
|
6
|
+
import {useMutation, useQuery, useQueryClient} from "react-query";
|
|
7
|
+
import { useAuthContext } from "./contexts/AuthContext.jsx";
|
|
8
|
+
import { SelectField, DurationField, TextField } from "./Field.jsx";
|
|
9
|
+
import {DashboardView} from "./DashboardView.jsx";
|
|
10
|
+
import {useModelContext} from "./contexts/ModelContext.jsx";
|
|
11
|
+
import {useNavigate, useParams, useSearchParams} from "react-router-dom";
|
|
12
|
+
import {getObjectHash} from "../../src/core.js";
|
|
13
|
+
import {getUserHash, getUserId} from "../../src/data.js";
|
|
14
|
+
import Button from "./Button.jsx";
|
|
15
|
+
import {FaNoteSticky} from "react-icons/fa6";
|
|
16
|
+
import ChartConfigModal from "./ChartConfigModal.jsx";
|
|
17
|
+
import {useUI} from "./contexts/UIContext.jsx";
|
|
18
|
+
import {DialogProvider} from "./Dialog.jsx";
|
|
19
|
+
|
|
20
|
+
// --- DashboardsPage (Reste inchangé) ---
|
|
21
|
+
export function DashboardsPage() {
|
|
22
|
+
const { setSelectedModel, selectedModel } = useModelContext()
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
const { me } = useAuthContext();
|
|
25
|
+
const [selectedDashboardId, setSelectedDashboardId] = useState(null); // This seems to be the main state
|
|
26
|
+
|
|
27
|
+
const { chartToAdd, setChartToAdd, flexViewToAdd, setFlexViewToAdd, htmlViewToAdd, setHtmlViewToAdd } = useUI();
|
|
28
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
29
|
+
const { hash } = useParams(); // 2. Récupérer le hash de l'URL
|
|
30
|
+
|
|
31
|
+
// ... après les autres hooks useState, useQuery, etc.
|
|
32
|
+
const queryClient = useQueryClient();
|
|
33
|
+
const [newDashboardName, setNewDashboardName] = useState('');
|
|
34
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
35
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
36
|
+
const [dashboardToEdit, setDashboardToEdit] = useState(null);
|
|
37
|
+
|
|
38
|
+
// États pour le modal de configuration des graphiques
|
|
39
|
+
const [isChartModalOpen, setIsChartModalOpen] = useState(false);
|
|
40
|
+
const [chartToConfigure, setChartToConfigure] = useState(null);
|
|
41
|
+
|
|
42
|
+
const { data: dashboardsData, isLoading: isLoadingDashboards, error: errorDashboards } = useQuery(
|
|
43
|
+
['userDashboards', me?.username],
|
|
44
|
+
async () => {
|
|
45
|
+
if (!me?.username) return null;
|
|
46
|
+
const response = await fetch(
|
|
47
|
+
`/api/data/search?model=dashboard&_user=${me.username}`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
// body: JSON.stringify({ sort: { name: 1 } }) // Optionnel: trier
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const res = await response.json();
|
|
54
|
+
throw new Error(res.error || t('dashboards.errorList', 'Erreur chargement des tableaux de bord'));
|
|
55
|
+
}
|
|
56
|
+
return await response.json();
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
enabled: !!me?.username
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
const selectedDashboard = useMemo(() => {
|
|
63
|
+
if (!selectedDashboardId || !dashboardsData?.data) return null;
|
|
64
|
+
return dashboardsData.data.find(db => db._id === selectedDashboardId);
|
|
65
|
+
}, [selectedDashboardId, dashboardsData]);
|
|
66
|
+
|
|
67
|
+
const createDashboardMutation = useMutation(
|
|
68
|
+
async (dashboardName) => {
|
|
69
|
+
const isFirstDashboard = !dashboardsData?.data || dashboardsData.data.length === 0;
|
|
70
|
+
const response = await fetch('/api/data', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
model: 'dashboard',
|
|
75
|
+
data: { name: dashboardName, isDefault: isFirstDashboard } // Le premier est défini par défaut
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const errorData = await response.json();
|
|
80
|
+
throw new Error(errorData.error || t('dashboards.errorCreate', 'Impossible de créer le tableau de bord'));
|
|
81
|
+
}
|
|
82
|
+
return response.json();
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
onSuccess: () => {
|
|
86
|
+
// Rafraîchit la liste des dashboards après la création
|
|
87
|
+
queryClient.invalidateQueries(['userDashboards', me?.username]);
|
|
88
|
+
setNewDashboardName('');
|
|
89
|
+
setIsCreating(false);
|
|
90
|
+
},
|
|
91
|
+
onError: (error) => {
|
|
92
|
+
// Idéalement, afficher une notification à l'utilisateur
|
|
93
|
+
console.error("Erreur lors de la création du dashboard:", error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const updateDashboardMutation = useMutation(
|
|
99
|
+
async (dashboardData) => {
|
|
100
|
+
const { _id, _hash, _user, _createdAt, _updatedAt, ...data } = dashboardData;
|
|
101
|
+
const response = await fetch(`/api/data`, {
|
|
102
|
+
method: 'PUT',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
model: 'dashboard',
|
|
106
|
+
_id: _id,
|
|
107
|
+
data: data
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
const errorData = await response.json();
|
|
112
|
+
throw new Error(errorData.error || t('dashboards.errorUpdate', 'Impossible de mettre à jour le tableau de bord'));
|
|
113
|
+
}
|
|
114
|
+
return response.json();
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
onSuccess: () => {
|
|
118
|
+
queryClient.invalidateQueries(['userDashboards', me?.username]);
|
|
119
|
+
setIsEditing(false);
|
|
120
|
+
},
|
|
121
|
+
onError: (error) => {
|
|
122
|
+
console.error("Erreur lors de la mise à jour du dashboard:", error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const handleCreateDashboard = (e) => {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
if (newDashboardName.trim()) {
|
|
130
|
+
createDashboardMutation.mutate(newDashboardName.trim());
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleUpdateDashboard = (e) => {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
if (dashboardToEdit.name.trim()) {
|
|
137
|
+
updateDashboardMutation.mutate(dashboardToEdit);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handleEditFieldChange = (e) => {
|
|
142
|
+
// e can be a standard event or an object { name, value } from custom fields
|
|
143
|
+
const name = e.target ? e.target.name : e.name;
|
|
144
|
+
const value = e.target ? e.target.value : e.value;
|
|
145
|
+
setDashboardToEdit(prev => ({ ...prev, [name]: value }));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handleSaveChart = (chartConfig) => {
|
|
149
|
+
if (!selectedDashboard) return;
|
|
150
|
+
|
|
151
|
+
const newLayout = [...(selectedDashboard.layout || [])];
|
|
152
|
+
|
|
153
|
+
if (chartConfig.id) { // Mode édition
|
|
154
|
+
const index = newLayout.findIndex(c => c.id === chartConfig.id);
|
|
155
|
+
if (index > -1) {
|
|
156
|
+
newLayout[index] = chartConfig;
|
|
157
|
+
}
|
|
158
|
+
} else { // Mode ajout
|
|
159
|
+
newLayout.push({ ...chartConfig, id: new Date().getTime().toString() });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
updateDashboardMutation.mutate({
|
|
163
|
+
...selectedDashboard,
|
|
164
|
+
layout: newLayout
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
setIsChartModalOpen(false);
|
|
168
|
+
setChartToConfigure(null);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleSaveFlexView = (flexViewConfig) => {
|
|
172
|
+
if (!selectedDashboard) return;
|
|
173
|
+
|
|
174
|
+
// Safely get the layout as a mutable array, creating a default section if needed.
|
|
175
|
+
const newLayout = JSON.parse(JSON.stringify(Array.isArray(selectedDashboard.layout) ? selectedDashboard.layout : []));
|
|
176
|
+
|
|
177
|
+
if (newLayout.length === 0) {
|
|
178
|
+
newLayout.push({
|
|
179
|
+
name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
|
|
180
|
+
kpis: [],
|
|
181
|
+
chartConfigs: [],
|
|
182
|
+
flexViews: [],
|
|
183
|
+
htmlViews: []
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Add the widget to the first section
|
|
188
|
+
const targetSection = newLayout[0];
|
|
189
|
+
if (!targetSection.flexViews) {
|
|
190
|
+
targetSection.flexViews = [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
targetSection.flexViews.push({
|
|
194
|
+
...flexViewConfig,
|
|
195
|
+
type: 'flexView', // Add a type to distinguish from charts
|
|
196
|
+
id: new Date().getTime().toString()
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
updateDashboardMutation.mutate({ ...selectedDashboard, layout: newLayout });
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleSaveHtmlView = (htmlViewConfig) => {
|
|
203
|
+
if (!selectedDashboard) return;
|
|
204
|
+
|
|
205
|
+
// Safely get the layout as a mutable array, creating a default section if needed.
|
|
206
|
+
const newLayout = JSON.parse(JSON.stringify(Array.isArray(selectedDashboard.layout) ? selectedDashboard.layout : []));
|
|
207
|
+
|
|
208
|
+
if (newLayout.length === 0) {
|
|
209
|
+
newLayout.push({
|
|
210
|
+
name: t('dashboards.defaultSectionName', 'Nouvelle Section'),
|
|
211
|
+
kpis: [],
|
|
212
|
+
chartConfigs: [],
|
|
213
|
+
flexViews: [],
|
|
214
|
+
htmlViews: []
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Add the widget to the first section
|
|
219
|
+
const targetSection = newLayout[0];
|
|
220
|
+
if (!targetSection.htmlViews) {
|
|
221
|
+
targetSection.htmlViews = [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// We only need to save the template and data-fetching info, not the data itself.
|
|
225
|
+
const { data, ...configToSave } = htmlViewConfig;
|
|
226
|
+
|
|
227
|
+
targetSection.htmlViews.push({
|
|
228
|
+
...configToSave,
|
|
229
|
+
type: 'htmlView', // Add a type to distinguish
|
|
230
|
+
id: `html-${Date.now()}`
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
updateDashboardMutation.mutate({ ...selectedDashboard, layout: newLayout });
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
237
|
+
|
|
238
|
+
const refreshPresets = useMemo(() => [
|
|
239
|
+
{ label: t('dashboards.refreshPresets.none', 'Désactivé'), value: null },
|
|
240
|
+
{ label: t('dashboards.refreshPresets.10s', '10 secondes'), value: 10 },
|
|
241
|
+
{ label: t('dashboards.refreshPresets.30s', '30 secondes'), value: 30 },
|
|
242
|
+
{ label: t('dashboards.refreshPresets.1m', '1 minute'), value: 60 },
|
|
243
|
+
{ label: t('dashboards.refreshPresets.5m', '5 minutes'), value: 300 },
|
|
244
|
+
{ label: t('dashboards.refreshPresets.15m', '15 minutes'), value: 900 },
|
|
245
|
+
], [t]);
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if( chartToAdd ){
|
|
249
|
+
// Ouvre le modal de configuration avec les données du graphique de l'IA
|
|
250
|
+
setChartToConfigure(chartToAdd);
|
|
251
|
+
setIsChartModalOpen(true);
|
|
252
|
+
setChartToAdd(null); // Réinitialise le contexte pour éviter de rouvrir le modal
|
|
253
|
+
}
|
|
254
|
+
}, [chartToAdd, setChartToAdd, selectedDashboard]);
|
|
255
|
+
|
|
256
|
+
// NOUVEAU: Gérer l'ajout d'une vue flexible depuis le contexte UI
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (flexViewToAdd && selectedDashboard) {
|
|
259
|
+
handleSaveFlexView(flexViewToAdd);
|
|
260
|
+
setFlexViewToAdd(null); // Réinitialiser le contexte
|
|
261
|
+
}
|
|
262
|
+
}, [flexViewToAdd, setFlexViewToAdd, selectedDashboard]);
|
|
263
|
+
|
|
264
|
+
// Gérer l'ajout d'une vue HTML depuis le contexte UI
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (htmlViewToAdd && selectedDashboard) {
|
|
267
|
+
handleSaveHtmlView(htmlViewToAdd);
|
|
268
|
+
setHtmlViewToAdd(null); // Réinitialiser le contexte
|
|
269
|
+
}
|
|
270
|
+
}, [htmlViewToAdd, setHtmlViewToAdd, selectedDashboard]);
|
|
271
|
+
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
// When a new dashboard is selected, close the editing form
|
|
274
|
+
setIsEditing(false);
|
|
275
|
+
}, [selectedDashboardId]);
|
|
276
|
+
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
// When the edit form is opened, initialize it with the selected dashboard's data
|
|
279
|
+
if (isEditing && selectedDashboard) {
|
|
280
|
+
setDashboardToEdit({ ...selectedDashboard });
|
|
281
|
+
}
|
|
282
|
+
}, [isEditing]);
|
|
283
|
+
|
|
284
|
+
// 3. Ce useEffect trouve le bon dashboard quand le hash ou la liste change
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
if (dashboardsData?.data.length > 0) {
|
|
287
|
+
if( hash ) {
|
|
288
|
+
const foundDashboard = dashboardsData?.data.find(d => d._hash+'' === hash);
|
|
289
|
+
setSelectedDashboardId(foundDashboard?._id || null); // Met à jour avec le dashboard trouvé ou null
|
|
290
|
+
}else{
|
|
291
|
+
const defaultDashboard = dashboardsData?.data.find(db => db.isDefault === true);
|
|
292
|
+
if( !defaultDashboard) {
|
|
293
|
+
setSelectedDashboardId(dashboardsData.data[0]._id);
|
|
294
|
+
}else
|
|
295
|
+
setSelectedDashboardId(defaultDashboard._id || null);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
setIsLoading(false);
|
|
299
|
+
}, [hash,dashboardsData]);
|
|
300
|
+
|
|
301
|
+
const dashboardOptions = useMemo(() => {
|
|
302
|
+
return (dashboardsData?.data || []).map(db => ({
|
|
303
|
+
label: db.name + (db.isDefault ? ` (${t('dashboards.default', 'Défaut')})` : ''),
|
|
304
|
+
value: db._id
|
|
305
|
+
}));
|
|
306
|
+
}, [dashboardsData, t]);
|
|
307
|
+
|
|
308
|
+
const nav = useNavigate();
|
|
309
|
+
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
setSelectedModel(null)
|
|
312
|
+
}, []);
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if (isLoading) {
|
|
316
|
+
return <div>{t('loading', 'Chargement...')}</div>;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!selectedDashboardId && hash) {
|
|
320
|
+
return (
|
|
321
|
+
<div className="dashboard-not-found">
|
|
322
|
+
<h2>{t('dashboards.notFound', 'Dashboard non trouvé')}</h2>
|
|
323
|
+
<p>{t('dashboards.notFoundHelp', 'Le dashboard avec l\'identifiant "{{0}}" n\'existe pas ou vous n\'y avez pas accès.',[hash] )}</p>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const creationForm = (
|
|
329
|
+
<form onSubmit={handleCreateDashboard} className="create-dashboard-form flex flex-start mg-v-1">
|
|
330
|
+
<input
|
|
331
|
+
type="text"
|
|
332
|
+
className="input-fit"
|
|
333
|
+
value={newDashboardName}
|
|
334
|
+
onChange={(e) => setNewDashboardName(e.target.value)}
|
|
335
|
+
placeholder={t('dashboards.newName', 'Nom du nouveau tableau de bord')}
|
|
336
|
+
disabled={createDashboardMutation.isLoading}
|
|
337
|
+
autoFocus
|
|
338
|
+
/>
|
|
339
|
+
<button type="submit" className="btn" disabled={createDashboardMutation.isLoading || !newDashboardName.trim()}>
|
|
340
|
+
{createDashboardMutation.isLoading
|
|
341
|
+
? <><FaSpinner className="spin" /> <Trans i18nKey="creating">Création...</Trans></>
|
|
342
|
+
: <Trans i18nKey="btns.create">Créer</Trans>
|
|
343
|
+
}
|
|
344
|
+
</button>
|
|
345
|
+
{/* Bouton pour annuler quand on a cliqué sur le "+" */}
|
|
346
|
+
{isCreating && (
|
|
347
|
+
<button type="button" className="btn btn-secondary" onClick={() => setIsCreating(false)}>
|
|
348
|
+
<Trans i18nKey="btns.cancel">Annuler</Trans>
|
|
349
|
+
</button>
|
|
350
|
+
)}
|
|
351
|
+
</form>
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<div className="dashboards-page">
|
|
356
|
+
<div className={"flex right actions"}>
|
|
357
|
+
<Button onClick={() => {
|
|
358
|
+
nav("/user/"+getUserHash(me)+"/?model="+(selectedModel?.name||'dashboard'));
|
|
359
|
+
}}><FaNoteSticky /> <Trans i18nKey={"models"}>Modèles</Trans></Button>
|
|
360
|
+
</div>
|
|
361
|
+
<h1>{t('dashboards.title', 'Mes Tableaux de Bord')}</h1>
|
|
362
|
+
|
|
363
|
+
{isLoadingDashboards && (
|
|
364
|
+
<p><FaSpinner className="spin" /> <Trans i18nKey="dashboards.loadingList">Chargement des tableaux de bord...</Trans></p>
|
|
365
|
+
)}
|
|
366
|
+
|
|
367
|
+
{errorDashboards && (
|
|
368
|
+
<p className="error">{errorDashboards.message}</p>
|
|
369
|
+
)}
|
|
370
|
+
|
|
371
|
+
{!isLoadingDashboards && !errorDashboards && (
|
|
372
|
+
<>
|
|
373
|
+
{dashboardsData?.data && dashboardsData.data.length > 0 ? (
|
|
374
|
+
<div className="dashboard-selector-container">
|
|
375
|
+
<div className="dashboard-selector flex" style={{ alignItems: 'flex-end', gap: '8px' }}>
|
|
376
|
+
<SelectField
|
|
377
|
+
name="dashboardSelection"
|
|
378
|
+
label={t('dashboards.select', 'Choisir un tableau de bord :')}
|
|
379
|
+
value={selectedDashboardId}
|
|
380
|
+
onChange={(selectedOption) => {
|
|
381
|
+
setSelectedDashboardId(selectedOption.value);
|
|
382
|
+
const d = dashboardsData.data.find(f => f._id === selectedOption.value);
|
|
383
|
+
history.pushState({}, null, '/user/'+getUserHash(me)+'/dashboards/'+d._hash);
|
|
384
|
+
}}
|
|
385
|
+
items={dashboardOptions}
|
|
386
|
+
/>
|
|
387
|
+
<button onClick={() => { setIsCreating(true); setIsEditing(false); }} className="btn" title={t('dashboards.add', 'Ajouter un tableau de bord')}>
|
|
388
|
+
<FaPlus />
|
|
389
|
+
</button>
|
|
390
|
+
{selectedDashboard && (
|
|
391
|
+
<button onClick={() => { setIsEditing(true); setIsCreating(false); }} className="btn" title={t('dashboards.configure', 'Configurer le tableau de bord')}>
|
|
392
|
+
<FaCog />
|
|
393
|
+
</button>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
{isCreating && creationForm}
|
|
397
|
+
{isEditing && dashboardToEdit && (
|
|
398
|
+
<form onSubmit={handleUpdateDashboard} className="edit-dashboard-form flex flex-col flex-start mg-v-1 pd-1" style={{ border: '1px solid #ccc', borderRadius: '4px' }}>
|
|
399
|
+
<h3 className="mg-b-1">{t('dashboards.configure', 'Configurer le tableau de bord')}</h3>
|
|
400
|
+
<TextField
|
|
401
|
+
name="name"
|
|
402
|
+
label={t('dashboards.dashboardName', 'Nom du tableau de bord')}
|
|
403
|
+
value={dashboardToEdit.name}
|
|
404
|
+
onChange={handleEditFieldChange}
|
|
405
|
+
required
|
|
406
|
+
autoFocus
|
|
407
|
+
/>
|
|
408
|
+
|
|
409
|
+
<div className="flex" style={{ gap: '1rem', alignItems: 'flex-end', width: '100%' }}>
|
|
410
|
+
<div style={{ flexGrow: 1 }}>
|
|
411
|
+
<DurationField
|
|
412
|
+
name="refreshInterval"
|
|
413
|
+
label={t('dashboards.refreshInterval', 'Intervalle de rafraîchissement')}
|
|
414
|
+
value={dashboardToEdit.refreshInterval}
|
|
415
|
+
onChange={handleEditFieldChange}
|
|
416
|
+
help={t('dashboards.refreshIntervalHelp', 'Laisser vide pour désactiver le rafraîchissement automatique. La valeur est en secondes.')}
|
|
417
|
+
/>
|
|
418
|
+
</div>
|
|
419
|
+
<SelectField
|
|
420
|
+
name="refreshIntervalPreset"
|
|
421
|
+
label={t('dashboards.refreshPresets.label', 'Préréglages')}
|
|
422
|
+
value={dashboardToEdit.refreshInterval}
|
|
423
|
+
onChange={(option) => {
|
|
424
|
+
console.log({option});
|
|
425
|
+
if (option?.value)
|
|
426
|
+
handleEditFieldChange({ name: 'refreshInterval', value: option?.value })
|
|
427
|
+
}}
|
|
428
|
+
items={refreshPresets}
|
|
429
|
+
style={{ minWidth: '150px' }}
|
|
430
|
+
/>
|
|
431
|
+
</div>
|
|
432
|
+
<div className="flex mg-t-1" style={{ gap: '8px' }}>
|
|
433
|
+
<button type="submit" className="btn" disabled={updateDashboardMutation.isLoading}>
|
|
434
|
+
{updateDashboardMutation.isLoading ? <><FaSpinner className="spin" /> <Trans i18nKey="btns.saving">Enregistrement...</Trans></> : <Trans i18nKey="btns.save">Enregistrer</Trans>}
|
|
435
|
+
</button>
|
|
436
|
+
<button type="button" className="btn btn-secondary" onClick={() => setIsEditing(false)}>
|
|
437
|
+
<Trans i18nKey="btns.cancel">Annuler</Trans>
|
|
438
|
+
</button>
|
|
439
|
+
</div>
|
|
440
|
+
</form>
|
|
441
|
+
)}
|
|
442
|
+
</div>
|
|
443
|
+
) : (
|
|
444
|
+
<>
|
|
445
|
+
<p><Trans i18nKey="dashboards.noDashboards">Aucun tableau de bord trouvé. Vous pouvez en créer un.</Trans></p>
|
|
446
|
+
{creationForm}
|
|
447
|
+
</>
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
{/* Affiche la vue du dashboard sélectionné */}
|
|
451
|
+
{/* Passe l'objet dashboard complet */}
|
|
452
|
+
{/*
|
|
453
|
+
TODO in DashboardView.jsx:
|
|
454
|
+
Utiliser la nouvelle propriété `dashboard.refreshInterval` (en secondes).
|
|
455
|
+
Si `refreshInterval` est défini et supérieur à 0, les données du dashboard (KPIs, graphiques)
|
|
456
|
+
doivent être rafraîchies automatiquement à cet intervalle.
|
|
457
|
+
Exemple avec react-query: `useQuery(queryKey, queryFn, { refetchInterval: dashboard.refreshInterval * 1000 })`
|
|
458
|
+
ou avec useEffect/setInterval:
|
|
459
|
+
useEffect(() => {
|
|
460
|
+
if (dashboard?.refreshInterval > 0) {
|
|
461
|
+
const intervalId = setInterval(() => {
|
|
462
|
+
// logique de rafraîchissement, ex: queryClient.invalidateQueries(...)
|
|
463
|
+
}, dashboard.refreshInterval * 1000);
|
|
464
|
+
return () => clearInterval(intervalId);
|
|
465
|
+
}
|
|
466
|
+
}, [dashboard?.refreshInterval, queryClient]);
|
|
467
|
+
*/}
|
|
468
|
+
<DashboardView dashboard={selectedDashboard} />
|
|
469
|
+
|
|
470
|
+
{/* Modal pour ajouter/éditer un graphique */}
|
|
471
|
+
<DialogProvider><ChartConfigModal
|
|
472
|
+
isOpen={isChartModalOpen}
|
|
473
|
+
onClose={() => setIsChartModalOpen(false)}
|
|
474
|
+
onSave={handleSaveChart}
|
|
475
|
+
initialConfig={chartToConfigure}
|
|
476
|
+
/></DialogProvider>
|
|
477
|
+
</>
|
|
478
|
+
)}
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
481
|
}
|