Commit d542b24ad44c527138a3f86228e5f1c5846f5386

Authored by Andrii Shvaika
2 parents 5cc7bbe6 9b92f67b

Merge branch 'master' of github.com:thingsboard/thingsboard

... ... @@ -31,7 +31,6 @@ import {
31 31 } from '@shared/models/query/query.models';
32 32 import { SubscriptionTimewindow } from '@shared/models/time/time.models';
33 33 import { AlarmDataListener } from '@core/api/alarm-data.service';
34   -import { UtilsService } from '@core/services/utils.service';
35 34 import { PageData } from '@shared/models/page/page-data';
36 35 import { deepClone, isDefined, isDefinedAndNotNull, isObject } from '@core/utils';
37 36 import { simulatedAlarm } from '@shared/models/alarm.models';
... ... @@ -68,8 +67,7 @@ export class AlarmDataSubscription {
68 67
69 68 constructor(public alarmDataSubscriptionOptions: AlarmDataSubscriptionOptions,
70 69 private listener: AlarmDataListener,
71   - private telemetryService: TelemetryService,
72   - private utils: UtilsService) {
  70 + private telemetryService: TelemetryService) {
73 71 }
74 72
75 73 public unsubscribe() {
... ... @@ -166,7 +164,7 @@ export class AlarmDataSubscription {
166 164 private onPageData(pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) {
167 165 this.pageData = pageData;
168 166 this.resetData();
169   - this.listener.alarmsLoaded(pageData, this.alarmDataSubscriptionOptions.pageLink, allowedEntities, totalEntities);
  167 + this.listener.alarmsLoaded(pageData, allowedEntities, totalEntities);
170 168 }
171 169
172 170 private onDataUpdate(update: Array<AlarmData>) {
... ...
... ... @@ -20,7 +20,6 @@ import { PageData } from '@shared/models/page/page-data';
20 20 import { AlarmData, AlarmDataPageLink, KeyFilter } from '@shared/models/query/query.models';
21 21 import { Injectable } from '@angular/core';
22 22 import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
23   -import { UtilsService } from '@core/services/utils.service';
24 23 import {
25 24 AlarmDataSubscription,
26 25 AlarmDataSubscriptionOptions,
... ... @@ -31,7 +30,7 @@ import { deepClone } from '@core/utils';
31 30 export interface AlarmDataListener {
32 31 subscriptionTimewindow?: SubscriptionTimewindow;
33 32 alarmSource: Datasource;
34   - alarmsLoaded: (pageData: PageData<AlarmData>, pageLink: AlarmDataPageLink, allowedEntities: number, totalEntities: number) => void;
  33 + alarmsLoaded: (pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) => void;
35 34 alarmsUpdated: (update: Array<AlarmData>, pageData: PageData<AlarmData>) => void;
36 35 subscription?: AlarmDataSubscription;
37 36 }
... ... @@ -41,8 +40,7 @@ export interface AlarmDataListener {
41 40 })
42 41 export class AlarmDataService {
43 42
44   - constructor(private telemetryService: TelemetryWebsocketService,
45   - private utils: UtilsService) {}
  43 + constructor(private telemetryService: TelemetryWebsocketService) {}
46 44
47 45
48 46 public subscribeForAlarms(listener: AlarmDataListener,
... ... @@ -88,7 +86,7 @@ export class AlarmDataService {
88 86 alarmDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
89 87 }
90 88 return new AlarmDataSubscription(alarmDataSubscriptionOptions,
91   - listener, this.telemetryService, this.utils);
  89 + listener, this.telemetryService);
92 90 }
93 91
94 92 }
... ...
... ... @@ -77,6 +77,10 @@ export function isUndefined(value: any): boolean {
77 77 return typeof value === 'undefined';
78 78 }
79 79
  80 +export function isUndefinedOrNull(value: any): boolean {
  81 + return typeof value === 'undefined' || value === null;
  82 +}
  83 +
80 84 export function isDefined(value: any): boolean {
81 85 return typeof value !== 'undefined';
82 86 }
... ... @@ -452,7 +456,7 @@ export function insertVariable(pattern: string, name: string, value: any): strin
452 456 const variable = match[0];
453 457 const variableName = match[1];
454 458 if (variableName === name) {
455   - result = result.split(variable).join(value);
  459 + result = result.replace(variable, value);
456 460 }
457 461 match = varsRegex.exec(pattern);
458 462 }
... ... @@ -469,17 +473,17 @@ export function createLabelFromDatasource(datasource: Datasource, pattern: strin
469 473 const variable = match[0];
470 474 const variableName = match[1];
471 475 if (variableName === 'dsName') {
472   - label = label.split(variable).join(datasource.name);
  476 + label = label.replace(variable, datasource.name);
473 477 } else if (variableName === 'entityName') {
474   - label = label.split(variable).join(datasource.entityName);
  478 + label = label.replace(variable, datasource.entityName);
475 479 } else if (variableName === 'deviceName') {
476   - label = label.split(variable).join(datasource.entityName);
  480 + label = label.replace(variable, datasource.entityName);
477 481 } else if (variableName === 'entityLabel') {
478   - label = label.split(variable).join(datasource.entityLabel || datasource.entityName);
  482 + label = label.replace(variable, datasource.entityLabel || datasource.entityName);
479 483 } else if (variableName === 'aliasName') {
480   - label = label.split(variable).join(datasource.aliasName);
  484 + label = label.replace(variable, datasource.aliasName);
481 485 } else if (variableName === 'entityDescription') {
482   - label = label.split(variable).join(datasource.entityDescription);
  486 + label = label.replace(variable, datasource.entityDescription);
483 487 }
484 488 match = varsRegex.exec(pattern);
485 489 }
... ...
... ... @@ -40,6 +40,7 @@ import { DialogService } from '@core/services/dialog.service';
40 40 import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
41 41 import { DatePipe } from '@angular/common';
42 42 import { TranslateService } from '@ngx-translate/core';
  43 +import { DomSanitizer } from '@angular/platform-browser';
43 44
44 45 export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
45 46
... ... @@ -74,6 +75,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
74 75 this.ctx.date = $injector.get(DatePipe);
75 76 this.ctx.translate = $injector.get(TranslateService);
76 77 this.ctx.http = $injector.get(HttpClient);
  78 + this.ctx.sanitizer = $injector.get(DomSanitizer);
77 79
78 80 this.ctx.$scope = this;
79 81 if (this.ctx.defaultSubscription) {
... ...
... ... @@ -121,9 +121,10 @@
121 121 </mat-cell>
122 122 </ng-container>
123 123 <mat-header-row [ngClass]="{'mat-row-select': enableSelection}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
124   - <mat-row [fxShow]="!alarmsDatasource.dataLoading" [ngClass]="{'mat-row-select': enableSelection,
  124 + <mat-row [ngClass]="{'mat-row-select': enableSelection,
125 125 'mat-selected': alarmsDatasource.isSelected(alarm),
126   - 'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm)}"
  126 + 'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm),
  127 + 'invisible': alarmsDatasource.dataLoading}"
127 128 *matRowDef="let alarm; columns: displayedColumns;"
128 129 (click)="onRowClick($event, alarm)"></mat-row>
129 130 </table>
... ...
... ... @@ -16,4 +16,23 @@
16 16 :host {
17 17 width: 100%;
18 18 height: 100%;
  19 + .tb-table-widget {
  20 + .table-container {
  21 + position: relative;
  22 + }
  23 + .mat-table {
  24 + .mat-row {
  25 + &.invisible {
  26 + visibility: hidden;
  27 + }
  28 + }
  29 + }
  30 + span.no-data-found {
  31 + position: absolute;
  32 + top: 60px;
  33 + bottom: 0;
  34 + left: 0;
  35 + right: 0;
  36 + }
  37 + }
19 38 }
... ...
... ... @@ -82,7 +82,8 @@
82 82 </mat-cell>
83 83 </ng-container>
84 84 <mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
85   - <mat-row [fxShow]="!entityDatasource.dataLoading" [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity)}"
  85 + <mat-row [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity),
  86 + 'invisible': entityDatasource.dataLoading}"
