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

This commit is contained in:
Andrii Shvaika 2020-08-10 12:53:47 +03:00
commit d542b24ad4
16 changed files with 328 additions and 80 deletions

View File

@ -31,7 +31,6 @@ import {
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { SubscriptionTimewindow } from '@shared/models/time/time.models'; import { SubscriptionTimewindow } from '@shared/models/time/time.models';
import { AlarmDataListener } from '@core/api/alarm-data.service'; import { AlarmDataListener } from '@core/api/alarm-data.service';
import { UtilsService } from '@core/services/utils.service';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { deepClone, isDefined, isDefinedAndNotNull, isObject } from '@core/utils'; import { deepClone, isDefined, isDefinedAndNotNull, isObject } from '@core/utils';
import { simulatedAlarm } from '@shared/models/alarm.models'; import { simulatedAlarm } from '@shared/models/alarm.models';
@ -68,8 +67,7 @@ export class AlarmDataSubscription {
constructor(public alarmDataSubscriptionOptions: AlarmDataSubscriptionOptions, constructor(public alarmDataSubscriptionOptions: AlarmDataSubscriptionOptions,
private listener: AlarmDataListener, private listener: AlarmDataListener,
private telemetryService: TelemetryService, private telemetryService: TelemetryService) {
private utils: UtilsService) {
} }
public unsubscribe() { public unsubscribe() {
@ -166,7 +164,7 @@ export class AlarmDataSubscription {
private onPageData(pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) { private onPageData(pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) {
this.pageData = pageData; this.pageData = pageData;
this.resetData(); this.resetData();
this.listener.alarmsLoaded(pageData, this.alarmDataSubscriptionOptions.pageLink, allowedEntities, totalEntities); this.listener.alarmsLoaded(pageData, allowedEntities, totalEntities);
} }
private onDataUpdate(update: Array<AlarmData>) { private onDataUpdate(update: Array<AlarmData>) {

View File

@ -20,7 +20,6 @@ import { PageData } from '@shared/models/page/page-data';
import { AlarmData, AlarmDataPageLink, KeyFilter } from '@shared/models/query/query.models'; import { AlarmData, AlarmDataPageLink, KeyFilter } from '@shared/models/query/query.models';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { UtilsService } from '@core/services/utils.service';
import { import {
AlarmDataSubscription, AlarmDataSubscription,
AlarmDataSubscriptionOptions, AlarmDataSubscriptionOptions,
@ -31,7 +30,7 @@ import { deepClone } from '@core/utils';
export interface AlarmDataListener { export interface AlarmDataListener {
subscriptionTimewindow?: SubscriptionTimewindow; subscriptionTimewindow?: SubscriptionTimewindow;
alarmSource: Datasource; alarmSource: Datasource;
alarmsLoaded: (pageData: PageData<AlarmData>, pageLink: AlarmDataPageLink, allowedEntities: number, totalEntities: number) => void; alarmsLoaded: (pageData: PageData<AlarmData>, allowedEntities: number, totalEntities: number) => void;
alarmsUpdated: (update: Array<AlarmData>, pageData: PageData<AlarmData>) => void; alarmsUpdated: (update: Array<AlarmData>, pageData: PageData<AlarmData>) => void;
subscription?: AlarmDataSubscription; subscription?: AlarmDataSubscription;
} }
@ -41,8 +40,7 @@ export interface AlarmDataListener {
}) })
export class AlarmDataService { export class AlarmDataService {
constructor(private telemetryService: TelemetryWebsocketService, constructor(private telemetryService: TelemetryWebsocketService) {}
private utils: UtilsService) {}
public subscribeForAlarms(listener: AlarmDataListener, public subscribeForAlarms(listener: AlarmDataListener,
@ -88,7 +86,7 @@ export class AlarmDataService {
alarmDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; alarmDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
} }
return new AlarmDataSubscription(alarmDataSubscriptionOptions, return new AlarmDataSubscription(alarmDataSubscriptionOptions,
listener, this.telemetryService, this.utils); listener, this.telemetryService);
} }
} }

View File

@ -77,6 +77,10 @@ export function isUndefined(value: any): boolean {
return typeof value === 'undefined'; return typeof value === 'undefined';
} }
export function isUndefinedOrNull(value: any): boolean {
return typeof value === 'undefined' || value === null;
}
export function isDefined(value: any): boolean { export function isDefined(value: any): boolean {
return typeof value !== 'undefined'; return typeof value !== 'undefined';
} }
@ -452,7 +456,7 @@ export function insertVariable(pattern: string, name: string, value: any): strin
const variable = match[0]; const variable = match[0];
const variableName = match[1]; const variableName = match[1];
if (variableName === name) { if (variableName === name) {
result = result.split(variable).join(value); result = result.replace(variable, value);
} }
match = varsRegex.exec(pattern); match = varsRegex.exec(pattern);
} }
@ -469,17 +473,17 @@ export function createLabelFromDatasource(datasource: Datasource, pattern: strin
const variable = match[0]; const variable = match[0];
const variableName = match[1]; const variableName = match[1];
if (variableName === 'dsName') { if (variableName === 'dsName') {
label = label.split(variable).join(datasource.name); label = label.replace(variable, datasource.name);
} else if (variableName === 'entityName') { } else if (variableName === 'entityName') {
label = label.split(variable).join(datasource.entityName); label = label.replace(variable, datasource.entityName);
} else if (variableName === 'deviceName') { } else if (variableName === 'deviceName') {
label = label.split(variable).join(datasource.entityName); label = label.replace(variable, datasource.entityName);
} else if (variableName === 'entityLabel') { } else if (variableName === 'entityLabel') {
label = label.split(variable).join(datasource.entityLabel || datasource.entityName); label = label.replace(variable, datasource.entityLabel || datasource.entityName);
} else if (variableName === 'aliasName') { } else if (variableName === 'aliasName') {
label = label.split(variable).join(datasource.aliasName); label = label.replace(variable, datasource.aliasName);
} else if (variableName === 'entityDescription') { } else if (variableName === 'entityDescription') {
label = label.split(variable).join(datasource.entityDescription); label = label.replace(variable, datasource.entityDescription);
} }
match = varsRegex.exec(pattern); match = varsRegex.exec(pattern);
} }

View File

@ -40,6 +40,7 @@ import { DialogService } from '@core/services/dialog.service';
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DomSanitizer } from '@angular/platform-browser';
export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy { export class DynamicWidgetComponent extends PageComponent implements IDynamicWidgetComponent, OnInit, OnDestroy {
@ -74,6 +75,7 @@ export class DynamicWidgetComponent extends PageComponent implements IDynamicWid
this.ctx.date = $injector.get(DatePipe); this.ctx.date = $injector.get(DatePipe);
this.ctx.translate = $injector.get(TranslateService); this.ctx.translate = $injector.get(TranslateService);
this.ctx.http = $injector.get(HttpClient); this.ctx.http = $injector.get(HttpClient);
this.ctx.sanitizer = $injector.get(DomSanitizer);
this.ctx.$scope = this; this.ctx.$scope = this;
if (this.ctx.defaultSubscription) { if (this.ctx.defaultSubscription) {

View File

@ -121,9 +121,10 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': enableSelection}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> <mat-header-row [ngClass]="{'mat-row-select': enableSelection}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [fxShow]="!alarmsDatasource.dataLoading" [ngClass]="{'mat-row-select': enableSelection, <mat-row [ngClass]="{'mat-row-select': enableSelection,
'mat-selected': alarmsDatasource.isSelected(alarm), 'mat-selected': alarmsDatasource.isSelected(alarm),
'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm)}" 'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm),
'invisible': alarmsDatasource.dataLoading}"
*matRowDef="let alarm; columns: displayedColumns;" *matRowDef="let alarm; columns: displayedColumns;"
(click)="onRowClick($event, alarm)"></mat-row> (click)="onRowClick($event, alarm)"></mat-row>
</table> </table>

View File

@ -16,4 +16,23 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
.tb-table-widget {
.table-container {
position: relative;
}
.mat-table {
.mat-row {
&.invisible {
visibility: hidden;
}
}
}
span.no-data-found {
position: absolute;
top: 60px;
bottom: 0;
left: 0;
right: 0;
}
}
} }

