@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.
@@ -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, measures, enableExportData, enableDatePicker, handleGetInfluxData, handleGetFirstTimestamp, theme, ...props }: {
12
+ declare const TrendChart: ({ deviceId, measures1, annotationsData, measures2, enableDatePicker, handleGetInfluxData, handleExportDataCB, theme, initialTimeStart, initialTimeEnd, dwEnabled, dwCallback, dwHandled }: {
10
13
  deviceId: string;
11
- measures: Array<Measure>;
12
- enableenableExportData: boolean;
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
- } & any) => import("react/jsx-runtime").JSX.Element;
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(2)} ${context.dataset.unit}`;
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 GetPoints(data) {
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
- // Create rows from timestampMap
164
- Object.values(timestampMap).forEach((entry) => {
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 or just the timestamp, add it to csvData
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, measures, enableExportData, enableDatePicker, handleGetInfluxData, handleGetFirstTimestamp, theme, ...props }) => {
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 [timeStart, setTimeStart] = useState(moment().subtract(1, 'day').unix());
188
- const [timeEnd, setTimeEnd] = useState(moment().unix());
189
- const [firstTimestamp, setFirstTimestamp] = useState();
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
- y: {
223
- ...options.scales.y,
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
- setZoomed(false);
234
- setDatePickerUsed(false);
235
- setCsvData('');
236
- if (newPeriod === "ALL") {
237
- setTimeStart(firstTimestamp - 86400);
238
- setTimeEnd(moment().unix());
239
- setChartPeriod(newPeriod);
240
- return;
241
- }
242
- const periodConfig = chartConfigByPeriod[newPeriod];
243
- if (periodConfig) {
244
- setTimeStart(Math.round(moment().subtract(periodConfig.from).valueOf() / 1000));
245
- setTimeEnd(Math.round(moment().valueOf() / 1000));
246
- setChartPeriod(newPeriod);
247
- setChartPeriodConfig(periodConfig);
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
- const intervalInSeconds = timeEnd - timeStart;
253
- const data = await Promise.all(measures.map(async (measure) => {
254
- const polltime = getPollTime(intervalInSeconds, measure.polltime);
255
- return await handleGetInfluxData(measure.name, timeStart, timeEnd, deviceId, polltime, true);
256
- }));
257
- const csvData = getCsvData(data, measures);
258
- setCsvData(csvData);
259
- setLoadingButton(false);
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
- setChartLoading(true);
268
- const intervalInSeconds = timeEnd - timeStart;
269
- // Inizializza un array di promesse per ottenere i dati per ciascuna misura
270
- const datasetsPromises = measures.map(async (measure) => {
271
- const polltime = getPollTime(intervalInSeconds, measure.polltime);
272
- const influxData = await handleGetInfluxData(measure.name, timeStart, timeEnd, deviceId, polltime, false);
273
- const points = GetPoints(influxData);
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
- spanGaps: false,
284
- fill: false
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 min = null;
290
- let max = null;
291
- let values = [];
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 = [...values, ...dataset.data.map((point) => point.y).filter((data) => data !== null)];
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 (min === null || datasetMin < min)
297
- min = datasetMin;
298
- if (max === null || datasetMax > max)
299
- max = datasetMax;
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
- let paddedMin = null;
302
- let paddedMax = null;
303
- if (min !== null && max !== null) {
304
- const diff = Math.round((max - min) / 3) || 1;
305
- paddedMin = min - diff;
306
- paddedMax = max + diff;
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
- setDataMeasures([...datasets]);
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
- y: {
314
- ...options.scales.y,
315
- min: paddedMin,
316
- max: paddedMax,
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
- time: {
323
- ...options.scales.x.time,
324
- unit: chartPeriod.scaleUnit
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 fetchFirstTimestamp = async () => {
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 < 2629800) { // Less than 1 month
476
+ else if (timeDifference < 2678400) { // Less than 1 month
349
477
  newChartPeriod = '1W'; // Set to 1 week
350
478
  }
351
- else if (timeDifference < 7889400) { // Less than 3 months
479
+ else if (timeDifference < 8035200) { // Less than 3 months
352
480
  newChartPeriod = '1M'; // Set to 1 month
353
481
  }
354
- else if (timeDifference < 15778440) { // Less than 6 months
482
+ else if (timeDifference < 16070400) { // Less than 6 months
355
483
  newChartPeriod = '3M'; // Set to 3 months
356
484
  }
357
- else if (timeDifference < 31556926) { // Less than 1 year
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
- const topY = chart.scales.y.top;
379
- const bottomY = chart.scales.y.bottom;
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(timeStart * 1000),
397
- // format="DD/MM/YY hh:mm A"
398
- onChange: (newValue) => {
399
- setTimeStart((moment(newValue).unix()));
400
- }, maxDate: moment(timeEnd * 1000), slotProps: {
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
- // format="DD/MM/YY hh:mm A"
404
- onChange: (newValue) => {
405
- setTimeEnd((moment(newValue).unix()));
406
- }, minDate: moment(timeStart * 1000), slotProps: {
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
- if (datePickerUsed) {
415
- setZoomed(false);
416
- }
417
- else if (chartPeriod === "ALL") {
418
- setZoomed(false);
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
- setTimeStart(firstTimestamp - 86400);
576
+ }
577
+ else if (datePickerUsed) {
578
+ setTimeStart(pickerTimeStart);
579
+ setTimeEnd(pickerTimeEnd);
421
580
  }
422
581
  else {
423
- setZoomed(false);
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', xl: '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: chartPeriod, exclusive: true, onChange: handleChange, size: "small",
433
- // sx={{ boxShadow: 1 }}
434
- 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(Box, { component: 'div', className: "chart-container", sx: { height: { xs: enableDatePicker && props.height ? `calc(${props.height} - 50px)` : props.height, lg: props.height }, minHeight: 300, mt: 2 }, children: chartJsLoaded && !chartLoading && typeof window !== 'undefined' ?
435
- _jsx(_Fragment, { children: dataMeasures && dataMeasures[0]?.data?.length ?
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: (productID: number, userInfo?: UserType) => Promise<any>;
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(1008, { role: 'support', uid: userId });
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: (productID: number, userInfo?: UserType) => Promise<any>;
14
+ handleGetGroups: (userInfo?: UserType) => Promise<any>;
15
15
  handleGetUsersGroup: (groupID: string) => Promise<any>;
16
16
  handleCreateGroup: (group: any) => Promise<any>;
17
- handleGetDevices: (user: UserType, group: string, selected: string) => Promise<any>;
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
- const devices = await handleGetDevices(userInfo, group, selected);
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(1008, userInfo);
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
- getUsersGroup(currentGroup);
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.managers,
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(1008, { role: 'support', uid: manager });
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-preview2",
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,4 @@
1
+ import { FirebaseConfig } from "./types";
2
+ export declare const getAnnotations: (firebaseConfig: FirebaseConfig, deviceID: number) => Promise<{
3
+ id: string;
4
+ }[]>;
@@ -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
- export type FirebaseConfig = {
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
  }[]>;
@@ -2,3 +2,4 @@ export * from "./groups";
2
2
  export * from "./influx";
3
3
  export * from "./logto";
4
4
  export * from "./trackle";
5
+ export * from "./annotations";
@@ -3,3 +3,4 @@ export * from "./groups";
3
3
  export * from "./influx";
4
4
  export * from "./logto";
5
5
  export * from "./trackle";
6
+ export * from "./annotations";
@@ -1,13 +1,4 @@
1
- export type InfluxConfig = {
2
- url: string;
3
- accessToken: string;
4
- bucket: string;
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>;
@@ -1,55 +1,129 @@
1
1
  "use server";
2
+ import { parse } from "csv-parse";
2
3
  import moment from "moment";
3
- /* export async function getInfluxDataV2(influxConfig: InfluxConfig, field: string, timeStart: string, timeEnd: string, deviceID: string, timeGroup: string, raw: boolean): Promise<Point[]> {
4
- const query = `
5
- from(bucket: "${influxConfig.bucket}")
6
- |> range(start: ${timeStart}, stop: ${timeEnd})
7
- |> filter(fn: (r) => r._measurement == "${influxConfig.measurement}")
8
- |> filter(fn: (r) => r["_field"] == "${field}")
9
- |> filter(fn: (r) => r["deviceid"] == "${deviceID}")
10
- |> aggregateWindow(every: ${timeGroup}m, fn: last, createEmpty: false)
11
- |> yield(name: "last")
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
- const response = await fetch(`${influxConfig.url}/api/v2/query?org=${influxConfig.orgId}`, {
15
- method: 'POST',
16
- headers: {
17
- 'Content-Type': 'application/json',
18
- 'Authorization': `Token ${influxConfig.accessToken}`,
19
- 'Accept': 'application/csv'
20
- },
21
- body: JSON.stringify({
22
- query: query
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
- return { x: new Date(timestamp).getTime(), y: value };
38
- }).filter(point => point !== null);
39
- }*/ // NOT WORKING, NEED TO FIX
40
- export async function getInfluxDataV1(influxConfig, field, timeStart, timeEnd, deviceID, timeGroup, raw) {
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}) fill(null)`;
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
- else {
76
- // Always override the name to be the field
77
- data.results[0].series.forEach((series) => {
78
- series.name = field; // Force the series name to be the field name
79
- });
80
- }
81
- return data;
82
- }
83
- export async function getManyMeasuresV1(influxConfig, fields, limit, offset, sort, deviceID, timeStart, timeEnd) {
84
- if (fields.length > 0) {
85
- const conditions = fields
86
- .map((field) => `"valueName" = '${field}'`)
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
- const data = await responses[0].json();
120
- const count = await responses[1].json();
121
- return { data, count };
122
- }
123
- else {
124
- return null;
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 getFirstTimestamp(influxConfig, deviceID) {
128
- // Query per ottenere il primo timestamp e il valore ordinato per "time" in modo crescente
129
- const query = `SELECT "time", "value" FROM "${influxConfig.measurement}" WHERE "deviceid" = '${deviceID}' ORDER BY time ASC LIMIT 1`;
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
- // Verifica che ci siano risultati nella risposta
140
- if (data?.results[0]?.series && data?.results[0]?.series[0]?.values[0]?.[0]) {
141
- // Ritorna il primo timestamp
142
- return data.results[0].series[0].values[0][0];
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
- return null; // Se non ci sono record, ritorna null
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, group?: string, selected?: string): Promise<{
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.createCustomerUrl)
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, group, selected) {
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?${queryParams}`
76
- : `/products/${productId}/devices?${queryParams}`)
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 {};
package/types/user.d.ts CHANGED
@@ -4,6 +4,7 @@ export type UserType = {
4
4
  name: string;
5
5
  picture?: string;
6
6
  role: string;
7
+ demo?: any;
7
8
  lastSignInAt?: number;
8
9
  firstname?: string;
9
10
  lastname?: string;