86 87 *matRowDef="let entity; columns: displayedColumns;"
87 88 (click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row>
88 89 </table>
... ...
... ... @@ -16,4 +16,23 @@
16 16 :host {
17 17 width: 100%;
18 18 height: 100%;
  19 + .tb-table-widget {
  20 + .table-container {
  21 + position: relative;
  22 + }
  23 + .mat-table {
  24 + .mat-row {
  25 + &.invisible {
  26 + visibility: hidden;
  27 + }
  28 + }
  29 + }
  30 + span.no-data-found {
  31 + position: absolute;
  32 + top: 60px;
  33 + bottom: 0;
  34 + left: 0;
  35 + right: 0;
  36 + }
  37 + }
19 38 }
... ...
... ... @@ -16,6 +16,7 @@
16 16
17 17 import L, {
18 18 FeatureGroup,
  19 + Icon,
19 20 LatLngBounds,
20 21 LatLngTuple,
21 22 markerClusterGroup,
... ... @@ -32,6 +33,7 @@ import {
32 33 MarkerSettings,
33 34 PolygonSettings,
34 35 PolylineSettings,
  36 + ReplaceInfo,
35 37 UnitedMapSettings
36 38 } from './map-models';
37 39 import { Marker } from './markers';
... ... @@ -39,7 +41,7 @@ import { BehaviorSubject, Observable, of } from 'rxjs';
39 41 import { filter } from 'rxjs/operators';
40 42 import { Polyline } from './polyline';
41 43 import { Polygon } from './polygon';
42   -import { createTooltip, parseArray, safeExecute } from '@home/components/widget/lib/maps/maps-utils';
  44 +import { createLoadingDiv, createTooltip, parseArray, safeExecute } from '@home/components/widget/lib/maps/maps-utils';
43 45 import { WidgetContext } from '@home/models/widget-component.models';
44 46 import { DatasourceData } from '@shared/models/widget.models';
45 47 import { deepClone, isDefinedAndNotNull } from '@core/utils';
... ... @@ -59,6 +61,13 @@ export default abstract class LeafletMap {
59 61 points: FeatureGroup;
60 62 markersData: FormattedData[] = [];
61 63 polygonsData: FormattedData[] = [];
  64 + defaultMarkerIconInfo: { size: number[], icon: Icon };
  65 + loadingDiv: JQuery<HTMLElement>;
  66 + loading = false;
  67 + replaceInfoLabelMarker: Array<ReplaceInfo> = [];
  68 + markerLabelText: string;
  69 + replaceInfoTooltipMarker: Array<ReplaceInfo> = [];
  70 + markerTooltipText: string;
62 71
63 72 protected constructor(public ctx: WidgetContext,
64 73 public $container: HTMLElement,
... ... @@ -168,6 +177,24 @@ export default abstract class LeafletMap {
168 177 }
169 178 }
170 179
  180 + public setLoading(loading: boolean) {
  181 + if (this.loading !== loading) {
  182 + this.loading = loading;
  183 + this.ready$.subscribe(() => {
  184 + if (this.loading) {
  185 + if (!this.loadingDiv) {
  186 + this.loadingDiv = createLoadingDiv(this.ctx.translate.instant('common.loading'));
  187 + }
  188 + this.$container.append(this.loadingDiv[0]);
  189 + } else {
  190 + if (this.loadingDiv) {
  191 + this.loadingDiv.remove();
  192 + }
  193 + }
  194 + });
  195 + }
  196 + }
  197 +
171 198 public setMap(map: L.Map) {
172 199 this.map = map;
173 200 if (this.options.useDefaultCenterPosition) {
... ... @@ -308,7 +335,11 @@ export default abstract class LeafletMap {
308 335 updateMarkers(markersData: FormattedData[], updateBounds = true, callback?) {
309 336 const rawMarkers = markersData.filter(mdata => !!this.convertPosition(mdata));
310 337 this.ready$.subscribe(() => {
311   - const keys: string[] = [];
  338 + const toDelete = new Set(Array.from(this.markers.keys()));
  339 + const createdMarkers: Marker[] = [];
  340 + const updatedMarkers: Marker[] = [];
  341 + const deletedMarkers: Marker[] = [];
  342 + let m: Marker;
312 343 rawMarkers.forEach(data => {
313 344 if (data.rotationAngle || data.rotationAngle === 0) {
314 345 const currentImage = this.options.useMarkerImageFunction ?
... ... @@ -325,22 +356,36 @@ export default abstract class LeafletMap {
325 356 this.options.icon = null;
326 357 }
327 358 if (this.markers.get(data.entityName)) {
328   - this.updateMarker(data.entityName, data, markersData, this.options)
  359 + m = this.updateMarker(data.entityName, data, markersData, this.options);
  360 + if (m) {
  361 + updatedMarkers.push(m);
  362 + }
329 363 } else {
330   - this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback);
331   - }
332   - keys.push(data.entityName);
333   - });
334   - const toDelete: string[] = [];
335   - this.markers.forEach((v, mKey) => {
336   - if (!keys.includes(mKey)) {
337   - toDelete.push(mKey);
  364 + m = this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback);
  365 + if (m) {
  366 + createdMarkers.push(m);
  367 + }
338 368 }
  369 + toDelete.delete(data.entityName);
339 370 });
340 371 toDelete.forEach((key) => {
341   - this.deleteMarker(key);
  372 + m = this.deleteMarker(key);
  373 + if (m) {
  374 + deletedMarkers.push(m);
  375 + }
342 376 });
343 377 this.markersData = markersData;
  378 + if ((this.options as MarkerSettings).useClusterMarkers) {
  379 + if (createdMarkers.length) {
  380 + this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker));
  381 + }
  382 + if (updatedMarkers.length) {
  383 + this.markersCluster.refreshClusters(updatedMarkers.map(marker => marker.leafletMarker))
  384 + }
  385 + if (deletedMarkers.length) {
  386 + this.markersCluster.removeLayers(deletedMarkers.map(marker => marker.leafletMarker));
  387 + }
  388 + }
344 389 });
345 390 }
346 391
... ... @@ -350,22 +395,20 @@ export default abstract class LeafletMap {
350 395 }
351 396
352 397 private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings,
353   - updateBounds = true, callback?) {
354   - const newMarker = new Marker(this.convertPosition(data), settings, data, dataSources, this.dragMarker);
  398 + updateBounds = true, callback?): Marker {
  399 + const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker);
355 400 if (callback)
356 401 newMarker.leafletMarker.on('click', () => { callback(data, true) });
357 402 if (this.bounds && updateBounds)
358 403 this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()));
359 404 this.markers.set(key, newMarker);
360   - if (this.options.useClusterMarkers) {
361   - this.markersCluster.addLayer(newMarker.leafletMarker);
362   - }
363   - else {
364   - this.map.addLayer(newMarker.leafletMarker);
  405 + if (!this.options.useClusterMarkers) {
  406 + this.map.addLayer(newMarker.leafletMarker);
365 407 }
  408 + return newMarker;
366 409 }
367 410
368   - private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings) {
  411 + private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings): Marker {
369 412 const marker: Marker = this.markers.get(key);
370 413 const location = this.convertPosition(data)
371 414 if (!location.equals(marker.location)) {
... ... @@ -374,24 +417,21 @@ export default abstract class LeafletMap {
374 417 if (settings.showTooltip) {
375 418 marker.updateMarkerTooltip(data);
376 419 }
377   - if (settings.useClusterMarkers) {
378   - this.markersCluster.refreshClusters()
379   - }
380 420 marker.setDataSources(data, dataSources);
381 421 marker.updateMarkerIcon(settings);
  422 + return marker;
382 423 }
383 424
384   - deleteMarker(key: string) {
385   - let marker = this.markers.get(key)?.leafletMarker;
386   - if (marker) {
387   - if (this.options.useClusterMarkers) {
388   - this.markersCluster.removeLayer(marker);
389   - } else {
390   - this.map.removeLayer(marker);
391   - }
392   - this.markers.delete(key);
393   - marker = null;
394   - }
  425 + deleteMarker(key: string): Marker {
  426 + const marker = this.markers.get(key);
  427 + const leafletMarker = marker?.leafletMarker;
  428 + if (leafletMarker) {
  429 + if (!this.options.useClusterMarkers) {
  430 + this.map.removeLayer(leafletMarker);
  431 + }
  432 + this.markers.delete(key);
  433 + }
  434 + return marker;
395 435 }
396 436
397 437 updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) {
... ...
... ... @@ -121,6 +121,12 @@ export interface FormattedData {
121 121 [key: string]: any
122 122 }
123 123
  124 +export interface ReplaceInfo {
  125 + variable: string;
  126 + valDec?: number;
  127 + dataKeyName: string
  128 +}
  129 +
124 130 export type PolygonSettings = {
125 131 showPolygon: boolean;
126 132 polygonKeyName: string;
... ...
... ... @@ -85,6 +85,7 @@ export class MapWidgetController implements MapWidgetInterface {
85 85 textSearch: null,
86 86 dynamic: true
87 87 };
  88 + this.map.setLoading(true);
88 89 this.ctx.defaultSubscription.subscribeAllForPaginatedData(this.pageLink, null);
89 90 }
90 91
... ... @@ -279,6 +280,7 @@ export class MapWidgetController implements MapWidgetInterface {
279 280 if (this.settings.draggableMarker) {
280 281 this.map.setDataSources(formattedData);
281 282 }
  283 + this.map.setLoading(false);
282 284 }
283 285
284 286 resize() {
... ...
... ... @@ -15,13 +15,12 @@
15 15 ///
16 16
17 17 import L from 'leaflet';
18   -import { FormattedData, MarkerSettings, PolygonSettings, PolylineSettings } from './map-models';
  18 +import { FormattedData, MarkerSettings, PolygonSettings, PolylineSettings, ReplaceInfo } from './map-models';
19 19 import { Datasource, DatasourceData } from '@app/shared/models/widget.models';
20 20 import _ from 'lodash';
21 21 import { Observable, Observer, of } from 'rxjs';
22 22 import { map } from 'rxjs/operators';
23   -import { createLabelFromDatasource, hashCode, isNumber, isUndefined, padValue } from '@core/utils';
24   -import { Form } from '@angular/forms';
  23 +import { createLabelFromDatasource, hashCode, isDefinedAndNotNull, isNumber, isUndefined, padValue } from '@core/utils';
25 24
26 25 export function createTooltip(target: L.Layer,
27 26 settings: MarkerSettings | PolylineSettings | PolygonSettings,
... ... @@ -185,7 +184,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
185 184 } else {
186 185 textValue = value;
187 186 }
188   - template = template.split(variable).join(textValue);
  187 + template = template.replace(variable, textValue);
189 188 match = /\${([^}]*)}/g.exec(template);
190 189 }
191 190
... ... @@ -198,7 +197,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
198 197 while (match !== null) {
199 198 [actionTags, actionName, actionText] = match;
200 199 action = createLinkElement(actionName, actionText);
201   - template = template.split(actionTags).join(action);
  200 + template = template.replace(actionTags, action);
202 201 match = linkActionRegex.exec(template);
203 202 }
204 203
... ... @@ -206,18 +205,107 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
206 205 while (match !== null) {
207 206 [actionTags, actionName, actionText] = match;
208 207 action = createButtonElement(actionName, actionText);
209   - template = template.split(actionTags).join(action);
  208 + template = template.replace(actionTags, action);
210 209 match = buttonActionRegex.exec(template);
211 210 }
212 211
213   - const compiled = _.template(template);
214   - res = compiled(data);
  212 + // const compiled = _.template(template);
  213 + // res = compiled(data);
  214 + res = template;
215 215 } catch (ex) {
216 216 console.log(ex, template)
217 217 }
218 218 return res;
219 219 }
220 220
  221 +export function processPattern(template: string, data: { $datasource?: Datasource, [key: string]: any }): Array<ReplaceInfo> {
  222 + const replaceInfo = [];
  223 + try {
  224 + const reg = /\${([^}]*)}/g;
  225 + let match = reg.exec(template);
  226 + while (match !== null) {
  227 + const variableInfo: ReplaceInfo = {
  228 + dataKeyName: '',
  229 + valDec: 2,
  230 + variable: ''
  231 + };
  232 + const variable = match[0];
  233 + let label = match[1];
  234 + let valDec = 2;
  235 + const splitValues = label.split(':');
  236 + if (splitValues.length > 1) {
  237 + label = splitValues[0];
  238 + valDec = parseFloat(splitValues[1]);
  239 + }
  240 +
  241 + variableInfo.variable = variable;
  242 + variableInfo.valDec = valDec;
  243 +
  244 + if (label.startsWith('#')) {
  245 + const keyIndexStr = label.substring(1);
  246 + const n = Math.floor(Number(keyIndexStr));
  247 + if (String(n) === keyIndexStr && n >= 0) {
  248 + variableInfo.dataKeyName = data.$datasource.dataKeys[n].label;
  249 + }
  250 + } else {
  251 + variableInfo.dataKeyName = label;
  252 + }
  253 + replaceInfo.push(variableInfo);
  254 +
  255 + match = reg.exec(template);
  256 + }
  257 + } catch (ex) {
  258 + console.log(ex, template)
  259 + }
  260 + return replaceInfo;
  261 +}
  262 +
  263 +export function fillPattern(markerLabelText: string, replaceInfoLabelMarker: Array<ReplaceInfo>, data: FormattedData) {
  264 + let text = createLabelFromDatasource(data.$datasource, markerLabelText);
  265 + if (replaceInfoLabelMarker) {
  266 + for(const variableInfo of replaceInfoLabelMarker) {
  267 + let txtVal = '';
  268 + if (variableInfo.dataKeyName && isDefinedAndNotNull(data[variableInfo.dataKeyName])) {
  269 + const varData = data[variableInfo.dataKeyName];
  270 + if (isNumber(varData)) {
  271 + txtVal = padValue(varData, variableInfo.valDec);
  272 + } else {
  273 + txtVal = varData;
  274 + }
  275 + }
  276 + text = text.replace(variableInfo.variable, txtVal);
  277 + }
  278 + }
  279 + return text;
  280 +}
  281 +
  282 +function prepareProcessPattern(template: string, translateFn?: TranslateFunc): string {
  283 + if (translateFn) {
  284 + template = translateFn(template);
  285 + }
  286 + let actionTags: string;
  287 + let actionText: string;
  288 + let actionName: string;
  289 + let action: string;
  290 +
  291 + let match = linkActionRegex.exec(template);
  292 + while (match !== null) {
  293 + [actionTags, actionName, actionText] = match;
  294 + action = createLinkElement(actionName, actionText);
  295 + template = template.replace(actionTags, action);
  296 + match = linkActionRegex.exec(template);
  297 + }
  298 +
  299 + match = buttonActionRegex.exec(template);
  300 + while (match !== null) {
  301 + [actionTags, actionName, actionText] = match;
  302 + action = createButtonElement(actionName, actionText);
  303 + template = template.replace(actionTags, action);
  304 + match = buttonActionRegex.exec(template);
  305 + }
  306 + return template;
  307 +}
  308 +
