@iotready/nextjs-components-library 1.0.0-preview2 → 1.0.0-preview20
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/components/charts/TrendChart.d.ts +15 -5
- package/components/charts/TrendChart.js +287 -125
- package/components/groups/GroupUpdate.d.ts +1 -1
- package/components/groups/GroupUpdate.js +1 -1
- package/components/groups/GroupsDevices.d.ts +2 -2
- package/components/groups/GroupsDevices.js +27 -13
- package/package.json +3 -2
- package/server-actions/annotations.d.ts +4 -0
- package/server-actions/annotations.js +15 -0
- package/server-actions/groups.d.ts +1 -8
- package/server-actions/index.d.ts +1 -0
- package/server-actions/index.js +1 -0
- package/server-actions/influx.d.ts +4 -13
- package/server-actions/influx.js +155 -97
- package/server-actions/trackle.d.ts +3 -2
- package/server-actions/trackle.js +4 -17
- package/server-actions/types.d.ts +19 -0
- package/server-actions/types.js +1 -0
- package/types/user.d.ts +1 -0
@@ -1,17 +1,27 @@
|
|
1
1
|
import 'chartjs-adapter-moment';
|
2
2
|
import 'moment/locale/it';
|
3
3
|
import { Theme } from "@emotion/react";
|
4
|
+
import { InfluxFillType } from '../../server-actions/types';
|
4
5
|
type Measure = {
|
5
6
|
name: string;
|
7
|
+
description: string;
|
6
8
|
polltime: number;
|
7
9
|
unit: string;
|
10
|
+
stepped: boolean;
|
8
11
|
};
|
9
|
-
declare const TrendChart: ({ deviceId,
|
12
|
+
declare const TrendChart: ({ deviceId, measures1, annotationsData, measures2, enableDatePicker, handleGetInfluxData, handleExportDataCB, theme, initialTimeStart, initialTimeEnd, dwEnabled, dwCallback, dwHandled }: {
|
10
13
|
deviceId: string;
|
11
|
-
|
12
|
-
|
14
|
+
annotationsData?: Array<[any, any]>;
|
15
|
+
measures1?: Array<Measure>;
|
16
|
+
measures2?: Array<Measure>;
|
13
17
|
enableDatePicker: boolean;
|
14
|
-
handleGetInfluxData: (measure: string, timeStart: number, timeEnd: number, deviceId: string, timeGroup: string, raw: boolean) => Promise<any>;
|
18
|
+
handleGetInfluxData: (measure: string, timeStart: number, timeEnd: number, deviceId: string, timeGroup: string, raw: boolean, fill?: InfluxFillType) => Promise<any>;
|
19
|
+
handleExportDataCB?: (measure: string, timeStart: number, timeEnd: number, deviceId: string) => Promise<any>;
|
15
20
|
theme: Theme;
|
16
|
-
|
21
|
+
initialTimeStart?: number;
|
22
|
+
initialTimeEnd?: number;
|
23
|
+
dwEnabled?: boolean;
|
24
|
+
dwCallback?: (val: boolean) => void;
|
25
|
+
dwHandled?: boolean;
|
26
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
17
27
|
export default TrendChart;
|
@@ -18,6 +18,10 @@ import 'moment/locale/it';
|
|
18
18
|
// import 'moment/locale/en-gb'; // TODO set locale based on browser
|
19
19
|
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
|
20
20
|
import { ThemeProvider } from '@mui/material/styles';
|
21
|
+
import TimelineIcon from '@mui/icons-material/Timeline';
|
22
|
+
import MuiTooltip from '@mui/material/Tooltip';
|
23
|
+
import EditNoteIcon from '@mui/icons-material/EditNote';
|
24
|
+
import AspectRatioIcon from '@mui/icons-material/AspectRatio';
|
21
25
|
const lineOptions = {
|
22
26
|
parsing: false,
|
23
27
|
normalized: true,
|
@@ -35,7 +39,7 @@ const lineOptions = {
|
|
35
39
|
tooltip: {
|
36
40
|
callbacks: {
|
37
41
|
label: (context) => {
|
38
|
-
return `${context.dataset.label}: ${context.parsed.y.toFixed(
|
42
|
+
return `${context.dataset.label}: ${context.parsed.y.toFixed(3)} ${context.dataset.unit}`;
|
39
43
|
}
|
40
44
|
},
|
41
45
|
},
|
@@ -62,7 +66,19 @@ const lineOptions = {
|
|
62
66
|
day: 'DD MMM YY',
|
63
67
|
hour: 'DD MMM HH:mm',
|
64
68
|
minute: 'HH:mm'
|
65
|
-
}
|
69
|
+
},
|
70
|
+
y1: {
|
71
|
+
type: 'linear',
|
72
|
+
position: 'left',
|
73
|
+
title: { display: false, text: 'Primary Axis' },
|
74
|
+
display: false
|
75
|
+
},
|
76
|
+
y2: {
|
77
|
+
type: 'linear',
|
78
|
+
position: 'right',
|
79
|
+
title: { display: false, text: 'Secondary Axis' },
|
80
|
+
display: false
|
81
|
+
},
|
66
82
|
},
|
67
83
|
title: {
|
68
84
|
display: false,
|
@@ -74,15 +90,6 @@ const lineOptions = {
|
|
74
90
|
drawOnChartArea: false,
|
75
91
|
}
|
76
92
|
},
|
77
|
-
y: {
|
78
|
-
ticks: {
|
79
|
-
callback: function (value) {
|
80
|
-
if (Math.floor(value) === value) {
|
81
|
-
return value;
|
82
|
-
}
|
83
|
-
}
|
84
|
-
}
|
85
|
-
},
|
86
93
|
},
|
87
94
|
};
|
88
95
|
const chartConfigByPeriod = {
|
@@ -119,7 +126,7 @@ const chartConfigByPeriod = {
|
|
119
126
|
scaleUnit: 'year',
|
120
127
|
}
|
121
128
|
};
|
122
|
-
function
|
129
|
+
function getChartPoints(data) {
|
123
130
|
const points = data.results[0].series[0].values.map((row) => {
|
124
131
|
return {
|
125
132
|
x: moment.unix(row[0]),
|
@@ -139,7 +146,7 @@ function getPollTime(intervalInSeconds, pollTime) {
|
|
139
146
|
}
|
140
147
|
function getCsvData(data, measures) {
|
141
148
|
// Initialize the header with timestamp and measure names
|
142
|
-
const headers = ["timestamp", ...measures.map(measure => measure.name)];
|
149
|
+
const headers = ["timestamp", ...measures.map(measure => measure.description || measure.name)];
|
143
150
|
const csvData = [];
|
144
151
|
// Add the header to the csvData
|
145
152
|
csvData.push(headers);
|
@@ -160,8 +167,11 @@ function getCsvData(data, measures) {
|
|
160
167
|
});
|
161
168
|
});
|
162
169
|
});
|
163
|
-
//
|
164
|
-
Object.
|
170
|
+
// Sort timestamps in ascending order
|
171
|
+
const sortedTimestamps = Object.keys(timestampMap).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
|
172
|
+
// Create rows from sorted timestampMap
|
173
|
+
sortedTimestamps.forEach(timestamp => {
|
174
|
+
const entry = timestampMap[timestamp];
|
165
175
|
const row = [entry.timestamp];
|
166
176
|
measures.forEach(measure => {
|
167
177
|
// Push the corresponding value or an empty string if undefined
|
@@ -169,7 +179,7 @@ function getCsvData(data, measures) {
|
|
169
179
|
});
|
170
180
|
// Check if the row contains only empty values (besides the timestamp)
|
171
181
|
const hasNonEmptyValues = row.slice(1).some(value => value !== null && value !== '' && value !== undefined);
|
172
|
-
// If the row has at least one non-empty value
|
182
|
+
// If the row has at least one non-empty value, add it to csvData
|
173
183
|
if (hasNonEmptyValues || row.length === 1) {
|
174
184
|
csvData.push(row);
|
175
185
|
}
|
@@ -178,19 +188,27 @@ function getCsvData(data, measures) {
|
|
178
188
|
return csvData.map(row => row.join(',')).join('\n');
|
179
189
|
}
|
180
190
|
// eslint-disable-next-line no-unused-vars
|
181
|
-
const TrendChart = ({ deviceId,
|
191
|
+
const TrendChart = ({ deviceId, measures1, annotationsData, measures2, enableDatePicker, handleGetInfluxData, handleExportDataCB, theme, initialTimeStart, initialTimeEnd, dwEnabled, dwCallback, dwHandled }) => {
|
182
192
|
const [chartJsLoaded, setChartJsLoaded] = useState(false);
|
193
|
+
const [annotentionsEnabled, setAnnotationsEnabled] = useState(true);
|
183
194
|
const [dataMeasures, setDataMeasures] = useState(null);
|
184
195
|
const [chartPeriod, setChartPeriod] = useState('1D');
|
185
196
|
const [chartPeriodConfig, setChartPeriodConfig] = useState(chartConfigByPeriod['1D']);
|
186
197
|
const [chartLoading, setChartLoading] = useState(false);
|
187
|
-
const [
|
188
|
-
const [
|
189
|
-
const [
|
190
|
-
const [datePickerUsed, setDatePickerUsed] = useState(false);
|
198
|
+
const [timeStartPicker, setTimeStartPicker] = useState(initialTimeStart || moment().subtract(1, 'day').unix());
|
199
|
+
const [timeStart, setTimeStart] = useState(initialTimeStart || moment().subtract(1, 'day').unix());
|
200
|
+
const [timeEnd, setTimeEnd] = useState(initialTimeEnd || moment().unix());
|
201
|
+
const [datePickerUsed, setDatePickerUsed] = useState(initialTimeStart || initialTimeEnd ? true : false);
|
202
|
+
const [pickerTimeStart, setPickerTimeStart] = useState(initialTimeStart || moment().subtract(1, 'day').unix());
|
203
|
+
const [pickerTimeEnd, setPickerTimeEnd] = useState(initialTimeEnd || moment().unix());
|
191
204
|
const [loadingButton, setLoadingButton] = useState(false);
|
192
205
|
const csvLinkRef = useRef(null);
|
193
206
|
const [csvData, setCsvData] = useState('');
|
207
|
+
const [spanGapsOption, setSpanGapsOption] = useState(true);
|
208
|
+
const enableExportData = handleExportDataCB ? true : false;
|
209
|
+
const measures = measures1 && measures2 ? [...measures1, ...measures2] :
|
210
|
+
measures1 ? [...measures1] :
|
211
|
+
measures2 ? [...measures2] : [];
|
194
212
|
const [options, setOptions] = useState({
|
195
213
|
...lineOptions,
|
196
214
|
plugins: {
|
@@ -214,13 +232,19 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
|
|
214
232
|
}
|
215
233
|
});
|
216
234
|
const [zoomed, setZoomed] = useState(false);
|
235
|
+
const prevMeasures = useRef();
|
217
236
|
const resetChart = () => {
|
218
237
|
setOptions({
|
219
238
|
...options,
|
220
239
|
scales: {
|
221
240
|
...options.scales,
|
222
|
-
|
223
|
-
...options.scales.
|
241
|
+
y1: {
|
242
|
+
...options.scales.y1,
|
243
|
+
min: 0,
|
244
|
+
max: 1
|
245
|
+
},
|
246
|
+
y2: {
|
247
|
+
...options.scales.y2,
|
224
248
|
min: 0,
|
225
249
|
max: 1
|
226
250
|
}
|
@@ -229,34 +253,44 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
|
|
229
253
|
setDataMeasures([...[]]);
|
230
254
|
setCsvData("");
|
231
255
|
};
|
256
|
+
const handleSpanGaps = (spanG) => {
|
257
|
+
setOptions({
|
258
|
+
...options,
|
259
|
+
spanGaps: spanG,
|
260
|
+
});
|
261
|
+
setSpanGapsOption(spanG);
|
262
|
+
};
|
232
263
|
const handleChange = (event, newPeriod) => {
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
264
|
+
if (newPeriod && newPeriod !== chartPeriod) {
|
265
|
+
setChartLoading(true);
|
266
|
+
setZoomed(false);
|
267
|
+
setDatePickerUsed(false);
|
268
|
+
setCsvData('');
|
269
|
+
if (newPeriod === "ALL") {
|
270
|
+
setTimeStart(1577854800);
|
271
|
+
setTimeEnd(moment().unix());
|
272
|
+
setChartPeriod(newPeriod);
|
273
|
+
return;
|
274
|
+
}
|
275
|
+
const periodConfig = chartConfigByPeriod[newPeriod];
|
276
|
+
if (periodConfig) {
|
277
|
+
setTimeStart(Math.round(moment().subtract(periodConfig.from).valueOf() / 1000));
|
278
|
+
setTimeEnd(Math.round(moment().valueOf() / 1000));
|
279
|
+
setChartPeriod(newPeriod);
|
280
|
+
setChartPeriodConfig(periodConfig);
|
281
|
+
}
|
248
282
|
}
|
249
283
|
};
|
250
284
|
const handleExportData = async () => {
|
251
285
|
setLoadingButton(true);
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
286
|
+
if (handleExportDataCB) {
|
287
|
+
const data = await Promise.all(measures.map(async (measure) => {
|
288
|
+
return await handleExportDataCB(measure.name, timeStart, timeEnd, deviceId);
|
289
|
+
}));
|
290
|
+
const csvData = getCsvData(data, measures);
|
291
|
+
setCsvData(csvData);
|
292
|
+
setLoadingButton(false);
|
293
|
+
}
|
260
294
|
};
|
261
295
|
useEffect(() => {
|
262
296
|
if (csvData.length > 0) {
|
@@ -264,80 +298,174 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
|
|
264
298
|
}
|
265
299
|
}, [csvData]);
|
266
300
|
const loadDatasets = async (chartPeriod) => {
|
267
|
-
|
268
|
-
const
|
269
|
-
//
|
270
|
-
const
|
271
|
-
|
272
|
-
|
273
|
-
|
301
|
+
let intervalInSeconds = chartPeriod === "ALL" && !datePickerUsed && !zoomed ? 31536000 : timeEnd - timeStart;
|
302
|
+
const rawQuery = intervalInSeconds < 86400;
|
303
|
+
// Combine measures and track their source
|
304
|
+
const allMeasures = [
|
305
|
+
...(measures1?.map(m => ({ ...m, source: 'measures1' })) || []),
|
306
|
+
...(measures2?.map(m => ({ ...m, source: 'measures2' })) || [])
|
307
|
+
];
|
308
|
+
const datasetsPromises = allMeasures.map(async (measure) => {
|
309
|
+
const polltime = getPollTime(intervalInSeconds, measure.polltime || 30);
|
310
|
+
const influxData = await handleGetInfluxData(measure.name, timeStart, timeEnd, deviceId, polltime, !measure.polltime && rawQuery, dwEnabled ? "previous" : !measure.polltime ? "none" : "null");
|
311
|
+
const points = getChartPoints(influxData);
|
274
312
|
return {
|
275
|
-
label: measure.name,
|
313
|
+
label: measure.description || measure.name,
|
276
314
|
data: points,
|
277
315
|
unit: measure.unit,
|
278
|
-
// borderColor: `hsl(${index * 50}, 70%, 50%)`, // Colore unico per ogni dataset
|
279
316
|
borderWidth: 2,
|
280
317
|
pointRadius: 1,
|
281
318
|
pointHoverRadius: 5,
|
282
319
|
pointHoverBackgroundColor: 'rgba(52, 125, 236, 0.5)',
|
283
|
-
|
284
|
-
|
320
|
+
change: !measure.polltime,
|
321
|
+
stepped: measure.stepped,
|
322
|
+
yAxisID: measure.source === 'measures1' ? 'y1' : 'y2'
|
285
323
|
};
|
286
324
|
});
|
287
|
-
// Risolvi tutte le promesse per popolare i dataset
|
288
325
|
const datasets = await Promise.all(datasetsPromises);
|
289
|
-
let
|
290
|
-
let
|
291
|
-
let
|
326
|
+
let min1 = null;
|
327
|
+
let max1 = null;
|
328
|
+
let min2 = null;
|
329
|
+
let max2 = null;
|
330
|
+
let time;
|
331
|
+
let minTime = null;
|
292
332
|
datasets.forEach(dataset => {
|
293
|
-
values =
|
333
|
+
const values = dataset.data.map((point) => point.y).filter((data) => data !== null);
|
334
|
+
if (chartPeriod === "ALL" && !datePickerUsed && !zoomed && values.length) {
|
335
|
+
time = dataset.data.filter((data) => data.y !== null).map((data) => data.x)[0];
|
336
|
+
}
|
294
337
|
const datasetMin = Math.min(...values);
|
295
338
|
const datasetMax = Math.max(...values);
|
296
|
-
if (
|
297
|
-
|
298
|
-
|
299
|
-
|
339
|
+
if (dataset.yAxisID === 'y1') {
|
340
|
+
if (min1 === null || datasetMin < min1)
|
341
|
+
min1 = datasetMin;
|
342
|
+
if (max1 === null || datasetMax > max1)
|
343
|
+
max1 = datasetMax;
|
344
|
+
}
|
345
|
+
else {
|
346
|
+
if (min2 === null || datasetMin < min2)
|
347
|
+
min2 = datasetMin;
|
348
|
+
if (max2 === null || datasetMax > max2)
|
349
|
+
max2 = datasetMax;
|
350
|
+
}
|
351
|
+
if (time && (minTime === null || time.unix() < minTime))
|
352
|
+
minTime = time.unix() - 86400;
|
300
353
|
});
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
const diff =
|
305
|
-
|
306
|
-
|
354
|
+
const getPaddedMinMax = (min, max) => {
|
355
|
+
if (min === null || max === null)
|
356
|
+
return { paddedMin: null, paddedMax: null };
|
357
|
+
const diff = ((max - min) * 0.2) < 0.1 ? 0.1 : (max - min) * 0.2;
|
358
|
+
return {
|
359
|
+
paddedMin: Math.floor((min - diff) * 10) / 10,
|
360
|
+
paddedMax: Math.ceil((max + diff) * 10) / 10
|
361
|
+
};
|
362
|
+
};
|
363
|
+
const { paddedMin: paddedMin1, paddedMax: paddedMax1 } = getPaddedMinMax(min1, max1);
|
364
|
+
const { paddedMin: paddedMin2, paddedMax: paddedMax2 } = getPaddedMinMax(min2, max2);
|
365
|
+
// Handle undefined/null annotationsData
|
366
|
+
let dynamicAnnotations = {};
|
367
|
+
if (Array.isArray(annotationsData) && annotationsData.length > 0 && annotentionsEnabled) {
|
368
|
+
dynamicAnnotations = annotationsData.reduce((acc, [timestamp, label], index) => {
|
369
|
+
const yVal = paddedMin1 !== null && paddedMin1 !== undefined
|
370
|
+
? paddedMin1 + 0.01 * (paddedMax1 - paddedMin1)
|
371
|
+
: paddedMin2 !== null && paddedMin2 !== undefined
|
372
|
+
? paddedMin2 + 0.01 * (paddedMax2 - paddedMin2)
|
373
|
+
: 0;
|
374
|
+
acc[`line${index}`] = {
|
375
|
+
type: 'line',
|
376
|
+
xMin: timestamp,
|
377
|
+
xMax: timestamp,
|
378
|
+
borderColor: 'rgba(255, 225, 0, 0.8)',
|
379
|
+
borderWidth: 2,
|
380
|
+
drawTime: 'afterDatasetsDraw',
|
381
|
+
label: {
|
382
|
+
content: label,
|
383
|
+
enabled: false
|
384
|
+
}
|
385
|
+
};
|
386
|
+
acc[`triangle${index}`] = {
|
387
|
+
type: 'point',
|
388
|
+
xValue: timestamp,
|
389
|
+
yValue: yVal,
|
390
|
+
backgroundColor: 'rgba(255, 225, 0, 0.8)',
|
391
|
+
pointStyle: 'triangle',
|
392
|
+
radius: 6,
|
393
|
+
rotation: 0,
|
394
|
+
tooltip: {
|
395
|
+
enabled: true,
|
396
|
+
callbacks: {
|
397
|
+
label: () => label
|
398
|
+
}
|
399
|
+
}
|
400
|
+
};
|
401
|
+
acc[`label${index}`] = {
|
402
|
+
type: 'label',
|
403
|
+
xValue: timestamp,
|
404
|
+
yValue: yVal + 0.1 * (((paddedMax1 ?? paddedMax2 ?? 0) - (paddedMax1 ?? paddedMax2 ?? 0))),
|
405
|
+
xAdjust: 0,
|
406
|
+
yAdjust: -20,
|
407
|
+
backgroundColor: 'rgba(245,245,245)',
|
408
|
+
borderColor: 'rgba(255, 225, 0, 0.8)',
|
409
|
+
borderWidth: 1,
|
410
|
+
content: [label],
|
411
|
+
textAlign: 'start',
|
412
|
+
font: {
|
413
|
+
size: 10,
|
414
|
+
weight: 'bold'
|
415
|
+
},
|
416
|
+
callout: {
|
417
|
+
display: false,
|
418
|
+
}
|
419
|
+
};
|
420
|
+
return acc;
|
421
|
+
}, {});
|
307
422
|
}
|
308
|
-
|
423
|
+
// 👇 Combina statiche + dinamiche
|
424
|
+
const annotations = {
|
425
|
+
// ...staticAnnotations,
|
426
|
+
...dynamicAnnotations
|
427
|
+
};
|
428
|
+
// 👇 Configurazione completa
|
429
|
+
setDataMeasures(datasets);
|
430
|
+
setTimeStartPicker(minTime || timeStart);
|
309
431
|
setOptions({
|
310
432
|
...options,
|
311
433
|
scales: {
|
312
434
|
...options.scales,
|
313
|
-
|
314
|
-
...options.scales.
|
315
|
-
min:
|
316
|
-
max:
|
435
|
+
y1: {
|
436
|
+
...options.scales.y1,
|
437
|
+
min: paddedMin1,
|
438
|
+
max: paddedMax1,
|
439
|
+
position: "left",
|
440
|
+
display: "auto"
|
441
|
+
},
|
442
|
+
y2: {
|
443
|
+
...options.scales.y2,
|
444
|
+
min: paddedMin2,
|
445
|
+
max: paddedMax2,
|
446
|
+
position: "right",
|
447
|
+
display: "auto"
|
317
448
|
},
|
318
449
|
x: {
|
319
450
|
...options.scales.x,
|
320
|
-
min: moment.unix(timeStart).toString(),
|
321
|
-
max: moment.unix(timeEnd).toString()
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
451
|
+
min: moment.unix(minTime || timeStart).toString(),
|
452
|
+
max: moment.unix(timeEnd).toString()
|
453
|
+
}
|
454
|
+
},
|
455
|
+
plugins: {
|
456
|
+
...options.plugins,
|
457
|
+
annotation: {
|
458
|
+
interaction: {
|
459
|
+
mode: 'nearest',
|
460
|
+
intersect: true
|
461
|
+
},
|
462
|
+
annotations
|
326
463
|
}
|
327
464
|
}
|
328
465
|
});
|
329
466
|
};
|
330
467
|
useEffect(() => {
|
331
|
-
const
|
332
|
-
const response = await handleGetFirstTimestamp(deviceId);
|
333
|
-
if (response) {
|
334
|
-
setFirstTimestamp(response);
|
335
|
-
}
|
336
|
-
};
|
337
|
-
fetchFirstTimestamp();
|
338
|
-
}, []);
|
339
|
-
useEffect(() => {
|
340
|
-
const timeDifference = Math.abs(moment(timeStart).valueOf() - moment(timeEnd).valueOf()); // Convert milliseconds to seconds
|
468
|
+
const timeDifference = Math.abs(moment(timeEnd).valueOf() - moment(timeStart).valueOf()); // Convert milliseconds to seconds
|
341
469
|
let newChartPeriod = '1D'; // Default to 1 day
|
342
470
|
if (timeDifference < 86400) { // Less than 1 day
|
343
471
|
newChartPeriod = '1H'; // Set to 1 day
|
@@ -345,26 +473,34 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
|
|
345
473
|
else if (timeDifference < 604800) { // Less than 1 week
|
346
474
|
newChartPeriod = '1D'; // Set to 1 day
|
347
475
|
}
|
348
|
-
else if (timeDifference <
|
476
|
+
else if (timeDifference < 2678400) { // Less than 1 month
|
349
477
|
newChartPeriod = '1W'; // Set to 1 week
|
350
478
|
}
|
351
|
-
else if (timeDifference <
|
479
|
+
else if (timeDifference < 8035200) { // Less than 3 months
|
352
480
|
newChartPeriod = '1M'; // Set to 1 month
|
353
481
|
}
|
354
|
-
else if (timeDifference <
|
482
|
+
else if (timeDifference < 16070400) { // Less than 6 months
|
355
483
|
newChartPeriod = '3M'; // Set to 3 months
|
356
484
|
}
|
357
|
-
else if (timeDifference <
|
485
|
+
else if (timeDifference < 31536000) { // Less than 1 year
|
358
486
|
newChartPeriod = '6M'; // Set to 6 months
|
359
487
|
}
|
360
488
|
else {
|
361
489
|
newChartPeriod = '1Y'; // Set to 1 year
|
362
490
|
}
|
363
491
|
setChartPeriodConfig(chartConfigByPeriod[newChartPeriod]);
|
492
|
+
// check prev measures value in order to show the loader
|
493
|
+
// hide the loader if measure is the same (for interval get measure value)
|
494
|
+
const prevMeasuresValue = prevMeasures.current || [];
|
495
|
+
//@ts-ignore
|
496
|
+
prevMeasures.current = measures;
|
497
|
+
if (!prevMeasuresValue || prevMeasuresValue.length !== measures?.length || (prevMeasuresValue[0] && prevMeasuresValue[0].name !== measures[0].name)) {
|
498
|
+
setChartLoading(true);
|
499
|
+
}
|
364
500
|
loadDatasets(chartPeriod).then(() => {
|
365
501
|
setChartLoading(false);
|
366
502
|
});
|
367
|
-
}, [timeEnd, timeStart]);
|
503
|
+
}, [measures1, timeEnd, timeStart, measures2, annotentionsEnabled, annotationsData, dwEnabled]);
|
368
504
|
useEffect(() => {
|
369
505
|
const loadZoomPlugin = async () => {
|
370
506
|
const zoomPlugin = (await import('chartjs-plugin-zoom')).default;
|
@@ -375,8 +511,24 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
|
|
375
511
|
const activePoint = chart.tooltip._active[0];
|
376
512
|
const ctx = chart.ctx;
|
377
513
|
const x = activePoint.element.x;
|
378
|
-
|
379
|
-
|
514
|
+
let topY1 = 0;
|
515
|
+
let topY2 = 0;
|
516
|
+
let bottomY1 = 0;
|
517
|
+
let bottomY2 = 0;
|
518
|
+
if (chart.scales.y1?.top) {
|
519
|
+
topY1 = chart.scales.y1.top;
|
520
|
+
}
|
521
|
+
if (chart.scales.y2?.top) {
|
522
|
+
topY2 = chart.scales.y2.top;
|
523
|
+
}
|
524
|
+
if (chart.scales.y1?.bottom) {
|
525
|
+
bottomY1 = chart.scales.y1.bottom;
|
526
|
+
}
|
527
|
+
if (chart.scales.y2?.bottom) {
|
528
|
+
bottomY2 = chart.scales.y2.bottom;
|
529
|
+
}
|
530
|
+
const topY = Math.max(topY1, topY2);
|
531
|
+
const bottomY = Math.min(bottomY1, bottomY2);
|
380
532
|
ctx.save();
|
381
533
|
ctx.beginPath();
|
382
534
|
ctx.moveTo(x, topY);
|
@@ -393,34 +545,41 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
|
|
393
545
|
loadZoomPlugin();
|
394
546
|
resetChart();
|
395
547
|
}, []);
|
396
|
-
const datePicker = _jsx(Box, { sx: { display: 'flex', alignItems: 'center', mr: { xs: 0, lg: 2 } }, children: _jsxs(LocalizationProvider, { dateAdapter: AdapterMoment, adapterLocale: "it", children: [_jsx(DateTimePicker, { value: moment(
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
548
|
+
const datePicker = _jsx(Box, { sx: { display: 'flex', alignItems: 'center', mr: { xs: 0, lg: 2 } }, children: _jsxs(LocalizationProvider, { dateAdapter: AdapterMoment, adapterLocale: "it", children: [_jsx(DateTimePicker, { value: moment(timeStartPicker * 1000), onChange: (newValue) => {
|
549
|
+
setChartLoading(true);
|
550
|
+
setDatePickerUsed(true);
|
551
|
+
setZoomed(false);
|
552
|
+
setPickerTimeStart(moment(newValue).unix());
|
553
|
+
setTimeStart(moment(newValue).unix());
|
554
|
+
}, maxDateTime: moment(timeEnd * 1000), slotProps: {
|
401
555
|
textField: { size: 'small', sx: { width: { sm: 210 } } }
|
402
|
-
} }), " \u00A0\u00A0 ", _jsx(Box, { children: "\u2013" }), " \u00A0\u00A0", _jsx(DateTimePicker, { value: moment(timeEnd * 1000),
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
556
|
+
} }), " \u00A0\u00A0 ", _jsx(Box, { children: "\u2013" }), " \u00A0\u00A0", _jsx(DateTimePicker, { value: moment(timeEnd * 1000), onChange: (newValue) => {
|
557
|
+
setChartLoading(true);
|
558
|
+
setDatePickerUsed(true);
|
559
|
+
setZoomed(false);
|
560
|
+
setPickerTimeEnd(moment(newValue).unix());
|
561
|
+
setTimeEnd(moment(newValue).unix());
|
562
|
+
}, minDateTime: moment(timeStart * 1000), slotProps: {
|
407
563
|
textField: { size: 'small', sx: { width: { sm: 210 } } }
|
408
564
|
} })] }) });
|
409
|
-
return (_jsxs(ThemeProvider, { theme: theme, children: [enableDatePicker && _jsx(Box, { sx: { display: { xs: 'flex', lg: 'none', justifyContent: 'flex-end' }, mb: 2 }, children: datePicker }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsxs(Box, { children: [enableExportData && (_jsxs(_Fragment, { children: [_jsxs(LoadingButton, { sx: { minWidth: '40px !important', mr: 1, px: 1 }, loading: loadingButton, type: "submit", variant: "text", color: "primary", onClick: handleExportData, disabled: !dataMeasures || !dataMeasures.length, size: 'small', children: [_jsx(LoginIcon, { fontSize: 'small', style: { transform: "rotate(90deg)" } }), _jsx(Box, { sx: { display: { xs: 'none', xl: 'block' }, ml: { md: 1 } }, children: "Export" })] }), _jsx(CSVLink
|
565
|
+
return (_jsxs(ThemeProvider, { theme: theme, children: [enableDatePicker && _jsx(Box, { sx: { display: { xs: 'flex', lg: 'none', justifyContent: 'flex-end' }, mb: 2 }, children: datePicker }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center' }, children: [!enableExportData && measures && measures.length === 1 ? _jsx(Typography, { variant: 'body1', sx: { mr: 2 }, children: measures[0].description || measures[0].name }) : '', enableExportData && (_jsxs(_Fragment, { children: [_jsxs(LoadingButton, { sx: { minWidth: '40px !important', mr: 1, px: 1 }, loading: loadingButton, type: "submit", variant: "text", color: "primary", onClick: handleExportData, disabled: !dataMeasures || !dataMeasures.length, size: 'small', children: [_jsx(LoginIcon, { fontSize: 'small', style: { transform: "rotate(90deg)" } }), _jsx(Box, { sx: { display: { xs: 'none', xl: 'block' }, ml: { md: 1 } }, children: "Export" })] }), _jsx(CSVLink
|
410
566
|
//@ts-ignore
|
411
567
|
, {
|
412
568
|
//@ts-ignore
|
413
|
-
ref: csvLinkRef, data: csvData, filename: `export_${moment().toISOString()}.csv`, separator: ';' })] })), zoomed && (_jsxs(Button, { sx: { minWidth: '40px !important', boxShadow: 1, px: 1 }, variant: "contained", color: "secondary", size: 'small', onClick: () => {
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
569
|
+
ref: csvLinkRef, data: csvData, filename: `export_${moment().toISOString()}.csv`, separator: ';' })] })), zoomed && measures.length > 0 && (_jsxs(Button, { sx: { minWidth: '40px !important', boxShadow: 1, px: 1 }, variant: "contained", color: "secondary", size: 'small', onClick: () => {
|
570
|
+
setChartLoading(true);
|
571
|
+
setZoomed(false);
|
572
|
+
if (chartPeriod === "ALL") {
|
573
|
+
setDatePickerUsed(false);
|
574
|
+
setTimeStart(1577854800);
|
419
575
|
setTimeEnd(moment().unix());
|
420
|
-
|
576
|
+
}
|
577
|
+
else if (datePickerUsed) {
|
578
|
+
setTimeStart(pickerTimeStart);
|
579
|
+
setTimeEnd(pickerTimeEnd);
|
421
580
|
}
|
422
581
|
else {
|
423
|
-
|
582
|
+
setDatePickerUsed(false);
|
424
583
|
setChartPeriodConfig(chartConfigByPeriod[chartPeriod]);
|
425
584
|
setTimeStart(chartPeriodConfig.from?.days
|
426
585
|
? moment()
|
@@ -429,12 +588,15 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
|
|
429
588
|
: moment().subtract(1, 'day').unix());
|
430
589
|
setTimeEnd(moment().unix());
|
431
590
|
}
|
432
|
-
}, children: [_jsx(ZoomOut, { fontSize: 'small' }), _jsx(Box, { sx: { display: { xs: 'none',
|
433
|
-
|
434
|
-
|
435
|
-
|
591
|
+
}, children: [_jsx(ZoomOut, { fontSize: 'small' }), _jsx(Box, { sx: { display: { xs: 'none', lg: 'block' }, ml: { md: 1 } }, children: "Reset" })] }))] }), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'flex-end' }, children: [enableDatePicker && _jsx(Box, { sx: { display: { xs: 'none', lg: 'flex' } }, children: datePicker }), _jsxs(ToggleButtonGroup, { color: "primary", value: !datePickerUsed && !zoomed ? chartPeriod : null, exclusive: true, onChange: handleChange, size: "small", sx: {
|
592
|
+
'& .MuiToggleButton-root': {
|
593
|
+
color: 'text.primary', fontSize: '0.95rem', fontWeight: 'normal', paddingTop: '6px', paddingBottom: '6px'
|
594
|
+
}
|
595
|
+
}, disabled: chartLoading, children: [_jsx(ToggleButton, { value: "1D", sx: { px: 1 }, children: "1d" }), _jsx(ToggleButton, { value: "1W", sx: { px: 1 }, children: "1w" }), _jsx(ToggleButton, { value: "1M", sx: { px: 1 }, children: "1M" }), _jsx(ToggleButton, { value: "3M", sx: { px: 1 }, children: "3M" }), _jsx(ToggleButton, { value: "6M", sx: { px: 1 }, children: "6M" }), _jsx(ToggleButton, { value: "1Y", sx: { px: 1 }, children: "1Y" }), _jsx(ToggleButton, { value: "ALL", sx: { px: 1 }, children: "ALL" })] }), _jsx(MuiTooltip, { placement: "top", arrow: true, title: "Connect point values", children: _jsx(ToggleButton, { value: "check", color: "primary", size: "small", selected: spanGapsOption, disabled: chartLoading, onChange: () => handleSpanGaps(!spanGapsOption), sx: { ml: 1 }, children: _jsx(TimelineIcon, {}) }) }), annotationsData !== undefined && (_jsx(MuiTooltip, { placement: "top", arrow: true, title: "Show annotations", children: _jsx(ToggleButton, { value: "check", color: "primary", size: "small", selected: annotentionsEnabled, disabled: chartLoading, onChange: () => setAnnotationsEnabled(!annotentionsEnabled), sx: { ml: 1 }, children: _jsx(EditNoteIcon, {}) }) })), dwCallback !== undefined && dwEnabled != undefined && (_jsx(MuiTooltip, { placement: "top", arrow: true, title: "Show data window data", children: _jsx(ToggleButton, { value: "check", color: "primary", size: "small", selected: dwEnabled, disabled: chartLoading || !dwHandled, onChange: () => dwCallback(!dwEnabled), sx: { ml: 1 }, children: _jsx(AspectRatioIcon, {}) }) }))] })] }), _jsx(Box, { component: 'div', className: "chart-container", sx: { mt: 2, height: '100%' }, children: chartJsLoaded && !chartLoading && typeof window !== 'undefined' ?
|
596
|
+
_jsx(_Fragment, { children: dataMeasures && (dataMeasures.length > 1 || (dataMeasures.length === 1 && dataMeasures[0].data?.length)) ?
|
436
597
|
(_jsx(Line, { options: options, data: {
|
437
|
-
datasets: dataMeasures || [{ data: [] }]
|
598
|
+
// datasets: dataMeasures || [{ data: [] }]
|
599
|
+
datasets: dataMeasures.map(d => ({ ...d, showLine: spanGapsOption || !d.change })) || [{ data: [] }]
|
438
600
|
} })) : _jsxs(Box, { sx: {
|
439
601
|
display: 'flex',
|
440
602
|
flexDirection: 'column',
|
@@ -448,7 +610,7 @@ const TrendChart = ({ deviceId, measures, enableExportData, enableDatePicker, ha
|
|
448
610
|
alignItems: 'center',
|
449
611
|
justifyContent: 'center',
|
450
612
|
textAlign: 'center',
|
451
|
-
height: '100%'
|
613
|
+
height: 'calc(100% - 50px)'
|
452
614
|
}, children: _jsx(CircularProgress, {}) })) })] }));
|
453
615
|
};
|
454
616
|
export default TrendChart;
|
@@ -12,7 +12,7 @@ declare const GroupUpdate: ({ userInfo, groupInfo, usersGroup, usersList, device
|
|
12
12
|
handleAddUserToGroup: (groupID: string, userName: string, userID: string) => Promise<any>;
|
13
13
|
handleRemoveUserFromGroup: (groupID: string, userID: string) => Promise<any>;
|
14
14
|
handleUpdateDevice: (id: string, body: any, user: UserType) => Promise<any>;
|
15
|
-
handleGetGroups: (
|
15
|
+
handleGetGroups: (userInfo?: UserType) => Promise<any>;
|
16
16
|
handleUpdateGroup: (id: string, group: any) => Promise<void>;
|
17
17
|
handleDeleteGroup: (id: string) => Promise<void>;
|
18
18
|
container?: "Box" | "Card";
|
@@ -27,7 +27,7 @@ const GroupUpdate = ({ userInfo, groupInfo, usersGroup, usersList, devicesList,
|
|
27
27
|
if (response) {
|
28
28
|
if (devicesList.length > 0) {
|
29
29
|
// should check if device is present in other groups the user is member of
|
30
|
-
const groupsData = await handleGetGroups(
|
30
|
+
const groupsData = await handleGetGroups({ role: 'support', uid: userId });
|
31
31
|
const userGroupIds = groupsData.map((group) => group.id);
|
32
32
|
// for all devices in the group set the selectedUser as manager
|
33
33
|
for (const device of devicesList) {
|
@@ -11,10 +11,10 @@ declare const GroupsDevices: ({ userInfo, handleGetUsersList, handleAddUserToGro
|
|
11
11
|
}>;
|
12
12
|
handleAddUserToGroup: (groupID: string, userName: string, userID: string) => Promise<any>;
|
13
13
|
handleRemoveUserFromGroup: (groupID: string, userID: string) => Promise<any>;
|
14
|
-
handleGetGroups: (
|
14
|
+
handleGetGroups: (userInfo?: UserType) => Promise<any>;
|
15
15
|
handleGetUsersGroup: (groupID: string) => Promise<any>;
|
16
16
|
handleCreateGroup: (group: any) => Promise<any>;
|
17
|
-
handleGetDevices: (user: UserType,
|
17
|
+
handleGetDevices: (user: UserType, query: string) => Promise<any>;
|
18
18
|
handleGetPositions: (devices: any) => Promise<any>;
|
19
19
|
handleAddDevicesToGroup: (user: UserType, group: string, devicesToPatch: any[]) => Promise<any>;
|
20
20
|
handleRemoveDevicesFromGroup: (user: UserType, group: string, devicesToPatch: any[]) => Promise<any>;
|
@@ -28,7 +28,7 @@ const GroupsDevices = ({ userInfo, handleGetUsersList, handleAddUserToGroup, han
|
|
28
28
|
const [selectedGroup, setSelectedGroup] = useState(group);
|
29
29
|
const [groups, setGroups] = useState(null);
|
30
30
|
const [usersList, setUsersList] = useState([]);
|
31
|
-
const [usersGroup, setUsersGroup] = useState(
|
31
|
+
const [usersGroup, setUsersGroup] = useState(null);
|
32
32
|
const [groupInfo, setGroupInfo] = useState(null);
|
33
33
|
const [openAdd, setOpenAdd] = useState(false);
|
34
34
|
const [groupName, setGroupName] = useState('');
|
@@ -60,7 +60,7 @@ const GroupsDevices = ({ userInfo, handleGetUsersList, handleAddUserToGroup, han
|
|
60
60
|
moreData = usersList.users.length === pageSize;
|
61
61
|
page++;
|
62
62
|
}
|
63
|
-
const userGroupsUserIds = usersGroup.map((ug) => ug.user.userId);
|
63
|
+
const userGroupsUserIds = usersGroup && usersGroup.map((ug) => ug.user.userId) || [];
|
64
64
|
setUsersList(usersData.filter((user) => user.role === 'support' && !userGroupsUserIds.includes(user.uid)).map((user) => {
|
65
65
|
const userName = user.name ? user.name.replace("/", " ") : "";
|
66
66
|
return { label: `${userName} (${user.email})`, id: user.uid };
|
@@ -73,7 +73,20 @@ const GroupsDevices = ({ userInfo, handleGetUsersList, handleAddUserToGroup, han
|
|
73
73
|
const fetchDevices = async (group, selected) => {
|
74
74
|
try {
|
75
75
|
setLoadingDevices(true);
|
76
|
-
|
76
|
+
let queryParams = "last_handshake_at!=null";
|
77
|
+
if (group !== "all") {
|
78
|
+
queryParams = `last_handshake_at!=null&state.groups=/${group}/`;
|
79
|
+
if (userInfo.role === "admin") {
|
80
|
+
queryParams += "&quarantined=false";
|
81
|
+
}
|
82
|
+
}
|
83
|
+
else if (selected !== "all") {
|
84
|
+
queryParams = `last_handshake_at!=null&state.groups!=/${selected}/`;
|
85
|
+
if (userInfo.role === "admin") {
|
86
|
+
queryParams += "&quarantined=false";
|
87
|
+
}
|
88
|
+
}
|
89
|
+
const devices = await handleGetDevices(userInfo, queryParams);
|
77
90
|
setDevices(devices);
|
78
91
|
if (enableMaps) {
|
79
92
|
const positions = await handleGetPositions(devices);
|
@@ -89,7 +102,7 @@ const GroupsDevices = ({ userInfo, handleGetUsersList, handleAddUserToGroup, han
|
|
89
102
|
};
|
90
103
|
const getGroups = async () => {
|
91
104
|
try {
|
92
|
-
const responseData = await handleGetGroups(
|
105
|
+
const responseData = await handleGetGroups(userInfo);
|
93
106
|
setGroups(responseData);
|
94
107
|
}
|
95
108
|
catch (err) {
|
@@ -119,7 +132,9 @@ const GroupsDevices = ({ userInfo, handleGetUsersList, handleAddUserToGroup, han
|
|
119
132
|
if (currentGroup && !checkboxSelection) {
|
120
133
|
setSelectedGroup(currentGroup);
|
121
134
|
setGroupInfo(groups && groups.find((g) => g.id === currentGroup));
|
122
|
-
|
135
|
+
if (currentGroup !== 'all') {
|
136
|
+
getUsersGroup(currentGroup);
|
137
|
+
}
|
123
138
|
fetchDevices(currentGroup, currentGroup);
|
124
139
|
}
|
125
140
|
}, [currentGroup]);
|
@@ -130,7 +145,6 @@ const GroupsDevices = ({ userInfo, handleGetUsersList, handleAddUserToGroup, han
|
|
130
145
|
const newGroup = {
|
131
146
|
name: groupName,
|
132
147
|
description,
|
133
|
-
productID: 1008, // Assuming this is the correct productID
|
134
148
|
};
|
135
149
|
const id = await handleCreateGroup(newGroup);
|
136
150
|
await getGroups();
|
@@ -177,13 +191,13 @@ const GroupsDevices = ({ userInfo, handleGetUsersList, handleAddUserToGroup, han
|
|
177
191
|
const devicesPatched = await handleAddDevicesToGroup(userInfo, selectedGroup, devicesToPatch);
|
178
192
|
if (devicesPatched && devicesPatched.length > 0) {
|
179
193
|
// get all members of the group
|
180
|
-
const userIdsToAdd = usersGroup.map((ug) => ug.user.userId);
|
181
|
-
if (userIdsToAdd.length > 0) {
|
194
|
+
const userIdsToAdd = usersGroup && usersGroup.map((ug) => ug.user.userId) || [];
|
195
|
+
if (userIdsToAdd && userIdsToAdd.length > 0) {
|
182
196
|
for (const device of devicesToPatch) {
|
183
197
|
// add members to devices managers
|
184
198
|
await handleUpdateDevice(device.id, {
|
185
199
|
managers: [
|
186
|
-
...device
|
200
|
+
...(device?.managers || []),
|
187
201
|
...userIdsToAdd
|
188
202
|
]
|
189
203
|
}, userInfo);
|
@@ -210,12 +224,12 @@ const GroupsDevices = ({ userInfo, handleGetUsersList, handleAddUserToGroup, han
|
|
210
224
|
const devicesPatched = await handleRemoveDevicesFromGroup(userInfo, selectedGroup, devicesToPatch);
|
211
225
|
if (devicesPatched && devicesPatched.length > 0) {
|
212
226
|
for (const device of devicesToPatch) {
|
213
|
-
if (device.managers.length > 0) {
|
214
|
-
let managersToKeep = device.managers
|
227
|
+
if (device.managers && device.managers.length > 0) {
|
228
|
+
let managersToKeep = device.managers;
|
215
229
|
for (const manager of device.managers) {
|
216
230
|
// should check if device is present in other groups the user is member of
|
217
|
-
const groupsData = await handleGetGroups(
|
218
|
-
const userGroupIds = groupsData.map((group) => group.id);
|
231
|
+
const groupsData = await handleGetGroups({ role: 'support', uid: manager });
|
232
|
+
const userGroupIds = groupsData && groupsData.map((group) => group.id) || [];
|
219
233
|
const found = device.groups && device.groups.some((gID) => userGroupIds.includes(gID));
|
220
234
|
if (!found) {
|
221
235
|
// @ts-ignore
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@iotready/nextjs-components-library",
|
3
|
-
"version": "1.0.0-
|
3
|
+
"version": "1.0.0-preview20",
|
4
4
|
"main": "index.js",
|
5
5
|
"scripts": {
|
6
6
|
"build": "rm -rf dist && tsc --project tsconfig.build.json && cp package.json dist/",
|
@@ -19,6 +19,7 @@
|
|
19
19
|
"chartjs-adapter-moment": "^1.0.1",
|
20
20
|
"chartjs-plugin-annotation": "^3.1.0",
|
21
21
|
"chartjs-plugin-zoom": "^2.0.1",
|
22
|
+
"csv-parse": "^5.6.0",
|
22
23
|
"firebase": "^10.13.1",
|
23
24
|
"leaflet": "^1.9.4",
|
24
25
|
"leaflet-defaulticon-compatibility": "^0.1.2",
|
@@ -42,4 +43,4 @@
|
|
42
43
|
"eslint-config-next": "14.2.9",
|
43
44
|
"typescript": "^5"
|
44
45
|
}
|
45
|
-
}
|
46
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"use server";
|
2
|
+
import { initializeApp } from "firebase/app";
|
3
|
+
import { collection, query, orderBy, getDocs, where } from "@firebase/firestore";
|
4
|
+
import { getFirestore } from "@firebase/firestore";
|
5
|
+
export const getAnnotations = async (firebaseConfig, deviceID) => {
|
6
|
+
// Initialize Firebase
|
7
|
+
const app = initializeApp(firebaseConfig);
|
8
|
+
const db = getFirestore(app);
|
9
|
+
const annotationsQuery = query(collection(db, "annotations"), where("target", "==", deviceID), orderBy("createdAt", "desc"));
|
10
|
+
const groupsSnapshot = await getDocs(annotationsQuery);
|
11
|
+
return groupsSnapshot.docs.map((doc) => ({
|
12
|
+
id: doc.id,
|
13
|
+
...doc.data()
|
14
|
+
}));
|
15
|
+
};
|
@@ -1,11 +1,4 @@
|
|
1
|
-
|
2
|
-
apiKey: string;
|
3
|
-
authDomain: string;
|
4
|
-
projectId: string;
|
5
|
-
storageBucket: string;
|
6
|
-
messagingSenderId: string;
|
7
|
-
appId: string;
|
8
|
-
};
|
1
|
+
import { FirebaseConfig } from "./types";
|
9
2
|
export declare const getGroups: (firebaseConfig: FirebaseConfig, productID: number, userID?: string) => Promise<{
|
10
3
|
id: string;
|
11
4
|
}[]>;
|
package/server-actions/index.js
CHANGED
@@ -1,13 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
orgId: string;
|
6
|
-
measurement: string;
|
7
|
-
dbName: string;
|
8
|
-
username: string;
|
9
|
-
password: string;
|
10
|
-
};
|
11
|
-
export declare function getInfluxDataV1(influxConfig: InfluxConfig, field: string, timeStart: number, timeEnd: number, deviceID: string, timeGroup: string, raw: boolean): Promise<any>;
|
12
|
-
export declare function getManyMeasuresV1(influxConfig: InfluxConfig, fields: string[], limit: number, offset: number, sort: any, deviceID: string, timeStart?: number, timeEnd?: number): Promise<any>;
|
13
|
-
export declare function getFirstTimestamp(influxConfig: InfluxConfig, deviceID: string): Promise<any>;
|
1
|
+
import { InfluxConfig, InfluxFillType } from "./types";
|
2
|
+
export declare function getInfluxAlerts(influxConfig: InfluxConfig, fields: string[], limit: number, offset: number, sort: any, deviceID: string, timeStart: number, timeEnd: number): Promise<any>;
|
3
|
+
export declare function getInfluxDataV1(influxConfig: InfluxConfig, field: string, timeStart: number, timeEnd: number, deviceID: string, timeGroup: string, raw: boolean, fill?: InfluxFillType, filterTag?: number): Promise<any>;
|
4
|
+
export declare function exportDataV1(influxConfig: InfluxConfig, field: string, timeStart: number, timeEnd: number, deviceID: string): Promise<any>;
|
package/server-actions/influx.js
CHANGED
@@ -1,55 +1,129 @@
|
|
1
1
|
"use server";
|
2
|
+
import { parse } from "csv-parse";
|
2
3
|
import moment from "moment";
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
export async function getInfluxAlerts(influxConfig, fields, limit, offset, sort, deviceID, timeStart, timeEnd) {
|
5
|
+
const conditions = fields
|
6
|
+
.map((field) => `r["valueName"] == "${field}"`)
|
7
|
+
.join(" or ");
|
8
|
+
let query = `
|
9
|
+
import "contrib/tomhollingworth/events"
|
10
|
+
from(bucket: "${influxConfig.bucket}")`;
|
11
|
+
if (timeStart && timeEnd) {
|
12
|
+
query += ` |> range(start: ${timeStart}, stop: ${timeEnd})`;
|
13
|
+
}
|
14
|
+
query += `
|
15
|
+
|> filter(fn: (r) => r["_measurement"] == "${influxConfig.measurement}" and r["deviceid"] == "${deviceID}" and (${conditions}))
|
16
|
+
`;
|
17
|
+
// Add filter for tag d === 3
|
18
|
+
query += `
|
19
|
+
|> filter(fn: (r) => r["d"] == "3" or r["d"] !~ /./) // Include only events with tag d == 3 or no tag d
|
12
20
|
`;
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
})
|
24
|
-
});
|
25
|
-
if (!response.ok) {
|
26
|
-
throw new Error(`Failed to fetch data from InfluxDB: ${response.statusText}`);
|
27
|
-
}
|
28
|
-
const data = await response.text();
|
29
|
-
return data.split('\n').map(line => {
|
30
|
-
const row = line.split(',');
|
31
|
-
const timestamp = row[5];
|
32
|
-
const value = parseFloat(row[6]);
|
33
|
-
console.log(timestamp, value);
|
34
|
-
if (isNaN(value)) {
|
35
|
-
return null;
|
21
|
+
const queryCount = `${query} |> group() |> count()`;
|
22
|
+
query += `
|
23
|
+
|> sort(columns: ["_time"]) // Ordina gli eventi cronologicamente
|
24
|
+
|> group(columns: ["valueName"]) // Raggruppa per il tag
|
25
|
+
|> events.duration(unit: 1s, stop: 2020-01-02T00:00:00Z)
|
26
|
+
|> keep(columns: ["_time", "valueName", "_value", "duration"])
|
27
|
+
|> group() // Raggruppa tutti i dati in un unico gruppo
|
28
|
+
`;
|
29
|
+
if (sort && sort.field === "time" && sort.sort === "desc") {
|
30
|
+
query += ` |> sort(columns: ["_time"], desc: true)`;
|
36
31
|
}
|
37
|
-
|
38
|
-
|
39
|
-
}
|
40
|
-
|
32
|
+
else {
|
33
|
+
query += ` |> sort(columns: ["_time"])`;
|
34
|
+
}
|
35
|
+
query += ` |> limit(n:${limit}, offset:${offset})`;
|
36
|
+
const responses = await Promise.all([
|
37
|
+
fetch(encodeURI(`${influxConfig.url}/api/v2/query?org=${influxConfig.orgId}`), {
|
38
|
+
method: "POST",
|
39
|
+
headers: {
|
40
|
+
Authorization: `Token ${influxConfig.accessToken}`,
|
41
|
+
"Content-Type": "application/json"
|
42
|
+
},
|
43
|
+
body: JSON.stringify({
|
44
|
+
query: query,
|
45
|
+
type: "flux"
|
46
|
+
})
|
47
|
+
}),
|
48
|
+
fetch(encodeURI(`${influxConfig.url}/api/v2/query?org=${influxConfig.orgId}`), {
|
49
|
+
method: "POST",
|
50
|
+
headers: {
|
51
|
+
Authorization: `Token ${influxConfig.accessToken}`,
|
52
|
+
"Content-Type": "application/json"
|
53
|
+
},
|
54
|
+
body: JSON.stringify({
|
55
|
+
query: queryCount,
|
56
|
+
type: "flux"
|
57
|
+
})
|
58
|
+
})
|
59
|
+
]);
|
60
|
+
if (!responses[0].ok) {
|
61
|
+
throw new Error(`Failed to fetch data from InfluxDB: ${responses[0].statusText}`);
|
62
|
+
}
|
63
|
+
const data = await responses[0].text();
|
64
|
+
const count = await responses[1].text();
|
65
|
+
const rows = [];
|
66
|
+
const parsedData = parse(data.trim(), { columns: true });
|
67
|
+
parsedData.forEach((row) => {
|
68
|
+
rows.push({
|
69
|
+
time: row["_time"],
|
70
|
+
duration: parseInt(row["duration"], 10),
|
71
|
+
value: parseInt(row["_value"], 10),
|
72
|
+
valueName: row["valueName"]
|
73
|
+
});
|
74
|
+
});
|
75
|
+
const parsedCount = count.split("\n")[1].split(",");
|
76
|
+
let countData = parsedCount[5];
|
77
|
+
console.log("Count Data:", countData);
|
78
|
+
console.log("Rows:", rows);
|
79
|
+
return {
|
80
|
+
data: rows,
|
81
|
+
count: countData
|
82
|
+
};
|
83
|
+
}
|
84
|
+
export async function getInfluxDataV1(influxConfig, field, timeStart, timeEnd, deviceID, timeGroup, raw, fill, filterTag) {
|
41
85
|
let query;
|
86
|
+
let preStartValue = null;
|
87
|
+
// Build filter for filterTag if provided
|
88
|
+
const filterTagCondition = filterTag !== undefined && filterTag !== null
|
89
|
+
? ` AND ("d" = '${filterTag}' OR "d" !~ /./)`
|
90
|
+
: "";
|
42
91
|
if (raw) {
|
43
|
-
query = `SELECT ("value") FROM "${influxConfig.measurement}" WHERE "deviceid" = '${deviceID}' AND "valueName" = '${field}' AND time >= '${moment
|
92
|
+
query = `SELECT ("value") FROM "${influxConfig.measurement}" WHERE "deviceid" = '${deviceID}' AND "valueName" = '${field}'${filterTagCondition} AND time >= '${moment
|
44
93
|
.unix(timeStart)
|
45
94
|
.toISOString()}' AND time <= '${moment.unix(timeEnd).toISOString()}'`;
|
46
95
|
}
|
47
96
|
else {
|
48
|
-
query = `SELECT last("value") FROM "${influxConfig.measurement}" WHERE "deviceid" = '${deviceID}' AND "valueName" = '${field}' AND time >= '${moment
|
97
|
+
query = `SELECT last("value") FROM "${influxConfig.measurement}" WHERE "deviceid" = '${deviceID}' AND "valueName" = '${field}'${filterTagCondition} AND time >= '${moment
|
49
98
|
.unix(timeStart)
|
50
99
|
.toISOString()}' AND time <= '${moment
|
51
100
|
.unix(timeEnd)
|
52
|
-
.toISOString()}' GROUP BY time(${timeGroup})
|
101
|
+
.toISOString()}' GROUP BY time(${timeGroup})`;
|
102
|
+
if (fill === "none") {
|
103
|
+
query += ` fill(none)`;
|
104
|
+
}
|
105
|
+
else if (fill === "previous") {
|
106
|
+
query += ` fill(previous)`;
|
107
|
+
}
|
108
|
+
else {
|
109
|
+
query += ` fill(null)`;
|
110
|
+
}
|
111
|
+
}
|
112
|
+
if (fill) {
|
113
|
+
// Query to get the last data point before timeStart
|
114
|
+
const preStartQuery = `SELECT last("value") FROM "${influxConfig.measurement}" WHERE "deviceid" = '${deviceID}' AND "valueName" = '${field}'${filterTagCondition} AND time < '${moment
|
115
|
+
.unix(timeStart)
|
116
|
+
.toISOString()}'`;
|
117
|
+
const preStartResponse = await fetch(encodeURI(`${influxConfig.url}/query?db=${influxConfig.dbName}&epoch=s&q=${preStartQuery}`), {
|
118
|
+
headers: {
|
119
|
+
Authorization: `Basic ${btoa(`${influxConfig.username}:${influxConfig.password}`)}`
|
120
|
+
}
|
121
|
+
});
|
122
|
+
if (!preStartResponse.ok) {
|
123
|
+
throw new Error(`Failed to fetch pre-start data from InfluxDB: ${preStartResponse.statusText}`);
|
124
|
+
}
|
125
|
+
const preStartData = await preStartResponse.json();
|
126
|
+
preStartValue = preStartData.results[0].series?.[0]?.values?.[0]?.[1];
|
53
127
|
}
|
54
128
|
const response = await fetch(encodeURI(`${influxConfig.url}/query?db=${influxConfig.dbName}&epoch=s&q=${query}`), {
|
55
129
|
headers: {
|
@@ -57,7 +131,6 @@ export async function getInfluxDataV1(influxConfig, field, timeStart, timeEnd, d
|
|
57
131
|
}
|
58
132
|
});
|
59
133
|
if (!response.ok) {
|
60
|
-
console.log(response);
|
61
134
|
throw new Error(`Failed to fetch data from InfluxDB: ${response.statusText}`);
|
62
135
|
}
|
63
136
|
const data = await response.json();
|
@@ -72,61 +145,34 @@ export async function getInfluxDataV1(influxConfig, field, timeStart, timeEnd, d
|
|
72
145
|
}
|
73
146
|
];
|
74
147
|
}
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
.join(" OR ");
|
88
|
-
let queryCount = `SELECT count(*) FROM "${influxConfig.measurement}" WHERE "deviceid" = '${deviceID}' AND (${conditions})`;
|
89
|
-
let queryPagination = `SELECT "valueName", "value" FROM "${influxConfig.measurement}" WHERE "deviceid" = '${deviceID}' AND (${conditions})`;
|
90
|
-
if (timeStart) {
|
91
|
-
queryCount += ` AND time >= '${moment.unix(timeStart).toISOString()}'`;
|
92
|
-
queryPagination += ` AND time >= '${moment
|
93
|
-
.unix(timeStart)
|
94
|
-
.toISOString()}'`;
|
95
|
-
}
|
96
|
-
if (timeEnd) {
|
97
|
-
queryCount += ` AND time <= '${moment.unix(timeEnd).toISOString()}'`;
|
98
|
-
queryPagination += ` AND time <= '${moment.unix(timeEnd).toISOString()}'`;
|
99
|
-
}
|
100
|
-
if (sort && sort.field === "time") {
|
101
|
-
queryPagination = `${queryPagination} ORDER BY "${sort.field}" ${sort.sort}`;
|
102
|
-
}
|
103
|
-
queryPagination = `${queryPagination} LIMIT ${limit} OFFSET ${offset}`;
|
104
|
-
const responses = await Promise.all([
|
105
|
-
fetch(encodeURI(`${influxConfig.url}/query?db=${influxConfig.dbName}&epoch=s&q=${queryPagination}`), {
|
106
|
-
headers: {
|
107
|
-
Authorization: `Basic ${btoa(`${influxConfig.username}:${influxConfig.password}`)}`
|
108
|
-
}
|
109
|
-
}),
|
110
|
-
fetch(encodeURI(`${influxConfig.url}/query?db=${influxConfig.dbName}&epoch=s&q=${queryCount}`), {
|
111
|
-
headers: {
|
112
|
-
Authorization: `Basic ${btoa(`${influxConfig.username}:${influxConfig.password}`)}`
|
113
|
-
}
|
114
|
-
})
|
115
|
-
]);
|
116
|
-
if (!responses[0].ok) {
|
117
|
-
throw new Error(`Failed to fetch data from InfluxDB: ${responses[0].statusText}`);
|
148
|
+
// Always override the name to be the field
|
149
|
+
data.results[0].series.forEach((series) => {
|
150
|
+
series.name = field; // Force the series name to be the field name
|
151
|
+
});
|
152
|
+
// 1000000 REMOVED AND ADDED TO MOVE THE POINT AWAY IN THE CHART
|
153
|
+
if (fill) {
|
154
|
+
if (preStartValue !== null && preStartValue !== undefined) {
|
155
|
+
// Insert the pre-start value at the beginning of the dataset
|
156
|
+
data.results[0].series[0].values.unshift([
|
157
|
+
timeStart - 1000000,
|
158
|
+
preStartValue
|
159
|
+
]);
|
118
160
|
}
|
119
|
-
|
120
|
-
const
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
161
|
+
// Set the last point as the timeEnd and last value of the dataset
|
162
|
+
const lastSeries = data.results[0].series[0];
|
163
|
+
const lastValue = lastSeries?.values &&
|
164
|
+
lastSeries.values.length > 0 &&
|
165
|
+
lastSeries.values.slice(-1)[0].length > 1
|
166
|
+
? lastSeries.values.slice(-1)[0][1]
|
167
|
+
: null;
|
168
|
+
data.results[0].series[0].values.push([timeEnd + 1000000, lastValue]);
|
125
169
|
}
|
170
|
+
return data;
|
126
171
|
}
|
127
|
-
export async function
|
128
|
-
|
129
|
-
|
172
|
+
export async function exportDataV1(influxConfig, field, timeStart, timeEnd, deviceID) {
|
173
|
+
const query = `SELECT ("value") FROM "${influxConfig.measurement}" WHERE "deviceid" = '${deviceID}' AND "valueName" = '${field}' AND time >= '${moment
|
174
|
+
.unix(timeStart)
|
175
|
+
.toISOString()}' AND time <= '${moment.unix(timeEnd).toISOString()}'`;
|
130
176
|
const response = await fetch(encodeURI(`${influxConfig.url}/query?db=${influxConfig.dbName}&epoch=s&q=${query}`), {
|
131
177
|
headers: {
|
132
178
|
Authorization: `Basic ${btoa(`${influxConfig.username}:${influxConfig.password}`)}`
|
@@ -136,10 +182,22 @@ export async function getFirstTimestamp(influxConfig, deviceID) {
|
|
136
182
|
throw new Error(`Failed to fetch data from InfluxDB: ${response.statusText}`);
|
137
183
|
}
|
138
184
|
const data = await response.json();
|
139
|
-
//
|
140
|
-
if (data
|
141
|
-
//
|
142
|
-
|
185
|
+
// Ensure the name is manually set to the field
|
186
|
+
if (!data.results[0].series) {
|
187
|
+
// Set default value with null time and null value
|
188
|
+
data.results[0].series = [
|
189
|
+
{
|
190
|
+
name: field, // Manually set the series name as the field
|
191
|
+
columns: ["time", "value"],
|
192
|
+
values: [] // Set null point for time and value
|
193
|
+
}
|
194
|
+
];
|
143
195
|
}
|
144
|
-
|
196
|
+
else {
|
197
|
+
// Always override the name to be the field
|
198
|
+
data.results[0].series.forEach((series) => {
|
199
|
+
series.name = field; // Force the series name to be the field name
|
200
|
+
});
|
201
|
+
}
|
202
|
+
return data;
|
145
203
|
}
|
@@ -1,19 +1,20 @@
|
|
1
1
|
export type TrackleConfig = {
|
2
|
+
orgName: string;
|
2
3
|
apiUrl: string;
|
3
|
-
createCustomerUrl: string;
|
4
4
|
clientId: string;
|
5
5
|
clientSecret: string;
|
6
6
|
tokenUrl: string;
|
7
7
|
cookieName: string;
|
8
8
|
cookieSecure: boolean;
|
9
9
|
apiTimeout: number;
|
10
|
+
productID: number;
|
10
11
|
};
|
11
12
|
export declare function logOut(trackleConfig: TrackleConfig): Promise<void>;
|
12
13
|
export declare function createCustomer(trackleConfig: TrackleConfig, uid: string): Promise<{
|
13
14
|
organization: string;
|
14
15
|
uid: string;
|
15
16
|
}>;
|
16
|
-
export declare function getDevices(trackleConfig: TrackleConfig, productId?: number, uid?: string,
|
17
|
+
export declare function getDevices(trackleConfig: TrackleConfig, productId?: number, uid?: string, query?: string): Promise<{
|
17
18
|
devices: any[];
|
18
19
|
}>;
|
19
20
|
export declare function getDevice(trackleConfig: TrackleConfig, id: string, productId?: number, uid?: string): Promise<{
|
@@ -48,32 +48,19 @@ export async function logOut(trackleConfig) {
|
|
48
48
|
cookies().delete(trackleConfig.cookieName);
|
49
49
|
}
|
50
50
|
export async function createCustomer(trackleConfig, uid) {
|
51
|
-
return await wretch(trackleConfig.
|
51
|
+
return await wretch(`${trackleConfig.apiUrl}/orgs/${trackleConfig.orgName}/customers`)
|
52
52
|
.headers({
|
53
53
|
Authorization: `Basic ${btoa(`${trackleConfig.clientId}:${trackleConfig.clientSecret}`)}`
|
54
54
|
})
|
55
55
|
.post({ uid })
|
56
56
|
.json();
|
57
57
|
}
|
58
|
-
export async function getDevices(trackleConfig, productId, uid,
|
59
|
-
let queryParams = "last_handshake_at!=null";
|
60
|
-
if (group !== "all") {
|
61
|
-
queryParams = `last_handshake_at!=null&state.groups=/${group}/`;
|
62
|
-
if (!uid) {
|
63
|
-
queryParams += "&quarantined=false";
|
64
|
-
}
|
65
|
-
}
|
66
|
-
else if (selected !== "all") {
|
67
|
-
queryParams = `last_handshake_at!=null&state.groups!=/${selected}/`;
|
68
|
-
if (!uid) {
|
69
|
-
queryParams += "&quarantined=false";
|
70
|
-
}
|
71
|
-
}
|
58
|
+
export async function getDevices(trackleConfig, productId, uid, query) {
|
72
59
|
const api = uid ? wretchApi(trackleConfig, uid) : wretchApi(trackleConfig);
|
73
60
|
const response = await api
|
74
61
|
.get(uid
|
75
|
-
? `/devices
|
76
|
-
: `/products/${productId}/devices
|
62
|
+
? `/devices${query ? "?" + query : ""}`
|
63
|
+
: `/products/${productId}/devices${query ? "?" + query : ""}`)
|
77
64
|
.setTimeout(trackleConfig.apiTimeout)
|
78
65
|
.json();
|
79
66
|
return (uid ? { devices: response } : response);
|
@@ -0,0 +1,19 @@
|
|
1
|
+
export type FirebaseConfig = {
|
2
|
+
apiKey: string;
|
3
|
+
authDomain: string;
|
4
|
+
projectId: string;
|
5
|
+
storageBucket: string;
|
6
|
+
messagingSenderId: string;
|
7
|
+
appId: string;
|
8
|
+
};
|
9
|
+
export type InfluxConfig = {
|
10
|
+
url: string;
|
11
|
+
accessToken: string;
|
12
|
+
bucket: string;
|
13
|
+
orgId: string;
|
14
|
+
measurement: string;
|
15
|
+
dbName: string;
|
16
|
+
username: string;
|
17
|
+
password: string;
|
18
|
+
};
|
19
|
+
export type InfluxFillType = "null" | "none" | "previous";
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|