View File

@ -82,7 +82,8 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [fxShow]="!entityDatasource.dataLoading" [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity)}" <mat-row [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity),
'invisible': entityDatasource.dataLoading}"
*matRowDef="let entity; columns: displayedColumns;" *matRowDef="let entity; columns: displayedColumns;"
(click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row> (click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row>
</table> </table>

View File

@ -16,4 +16,23 @@
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
.tb-table-widget {
.table-container {
position: relative;
}
.mat-table {
.mat-row {
&.invisible {
visibility: hidden;
}
}
}
span.no-data-found {
position: absolute;
top: 60px;
bottom: 0;
left: 0;
right: 0;
}
}
} }

View File

@ -16,6 +16,7 @@
import L, { import L, {
FeatureGroup, FeatureGroup,
Icon,
LatLngBounds, LatLngBounds,
LatLngTuple, LatLngTuple,
markerClusterGroup, markerClusterGroup,
@ -32,6 +33,7 @@ import {
MarkerSettings, MarkerSettings,
PolygonSettings, PolygonSettings,
PolylineSettings, PolylineSettings,
ReplaceInfo,
UnitedMapSettings UnitedMapSettings
} from './map-models'; } from './map-models';
import { Marker } from './markers'; import { Marker } from './markers';
@ -39,7 +41,7 @@ import { BehaviorSubject, Observable, of } from 'rxjs';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { Polyline } from './polyline'; import { Polyline } from './polyline';
import { Polygon } from './polygon'; import { Polygon } from './polygon';
import { createTooltip, parseArray, safeExecute } from '@home/components/widget/lib/maps/maps-utils'; import { createLoadingDiv, createTooltip, parseArray, safeExecute } from '@home/components/widget/lib/maps/maps-utils';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { DatasourceData } from '@shared/models/widget.models'; import { DatasourceData } from '@shared/models/widget.models';
import { deepClone, isDefinedAndNotNull } from '@core/utils'; import { deepClone, isDefinedAndNotNull } from '@core/utils';
@ -59,6 +61,13 @@ export default abstract class LeafletMap {
points: FeatureGroup; points: FeatureGroup;
markersData: FormattedData[] = []; markersData: FormattedData[] = [];
polygonsData: FormattedData[] = []; polygonsData: FormattedData[] = [];
defaultMarkerIconInfo: { size: number[], icon: Icon };
loadingDiv: JQuery<HTMLElement>;
loading = false;
replaceInfoLabelMarker: Array<ReplaceInfo> = [];
markerLabelText: string;
replaceInfoTooltipMarker: Array<ReplaceInfo> = [];
markerTooltipText: string;
protected constructor(public ctx: WidgetContext, protected constructor(public ctx: WidgetContext,
public $container: HTMLElement, public $container: HTMLElement,
@ -168,6 +177,24 @@ export default abstract class LeafletMap {
} }
} }
public setLoading(loading: boolean) {
if (this.loading !== loading) {
this.loading = loading;
this.ready$.subscribe(() => {
if (this.loading) {
if (!this.loadingDiv) {
this.loadingDiv = createLoadingDiv(this.ctx.translate.instant('common.loading'));
}
this.$container.append(this.loadingDiv[0]);
} else {
if (this.loadingDiv) {
this.loadingDiv.remove();
}
}
});
}
}
public setMap(map: L.Map) { public setMap(map: L.Map) {
this.map = map; this.map = map;
if (this.options.useDefaultCenterPosition) { if (this.options.useDefaultCenterPosition) {
@ -308,7 +335,11 @@ export default abstract class LeafletMap {
updateMarkers(markersData: FormattedData[], updateBounds = true, callback?) { updateMarkers(markersData: FormattedData[], updateBounds = true, callback?) {
const rawMarkers = markersData.filter(mdata => !!this.convertPosition(mdata)); const rawMarkers = markersData.filter(mdata => !!this.convertPosition(mdata));
this.ready$.subscribe(() => { this.ready$.subscribe(() => {
const keys: string[] = []; const toDelete = new Set(Array.from(this.markers.keys()));
const createdMarkers: Marker[] = [];
const updatedMarkers: Marker[] = [];
const deletedMarkers: Marker[] = [];
let m: Marker;
rawMarkers.forEach(data => { rawMarkers.forEach(data => {
if (data.rotationAngle || data.rotationAngle === 0) { if (data.rotationAngle || data.rotationAngle === 0) {
const currentImage = this.options.useMarkerImageFunction ? const currentImage = this.options.useMarkerImageFunction ?
@ -325,22 +356,36 @@ export default abstract class LeafletMap {
this.options.icon = null; this.options.icon = null;
} }
if (this.markers.get(data.entityName)) { if (this.markers.get(data.entityName)) {
this.updateMarker(data.entityName, data, markersData, this.options) m = this.updateMarker(data.entityName, data, markersData, this.options);
if (m) {
updatedMarkers.push(m);
}
} else { } else {
this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback); m = this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback);
} if (m) {
keys.push(data.entityName); createdMarkers.push(m);
}); }
const toDelete: string[] = [];
this.markers.forEach((v, mKey) => {
if (!keys.includes(mKey)) {
toDelete.push(mKey);
} }
toDelete.delete(data.entityName);
}); });
toDelete.forEach((key) => { toDelete.forEach((key) => {
this.deleteMarker(key); m = this.deleteMarker(key);
if (m) {
deletedMarkers.push(m);
}
}); });
this.markersData = markersData; this.markersData = markersData;
if ((this.options as MarkerSettings).useClusterMarkers) {
if (createdMarkers.length) {
this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker));
}
if (updatedMarkers.length) {
this.markersCluster.refreshClusters(updatedMarkers.map(marker => marker.leafletMarker))
}
if (deletedMarkers.length) {
this.markersCluster.removeLayers(deletedMarkers.map(marker => marker.leafletMarker));
}
}
}); });
} }
@ -350,22 +395,20 @@ export default abstract class LeafletMap {
} }
private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings,
updateBounds = true, callback?) { updateBounds = true, callback?): Marker {
const newMarker = new Marker(this.convertPosition(data), settings, data, dataSources, this.dragMarker); const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker);
if (callback) if (callback)
newMarker.leafletMarker.on('click', () => { callback(data, true) }); newMarker.leafletMarker.on('click', () => { callback(data, true) });
if (this.bounds && updateBounds) if (this.bounds && updateBounds)
this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng())); this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()));
this.markers.set(key, newMarker); this.markers.set(key, newMarker);
if (this.options.useClusterMarkers) { if (!this.options.useClusterMarkers) {
this.markersCluster.addLayer(newMarker.leafletMarker); this.map.addLayer(newMarker.leafletMarker);
}
else {
this.map.addLayer(newMarker.leafletMarker);
} }
return newMarker;
} }
private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings) { private updateMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings): Marker {
const marker: Marker = this.markers.get(key); const marker: Marker = this.markers.get(key);
const location = this.convertPosition(data) const location = this.convertPosition(data)
if (!location.equals(marker.location)) { if (!location.equals(marker.location)) {
@ -374,24 +417,21 @@ export default abstract class LeafletMap {
if (settings.showTooltip) { if (settings.showTooltip) {
marker.updateMarkerTooltip(data); marker.updateMarkerTooltip(data);
} }
if (settings.useClusterMarkers) {
this.markersCluster.refreshClusters()
}
marker.setDataSources(data, dataSources); marker.setDataSources(data, dataSources);
marker.updateMarkerIcon(settings); marker.updateMarkerIcon(settings);
return marker;
} }
deleteMarker(key: string) { deleteMarker(key: string): Marker {
let marker = this.markers.get(key)?.leafletMarker; const marker = this.markers.get(key);
if (marker) { const leafletMarker = marker?.leafletMarker;
if (this.options.useClusterMarkers) { if (leafletMarker) {
this.markersCluster.removeLayer(marker); if (!this.options.useClusterMarkers) {
} else { this.map.removeLayer(leafletMarker);
this.map.removeLayer(marker); }
} this.markers.delete(key);
this.markers.delete(key); }
marker = null; return marker;
}
} }
updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) { updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) {

View File

@ -121,6 +121,12 @@ export interface FormattedData {
[key: string]: any [key: string]: any
} }
export interface ReplaceInfo {
variable: string;
valDec?: number;
dataKeyName: string
}
export type PolygonSettings = { export type PolygonSettings = {
showPolygon: boolean; showPolygon: boolean;
polygonKeyName: string; polygonKeyName: string;

View File

@ -85,6 +85,7 @@ export class MapWidgetController implements MapWidgetInterface {
textSearch: null, textSearch: null,
dynamic: true dynamic: true
}; };
this.map.setLoading(true);
this.ctx.defaultSubscription.subscribeAllForPaginatedData(this.pageLink, null); this.ctx.defaultSubscription.subscribeAllForPaginatedData(this.pageLink, null);
} }
@ -279,6 +280,7 @@ export class MapWidgetController implements MapWidgetInterface {
if (this.settings.draggableMarker) { if (this.settings.draggableMarker) {
this.map.setDataSources(formattedData); this.map.setDataSources(formattedData);
} }
this.map.setLoading(false);
} }
resize() { resize() {

View File

@ -15,13 +15,12 @@
/// ///
import L from 'leaflet'; import L from 'leaflet';
import { FormattedData, MarkerSettings, PolygonSettings, PolylineSettings } from './map-models'; import { FormattedData, MarkerSettings, PolygonSettings, PolylineSettings, ReplaceInfo } from './map-models';
import { Datasource, DatasourceData } from '@app/shared/models/widget.models'; import { Datasource, DatasourceData } from '@app/shared/models/widget.models';
import _ from 'lodash'; import _ from 'lodash';
import { Observable, Observer, of } from 'rxjs'; import { Observable, Observer, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { createLabelFromDatasource, hashCode, isNumber, isUndefined, padValue } from '@core/utils'; import { createLabelFromDatasource, hashCode, isDefinedAndNotNull, isNumber, isUndefined, padValue } from '@core/utils';
import { Form } from '@angular/forms';
export function createTooltip(target: L.Layer, export function createTooltip(target: L.Layer,
settings: MarkerSettings | PolylineSettings | PolygonSettings, settings: MarkerSettings | PolylineSettings | PolygonSettings,
@ -185,7 +184,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
} else { } else {
textValue = value; textValue = value;
} }
template = template.split(variable).join(textValue); template = template.replace(variable, textValue);
match = /\${([^}]*)}/g.exec(template); match = /\${([^}]*)}/g.exec(template);
} }
@ -198,7 +197,7 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
while (match !== null) { while (match !== null) {
[actionTags, actionName, actionText] = match; [actionTags, actionName, actionText] = match;
action = createLinkElement(actionName, actionText); action = createLinkElement(actionName, actionText);
template = template.split(actionTags).join(action); template = template.replace(actionTags, action);
match = linkActionRegex.exec(template); match = linkActionRegex.exec(template);
} }
@ -206,18 +205,107 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
while (match !== null) { while (match !== null) {
[actionTags, actionName, actionText] = match; [actionTags, actionName, actionText] = match;
action = createButtonElement(actionName, actionText); action = createButtonElement(actionName, actionText);
template = template.split(actionTags).join(action); template = template.replace(actionTags, action);
match = buttonActionRegex.exec(template); match = buttonActionRegex.exec(template);
} }
const compiled = _.template(template); // const compiled = _.template(template);
res = compiled(data); // res = compiled(data);
res = template;
} catch (ex) { } catch (ex) {
console.log(ex, template) console.log(ex, template)
} }
return res; return res;
} }
export function processPattern(template: string, data: { $datasource?: Datasource, [key: string]: any }): Array<ReplaceInfo> {
const replaceInfo = [];
try {
const reg = /\${([^}]*)}/g;
let match = reg.exec(template);
while (match !== null) {
const variableInfo: ReplaceInfo = {
dataKeyName: '',
valDec: 2,
variable: ''
};
const variable = match[0];
let label = match[1];
let valDec = 2;
const splitValues = label.split(':');
if (splitValues.length > 1) {
label = splitValues[0];
valDec = parseFloat(splitValues[1]);
}
variableInfo.variable = variable;
variableInfo.valDec = valDec;
if (label.startsWith('#')) {
const keyIndexStr = label.substring(1);
const n = Math.floor(Number(keyIndexStr));
if (String(n) === keyIndexStr && n >= 0) {
variableInfo.dataKeyName = data.$datasource.dataKeys[n].label;
}
} else {
variableInfo.dataKeyName = label;
}
replaceInfo.push(variableInfo);
match = reg.exec(template);
}
} catch (ex) {
console.log(ex, template)
}
return replaceInfo;
}
export function fillPattern(markerLabelText: string, replaceInfoLabelMarker: Array<ReplaceInfo>, data: FormattedData) {
let text = createLabelFromDatasource(data.$datasource, markerLabelText);
if (replaceInfoLabelMarker) {
for(const variableInfo of replaceInfoLabelMarker) {
let txtVal = '';
if (variableInfo.dataKeyName && isDefinedAndNotNull(data[variableInfo.dataKeyName])) {
const varData = data[variableInfo.dataKeyName];
if (isNumber(varData)) {
txtVal = padValue(varData, variableInfo.valDec);
} else {
txtVal = varData;
}
}
text = text.replace(variableInfo.variable, txtVal);
}
}
return text;
}
function prepareProcessPattern(template: string, translateFn?: TranslateFunc): string {
if (translateFn) {
template = translateFn(template);
}
let actionTags: string;
let actionText: string;
let actionName: string;
let action: string;
let match = linkActionRegex.exec(template);
while (match !== null) {
[actionTags, actionName, actionText] = match;
action = createLinkElement(actionName, actionText);
template = template.replace(actionTags, action);
match = linkActionRegex.exec(template);
}
match = buttonActionRegex.exec(template);
while (match !== null) {
[actionTags, actionName, actionText] = match;
action = createButtonElement(actionName, actionText);
template = template.replace(actionTags, action);
match = buttonActionRegex.exec(template);
}
return template;
}
export const parseWithTranslation = { export const parseWithTranslation = {
translateFn: null, translateFn: null,
@ -232,6 +320,9 @@ export const parseWithTranslation = {
parseTemplate(template: string, data: object, forceTranslate = false): string { parseTemplate(template: string, data: object, forceTranslate = false): string {
return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this)); return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this));
}, },
prepareProcessPattern(template: string, forceTranslate = false): string {
return prepareProcessPattern(forceTranslate ? this.translate(template) : template, this.translate.bind(this));
},
setTranslate(translateFn: TranslateFunc) { setTranslate(translateFn: TranslateFunc) {
this.translateFn = translateFn; this.translateFn = translateFn;
} }
@ -321,3 +412,28 @@ export function calculateNewPointCoordinate(coordinate: number, imageSize: numbe
} }
return pointCoordinate; return pointCoordinate;
} }
export function createLoadingDiv(loadingText: string): JQuery<HTMLElement> {
return $(`
<div style="
z-index: 12;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
flex-direction: column;
align-content: center;
align-items: center;
justify-content: center;
display: flex;
background: rgba(255,255,255,0.7);
font-size: 16px;
font-family: Roboto;
font-weight: 400;
text-transform: uppercase;
">
<span>${loadingText}</span>
</div>
`);
}