221 309 export const parseWithTranslation = {
222 310
223 311 translateFn: null,
... ... @@ -232,6 +320,9 @@ export const parseWithTranslation = {
232 320 parseTemplate(template: string, data: object, forceTranslate = false): string {
233 321 return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this));
234 322 },
  323 + prepareProcessPattern(template: string, forceTranslate = false): string {
  324 + return prepareProcessPattern(forceTranslate ? this.translate(template) : template, this.translate.bind(this));
  325 + },
235 326 setTranslate(translateFn: TranslateFunc) {
236 327 this.translateFn = translateFn;
237 328 }
... ... @@ -321,3 +412,28 @@ export function calculateNewPointCoordinate(coordinate: number, imageSize: numbe
321 412 }
322 413 return pointCoordinate;
323 414 }
  415 +
  416 +export function createLoadingDiv(loadingText: string): JQuery<HTMLElement> {
  417 + return $(`
  418 + <div style="
  419 + z-index: 12;
  420 + position: absolute;
  421 + top: 0;
  422 + bottom: 0;
  423 + left: 0;
  424 + right: 0;
  425 + flex-direction: column;
  426 + align-content: center;
  427 + align-items: center;
  428 + justify-content: center;
  429 + display: flex;
  430 + background: rgba(255,255,255,0.7);
  431 + font-size: 16px;
  432 + font-family: Roboto;
  433 + font-weight: 400;
  434 + text-transform: uppercase;
  435 + ">
  436 + <span>${loadingText}</span>
  437 + </div>
  438 + `);
  439 +}
