Commit d542b24ad44c527138a3f86228e5f1c5846f5386
Merge branch 'master' of github.com:thingsboard/thingsboard
Showing
16 changed files
with
328 additions
and
80 deletions
... | ... | @@ -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', | ... | ... |