View File

@ -16,9 +16,18 @@
import L, { LeafletMouseEvent } from 'leaflet'; import L, { LeafletMouseEvent } from 'leaflet';
import { FormattedData, MarkerSettings } from './map-models'; import { FormattedData, MarkerSettings } from './map-models';
import { aspectCache, bindPopupActions, createTooltip, parseWithTranslation, safeExecute } from './maps-utils'; import {
aspectCache,
bindPopupActions,
createTooltip,
fillPattern,
parseWithTranslation,
processPattern,
safeExecute
} from './maps-utils';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { isDefined } from '@core/utils'; import { isDefined } from '@core/utils';
import LeafletMap from './leaflet-map';
export class Marker { export class Marker {
leafletMarker: L.Marker; leafletMarker: L.Marker;
@ -29,7 +38,7 @@ export class Marker {
data: FormattedData; data: FormattedData;
dataSources: FormattedData[]; dataSources: FormattedData[];
constructor(location: L.LatLngExpression, public settings: MarkerSettings, constructor(private map: LeafletMap, location: L.LatLngExpression, public settings: MarkerSettings,
data?: FormattedData, dataSources?, onDragendListener?) { data?: FormattedData, dataSources?, onDragendListener?) {
this.setDataSources(data, dataSources); this.setDataSources(data, dataSources);
this.leafletMarker = L.marker(location, { this.leafletMarker = L.marker(location, {
@ -73,9 +82,13 @@ export class Marker {
} }
updateMarkerTooltip(data: FormattedData) { updateMarkerTooltip(data: FormattedData) {
if(!this.map.markerTooltipText || this.settings.useTooltipFunction) {
const pattern = this.settings.useTooltipFunction ? const pattern = this.settings.useTooltipFunction ?
safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern; safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern;
this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true)); this.map.markerTooltipText = parseWithTranslation.prepareProcessPattern(pattern, true);
this.map.replaceInfoTooltipMarker = processPattern(this.map.markerTooltipText, data);
}
this.tooltip.setContent(fillPattern(this.map.markerTooltipText, this.map.replaceInfoTooltipMarker, data));
if (this.tooltip.isOpen() && this.tooltip.getElement()) { if (this.tooltip.isOpen() && this.tooltip.getElement()) {
bindPopupActions(this.tooltip, this.settings, data.$datasource); bindPopupActions(this.tooltip, this.settings, data.$datasource);
} }
@ -88,9 +101,13 @@ export class Marker {
updateMarkerLabel(settings: MarkerSettings) { updateMarkerLabel(settings: MarkerSettings) {
this.leafletMarker.unbindTooltip(); this.leafletMarker.unbindTooltip();
if (settings.showLabel) { if (settings.showLabel) {
const pattern = settings.useLabelFunction ? if(!this.map.markerLabelText || settings.useLabelFunction) {
const pattern = settings.useLabelFunction ?
safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label; safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label;
settings.labelText = parseWithTranslation.parseTemplate(pattern, this.data, true); this.map.markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true);
this.map.replaceInfoLabelMarker = processPattern(this.map.markerLabelText, this.data);
}
settings.labelText = fillPattern(this.map.markerLabelText, this.map.replaceInfoLabelMarker, this.data);
this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`, this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`,
{ className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset }); { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset });
} }
@ -158,24 +175,24 @@ export class Marker {
} }
createDefaultMarkerIcon(color, onMarkerIconReady) { createDefaultMarkerIcon(color, onMarkerIconReady) {
if (!this.map.defaultMarkerIconInfo) {
const icon = L.icon({ const icon = L.icon({
iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color, iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color,
iconSize: [21, 34], iconSize: [21, 34],
iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]], iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]],
popupAnchor: [0, -34], popupAnchor: [0, -34],
shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow', shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow',
shadowSize: [40, 37], shadowSize: [40, 37],
shadowAnchor: [12, 35] shadowAnchor: [12, 35]
}); });
const iconInfo = { this.map.defaultMarkerIconInfo = {
size: [21, 34], size: [21, 34],
icon icon
}; };
onMarkerIconReady(iconInfo); }
onMarkerIconReady(this.map.defaultMarkerIconInfo);
} }
removeMarker() { removeMarker() {
/* this.map$.subscribe(map => /* this.map$.subscribe(map =>
this.leafletMarker.addTo(map))*/ this.leafletMarker.addTo(map))*/

View File

@ -75,6 +75,7 @@ import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { SortOrder } from '@shared/models/page/sort-order'; import { SortOrder } from '@shared/models/page/sort-order';
import { DomSanitizer } from '@angular/platform-browser';
export interface IWidgetAction { export interface IWidgetAction {
name: string; name: string;
@ -155,6 +156,7 @@ export class WidgetContext {
date: DatePipe; date: DatePipe;
translate: TranslateService; translate: TranslateService;
http: HttpClient; http: HttpClient;
sanitizer: DomSanitizer;
private changeDetectorValue: ChangeDetectorRef; private changeDetectorValue: ChangeDetectorRef;

View File

@ -1382,5 +1382,11 @@ export const serviceCompletions: TbEditorCompletions = {
'See <a href="https://angular.io/api/common/http/HttpClient">HttpClient</a> for API reference.', 'See <a href="https://angular.io/api/common/http/HttpClient">HttpClient</a> for API reference.',
meta: 'service', meta: 'service',
type: '<a href="https://angular.io/api/common/http/HttpClient">HttpClient</a>' type: '<a href="https://angular.io/api/common/http/HttpClient">HttpClient</a>'
},
sanitizer: {
description: 'DomSanitizer Service<br>' +
'See <a href="https://angular.io/api/platform-browser/DomSanitizer">DomSanitizer</a> for API reference.',
meta: 'service',
type: '<a href="https://angular.io/api/platform-browser/DomSanitizer">DomSanitizer</a>'
} }
} }

View File

@ -579,6 +579,23 @@ export const widgetContextCompletions: TbEditorCompletions = {
} }
] ]
}, },
pushAndOpenState: {
description: 'Navigate to new dashboard state and adding intermediate states.',
meta: 'function',
args: [
{
name: 'id',
description: 'An array state object of the target dashboard state.',
type: 'Array <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/core/api/widget-api.models.ts#L140">StateObject</a>',
},
{
name: 'openRightLayout',
description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.',
type: 'boolean',
optional: true
}
]
},
updateState: { updateState: {
description: 'Updates current dashboard state.', description: 'Updates current dashboard state.',
meta: 'function', meta: 'function',