... ...
... ... @@ -16,9 +16,18 @@
16 16
17 17 import L, { LeafletMouseEvent } from 'leaflet';
18 18 import { FormattedData, MarkerSettings } from './map-models';
19   -import { aspectCache, bindPopupActions, createTooltip, parseWithTranslation, safeExecute } from './maps-utils';
  19 +import {
  20 + aspectCache,
  21 + bindPopupActions,
  22 + createTooltip,
  23 + fillPattern,
  24 + parseWithTranslation,
  25 + processPattern,
  26 + safeExecute
  27 +} from './maps-utils';
20 28 import tinycolor from 'tinycolor2';
21 29 import { isDefined } from '@core/utils';
  30 +import LeafletMap from './leaflet-map';
22 31
23 32 export class Marker {
24 33 leafletMarker: L.Marker;
... ... @@ -29,7 +38,7 @@ export class Marker {
29 38 data: FormattedData;
30 39 dataSources: FormattedData[];
31 40
32   - constructor(location: L.LatLngExpression, public settings: MarkerSettings,
  41 + constructor(private map: LeafletMap, location: L.LatLngExpression, public settings: MarkerSettings,
33 42 data?: FormattedData, dataSources?, onDragendListener?) {
34 43 this.setDataSources(data, dataSources);
35 44 this.leafletMarker = L.marker(location, {
... ... @@ -73,9 +82,13 @@ export class Marker {
73 82 }
74 83
75 84 updateMarkerTooltip(data: FormattedData) {
  85 + if(!this.map.markerTooltipText || this.settings.useTooltipFunction) {
76 86 const pattern = this.settings.useTooltipFunction ?
77   - safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern;
78   - this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true));
  87 + safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern;
  88 + this.map.markerTooltipText = parseWithTranslation.prepareProcessPattern(pattern, true);
  89 + this.map.replaceInfoTooltipMarker = processPattern(this.map.markerTooltipText, data);
  90 + }
  91 + this.tooltip.setContent(fillPattern(this.map.markerTooltipText, this.map.replaceInfoTooltipMarker, data));
79 92 if (this.tooltip.isOpen() && this.tooltip.getElement()) {
80 93 bindPopupActions(this.tooltip, this.settings, data.$datasource);
81 94 }
... ... @@ -88,9 +101,13 @@ export class Marker {
88 101 updateMarkerLabel(settings: MarkerSettings) {
89 102 this.leafletMarker.unbindTooltip();
90 103 if (settings.showLabel) {
91   - const pattern = settings.useLabelFunction ?
  104 + if(!this.map.markerLabelText || settings.useLabelFunction) {
  105 + const pattern = settings.useLabelFunction ?
92 106 safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label;
93   - settings.labelText = parseWithTranslation.parseTemplate(pattern, this.data, true);
  107 + this.map.markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true);
  108 + this.map.replaceInfoLabelMarker = processPattern(this.map.markerLabelText, this.data);
  109 + }
  110 + settings.labelText = fillPattern(this.map.markerLabelText, this.map.replaceInfoLabelMarker, this.data);
94 111 this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`,
95 112 { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset });
96 113 }
... ... @@ -158,24 +175,24 @@ export class Marker {
158 175 }
159 176
160 177 createDefaultMarkerIcon(color, onMarkerIconReady) {
  178 + if (!this.map.defaultMarkerIconInfo) {
161 179 const icon = L.icon({
162   - iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color,
163   - iconSize: [21, 34],
164   - iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]],
165   - popupAnchor: [0, -34],
166   - shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow',
167   - shadowSize: [40, 37],
168   - shadowAnchor: [12, 35]
  180 + iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color,
  181 + iconSize: [21, 34],
  182 + iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]],
  183 + popupAnchor: [0, -34],
  184 + shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow',
  185 + shadowSize: [40, 37],
  186 + shadowAnchor: [12, 35]
169 187 });
170   - const iconInfo = {
171   - size: [21, 34],
172   - icon
  188 + this.map.defaultMarkerIconInfo = {
  189 + size: [21, 34],
  190 + icon
173 191 };
174   - onMarkerIconReady(iconInfo);
  192 + }
  193 + onMarkerIconReady(this.map.defaultMarkerIconInfo);
175 194 }
176 195
177   -
178   -
179 196 removeMarker() {
180 197 /* this.map$.subscribe(map =>
181 198 this.leafletMarker.addTo(map))*/
... ...
... ... @@ -75,6 +75,7 @@ import { DatePipe } from '@angular/common';
75 75 import { TranslateService } from '@ngx-translate/core';
76 76 import { PageLink } from '@shared/models/page/page-link';
77 77 import { SortOrder } from '@shared/models/page/sort-order';
  78 +import { DomSanitizer } from '@angular/platform-browser';
78 79
79 80 export interface IWidgetAction {
80 81 name: string;
... ... @@ -155,6 +156,7 @@ export class WidgetContext {
155 156 date: DatePipe;
156 157 translate: TranslateService;
157 158 http: HttpClient;
  159 + sanitizer: DomSanitizer;
158 160
159 161 private changeDetectorValue: ChangeDetectorRef;
160 162
... ...
... ... @@ -1382,5 +1382,11 @@ export const serviceCompletions: TbEditorCompletions = {
1382 1382 'See <a href="https://angular.io/api/common/http/HttpClient">HttpClient</a> for API reference.',
1383 1383 meta: 'service',
1384 1384 type: '<a href="https://angular.io/api/common/http/HttpClient">HttpClient</a>'
  1385 + },
  1386 + sanitizer: {
  1387 + description: 'DomSanitizer Service<br>' +
  1388 + 'See <a href="https://angular.io/api/platform-browser/DomSanitizer">DomSanitizer</a> for API reference.',
  1389 + meta: 'service',
  1390 + type: '<a href="https://angular.io/api/platform-browser/DomSanitizer">DomSanitizer</a>'
1385 1391 }
1386 1392 }
... ...
... ... @@ -579,6 +579,23 @@ export const widgetContextCompletions: TbEditorCompletions = {
579 579 }
580 580 ]
581 581 },
  582 + pushAndOpenState: {
  583 + description: 'Navigate to new dashboard state and adding intermediate states.',
  584 + meta: 'function',
  585 + args: [
  586 + {
  587 + name: 'id',
  588 + description: 'An array state object of the target dashboard state.',
  589 + type: 'Array <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/core/api/widget-api.models.ts#L140">StateObject</a>',
  590 + },
  591 + {
  592 + name: 'openRightLayout',
  593 + description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.',
  594 + type: 'boolean',
  595 + optional: true
  596 + }
  597 + ]
  598 + },
582 599 updateState: {
583 600 description: 'Updates current dashboard state.',
584 601 meta: 'function',
... ...