diff --git a/src/app/core/services/util.service.ts b/src/app/core/services/util.service.ts index 5de53fc1..0afcc8b7 100644 --- a/src/app/core/services/util.service.ts +++ b/src/app/core/services/util.service.ts @@ -117,3 +117,13 @@ export function hashCode(s: string): number { } return hash; } + +export async function digestMessage(message: string, algorithm = 'SHA-256'): Promise { + const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array + const hashBuffer = await window.crypto.subtle.digest(algorithm, msgUint8); // hash the message + const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); // convert bytes to hex string + return hashHex; +} diff --git a/src/app/widgets/charts/export/shared/charts-export.service.ts b/src/app/widgets/charts/export/shared/charts-export.service.ts index c8c6780e..08f487f2 100644 --- a/src/app/widgets/charts/export/shared/charts-export.service.ts +++ b/src/app/widgets/charts/export/shared/charts-export.service.ts @@ -15,7 +15,7 @@ */ import { Injectable } from '@angular/core'; -import { forkJoin, Observable, of } from 'rxjs'; +import { forkJoin, Observable, of, from } from 'rxjs'; import { ChartsModel } from '../../shared/charts.model'; import { ElementSizeService } from '../../../../core/services/element-size.service'; import { ErrorHandlerService } from '../../../../core/services/error-handler.service'; @@ -42,11 +42,11 @@ import { QueriesRequestTimeModel, QueriesRequestV2ElementTimescaleModel, } from '../../../shared/export-data.model'; -import { catchError, concatMap, map } from 'rxjs/operators'; +import { catchError, concatMap, map, mergeMap } from 'rxjs/operators'; import { environment } from '../../../../../environments/environment'; -import { DeviceInstancesTotalModel, DeviceInstancesWithDeviceTypeTotalModel, DeviceInstanceWithDeviceTypeModel } from 'src/app/modules/devices/device-instances/shared/device-instances.model'; +import { DeviceInstancesWithDeviceTypeTotalModel, DeviceInstanceWithDeviceTypeModel } from 'src/app/modules/devices/device-instances/shared/device-instances.model'; import { DeviceInstancesService } from 'src/app/modules/devices/device-instances/shared/device-instances.service'; -import { hashCode } from 'src/app/core/services/util.service'; +import { digestMessage } from 'src/app/core/services/util.service'; const customColor = '#4484ce'; // /* cc */ @@ -214,73 +214,73 @@ export class ChartsExportService { const metadataElem = { exportId: values[elementIndex].exportId, deviceId: values[elementIndex].deviceId, serviceId: values[elementIndex].serviceId, columnName: columnNames !== undefined && columnNames.length === 1 ? columnNames[0] : undefined }; if (threeD !== null) { switch ((properties.vAxes || [])[timescaleResultMapper[values[elementIndex].requestIndex]].deviceGroupMergingStrategy) { - case ChartsExportDeviceGroupMergingStrategy.Merge: - // merge into one table row - if (res[values[elementIndex].requestIndex].length === 0) { - res[values[elementIndex].requestIndex].push([]); + case ChartsExportDeviceGroupMergingStrategy.Merge: + // merge into one table row + if (res[values[elementIndex].requestIndex].length === 0) { + res[values[elementIndex].requestIndex].push([]); + } + threeD.forEach(rows => { + if (rows.length === 0) { + return; } - threeD.forEach(rows => { - if (rows.length === 0) { - return; - } - if (rows[0].length > 2) { - rows.forEach(row => { - row.slice(1).forEach(r => { - res[values[elementIndex].requestIndex][0].push([row[0], r]); - }); + if (rows[0].length > 2) { + rows.forEach(row => { + row.slice(1).forEach(r => { + res[values[elementIndex].requestIndex][0].push([row[0], r]); }); + }); + } else { + res[values[elementIndex].requestIndex][0].push(...rows); + } + }); + break; + case ChartsExportDeviceGroupMergingStrategy.Sum: + // sum by timestamp + // can only be selected together with a group to ensure identical timestamps + if (res[values[elementIndex].requestIndex].length === 0) { + res[values[elementIndex].requestIndex].push([]); + } + threeD.forEach(rows => { + rows.forEach(row => { + const elem = res[values[elementIndex].requestIndex][0].find((r: any) => r.length > 0 && r[0] === row[0]); + if (elem === undefined) { + res[values[elementIndex].requestIndex][0].push(row); } else { - res[values[elementIndex].requestIndex][0].push(...rows); + let v = (elem[1] as number); + row.slice(1).forEach(n => v += n); + elem[1] = v; } }); - break; - case ChartsExportDeviceGroupMergingStrategy.Sum: - // sum by timestamp - // can only be selected together with a group to ensure identical timestamps - if (res[values[elementIndex].requestIndex].length === 0) { - res[values[elementIndex].requestIndex].push([]); + }); + break; + case ChartsExportDeviceGroupMergingStrategy.Separate: + default: + //preserve individual rows + threeD.forEach(rows => { + if (rows.length === 0) { + return; } - threeD.forEach(rows => { + if (rows[0].length > 2) { + const off = res[values[elementIndex].requestIndex].length; rows.forEach(row => { - const elem = res[values[elementIndex].requestIndex][0].find((r: any) => r.length > 0 && r[0] === row[0]); - if (elem === undefined) { - res[values[elementIndex].requestIndex][0].push(row); - } else { - let v = (elem[1] as number); - row.slice(1).forEach(n => v += n); - elem[1] = v; - } - }); - }); - break; - case ChartsExportDeviceGroupMergingStrategy.Separate: - default: - //preserve individual rows - threeD.forEach(rows => { - if (rows.length === 0) { - return; - } - if (rows[0].length > 2) { - const off = res[values[elementIndex].requestIndex].length; - rows.forEach(row => { - row.slice(1).forEach((r, i) => { - while (res[values[elementIndex].requestIndex].length <= off + i) { - res[values[elementIndex].requestIndex].push([]); - metadata[values[elementIndex].requestIndex].push({}); - } - res[values[elementIndex].requestIndex][off + i].push([row[0], r]); - metadata[values[elementIndex].requestIndex][off + i] = JSON.parse(JSON.stringify(metadataElem)); - if (columnNames !== undefined && columnNames.length > i) { - metadata[values[elementIndex].requestIndex][off + i].columnName = columnNames[i]; - } - }); + row.slice(1).forEach((r, i) => { + while (res[values[elementIndex].requestIndex].length <= off + i) { + res[values[elementIndex].requestIndex].push([]); + metadata[values[elementIndex].requestIndex].push({}); + } + res[values[elementIndex].requestIndex][off + i].push([row[0], r]); + metadata[values[elementIndex].requestIndex][off + i] = JSON.parse(JSON.stringify(metadataElem)); + if (columnNames !== undefined && columnNames.length > i) { + metadata[values[elementIndex].requestIndex][off + i].columnName = columnNames[i]; + } }); - } else { - res[values[elementIndex].requestIndex].push(rows); - metadata[values[elementIndex].requestIndex].push(metadataElem); - } - }); - break; + }); + } else { + res[values[elementIndex].requestIndex].push(rows); + metadata[values[elementIndex].requestIndex].push(metadataElem); + } + }); + break; } } }); @@ -298,12 +298,12 @@ export class ChartsExportService { let mapper: number[] = []; res.forEach(r => { switch (r.source) { - case 'influx': - mapper = influxResultMapper; - break; - case 'timescale': - mapper = timescaleResultMapper; - break; + case 'influx': + mapper = influxResultMapper; + break; + case 'timescale': + mapper = timescaleResultMapper; + break; } r.res.forEach((req, index) => { table[mapper[index]] = req; @@ -327,47 +327,49 @@ export class ChartsExportService { } - getChartData(widget: WidgetModel, from?: string, to?: string, groupInterval?: string, hAxisFormat?: string, lastOverride?: string, chooseColors = false): Observable { - return new Observable((observer) => { - this.getData(widget.properties, from, to, groupInterval, lastOverride).pipe(concatMap(r => { - let obs: Observable = of({ result: [], total: 0 }); - const deviceIds: string[] = []; - r.metadata.forEach(m => { - m.forEach(subM => { - if (subM.deviceId !== undefined && !this.devices.has(subM.deviceId) && deviceIds.indexOf(subM.deviceId) === -1) { - deviceIds.push(subM.deviceId); - } - }); - }); - if (deviceIds.length > 0) { - obs = this.deviceInstancesService.getDeviceInstancesWithDeviceType({ - limit: deviceIds.length, - offset: 0, - deviceIds, - }); - } - return obs.pipe(map(devices => { - devices.result.forEach(d => this.devices.set(d.id, d)); - return r; - })); - })).subscribe(r => { - const resp = r.data; - if (resp === null) { - // no data - observer.next(this.setProcessInstancesStatusValues(widget, new ChartDataTableModel([[]]))); - } else if (this.errorHandlerService.checkIfErrorExists(resp)) { - observer.next(resp as ErrorModel); - } else { - const tableData = this.setData(resp, widget.properties, r.metadata); - if (chooseColors) { - tableData.colors = tableData.table.data[0].slice(1).map(title => - '#' + Math.abs(hashCode(JSON.stringify(title))).toString(16).slice(0, 6)); // semi-random color that is always the same for the same title + getChartData(widget: WidgetModel, _from?: string, to?: string, groupInterval?: string, hAxisFormat?: string, lastOverride?: string, chooseColors = false): Observable { + return this.getData(widget.properties, _from, to, groupInterval, lastOverride).pipe(concatMap(r => { + let obs: Observable = of({ result: [], total: 0 }); + const deviceIds: string[] = []; + r.metadata.forEach(m => { + m.forEach(subM => { + if (subM.deviceId !== undefined && !this.devices.has(subM.deviceId) && deviceIds.indexOf(subM.deviceId) === -1) { + deviceIds.push(subM.deviceId); } - observer.next(this.setProcessInstancesStatusValues(widget, tableData.table, tableData.colors, hAxisFormat)); - } - observer.complete(); + }); }); - }); + if (deviceIds.length > 0) { + obs = this.deviceInstancesService.getDeviceInstancesWithDeviceType({ + limit: deviceIds.length, + offset: 0, + deviceIds, + }); + } + return obs.pipe(map(devices => { + devices.result.forEach(d => this.devices.set(d.id, d)); + return r; + })); + })).pipe(mergeMap(r => { + const resp = r.data; + if (resp === null) { + // no data + return of(this.setProcessInstancesStatusValues(widget, new ChartDataTableModel([[]]))); + } else if (this.errorHandlerService.checkIfErrorExists(resp)) { + return of(resp as ErrorModel); + } else { + const obs: Observable[] = [of(null)]; // needs at leat one Obserfable for forkJoin + const tableData = this.setData(resp, widget.properties, r.metadata); + if (chooseColors) { + tableData.colors = Array(tableData.table.data[0].length - 1); + obs.push(...tableData.table.data[0].slice(1).map((title, i) => + // i just created it... + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + from(digestMessage(title as string, 'SHA-1')).pipe(map(digest => tableData.colors![i-1] = '#' + digest.slice(0, 6))))); + //'#' + Math.abs(hashCode(JSON.stringify(title))).toString(16).slice(0, 6)); // semi-random color that is always the same for the same title + } + return forkJoin(obs).pipe(map(_ => this.setProcessInstancesStatusValues(widget, tableData.table, tableData.colors, hAxisFormat))); + } + })); } private setData(data: any[][][][], properties: WidgetPropertiesModels, metadata: { exportId?: string; deviceId?: string; serviceId?: string; columnName?: string }[][]): {