Browse Source

UI: Map data layers implementation.

pull/12723/head
Igor Kulikov 1 year ago
parent
commit
e08f05ced3
  1. 11
      ui-ngx/src/app/core/utils.ts
  2. 4
      ui-ngx/src/app/modules/common/modules-map.ts
  3. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html
  4. 2
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts
  5. 2
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  6. 2
      ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts
  7. 3
      ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
  8. 10
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  9. 9
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts
  10. 170
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts
  11. 213
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts
  12. 86
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts
  13. 53
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html
  14. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.models.ts
  15. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.scss
  16. 11
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts
  17. 129
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html
  18. 45
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss
  19. 380
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts
  20. 51
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html
  21. 42
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.scss
  22. 177
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts
  23. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html
  24. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts
  25. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  26. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html
  27. 1
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  28. 22
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html
  29. 7
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  30. 7
      ui-ngx/src/app/shared/models/widget.models.ts
  31. 34
      ui-ngx/src/assets/locale/locale.constant-en_US.json

11
ui-ngx/src/app/core/utils.ts

@ -491,11 +491,12 @@ export const createLabelFromSubscriptionEntityInfo = (entityInfo: SubscriptionEn
export const hasDatasourceLabelsVariables = (pattern: string): boolean => varsRegex.test(pattern) !== null;
export function formattedDataFormDatasourceData(input: DatasourceData[], dataIndex?: number, ts?: number): FormattedData[] {
return _(input).groupBy(el => el.datasource.entityName + el.datasource.entityType)
export function formattedDataFormDatasourceData<D extends Datasource = Datasource>(input: DatasourceData[], dataIndex?: number, ts?: number,
groupFunction: (el: DatasourceData) => any = (el) => el.datasource.entityName + el.datasource.entityType): FormattedData<D>[] {
return _(input).groupBy(groupFunction)
.values().value().map((entityArray, i) => {
const datasource = entityArray[0].datasource;
const obj = formattedDataFromDatasource(datasource, i);
const datasource = entityArray[0].datasource as D;
const obj = formattedDataFromDatasource<D>(datasource, i);
entityArray.filter(el => el.data.length).forEach(el => {
const index = isDefined(dataIndex) ? dataIndex : el.data.length - 1;
const dataSet = isDefined(ts) ? el.data.find(data => data[0] === ts) : el.data[index];
@ -537,7 +538,7 @@ export function formattedDataArrayFromDatasourceData(input: DatasourceData[]): F
});
}
export function formattedDataFromDatasource(datasource: Datasource, dsIndex: number): FormattedData {
export function formattedDataFromDatasource<D extends Datasource = Datasource>(datasource: D, dsIndex: number): FormattedData<D> {
return {
entityName: datasource.entityName,
deviceName: datasource.entityName,

4
ui-ngx/src/app/modules/common/modules-map.ts

@ -231,7 +231,7 @@ import * as EntityFilterViewComponent from '@home/components/entity/entity-filte
import * as EntityAliasDialogComponent from '@home/components/alias/entity-alias-dialog.component';
import * as EntityFilterComponent from '@home/components/entity/entity-filter.component';
import * as RelationFiltersComponent from '@home/components/relation/relation-filters.component';
import * as EntityAliasSelectComponent from '@home/components/alias/entity-alias-select.component';
import * as EntityAliasSelectComponent from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component';
import * as DataKeysComponent from '@home/components/widget/config/data-keys.component';
import * as DataKeyConfigDialogComponent from '@home/components/widget/config/data-key-config-dialog.component';
import * as DataKeyConfigComponent from '@home/components/widget/config/data-key-config.component';
@ -577,7 +577,7 @@ class ModulesMap implements IModulesMap {
'@home/components/alias/entity-alias-dialog.component': EntityAliasDialogComponent,
'@home/components/entity/entity-filter.component': EntityFilterComponent,
'@home/components/relation/relation-filters.component': RelationFiltersComponent,
'@home/components/alias/entity-alias-select.component': EntityAliasSelectComponent,
'@home/components/widget/lib/settings/common/alias/entity-alias-select.component': EntityAliasSelectComponent,
'@home/components/widget/config/data-keys.component': DataKeysComponent,
'@home/components/widget/config/data-key-config-dialog.component': DataKeyConfigDialogComponent,
'@home/components/widget/config/data-key-config.component': DataKeyConfigComponent,

8
ui-ngx/src/app/modules/home/components/widget/config/basic/map/map-basic-config.component.html

@ -16,7 +16,13 @@
-->
<ng-container [formGroup]="mapWidgetConfigForm">
<tb-map-settings formControlName="mapSettings"></tb-map-settings>
<tb-map-settings
[functionsOnly]="functionsOnly"
[aliasController]="aliasController"
[callbacks]="callbacks"
[widget]="widget"
formControlName="mapSettings">
</tb-map-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div class="tb-form-row column-xs">

2
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models';
import { EntityAliasSelectCallbacks } from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component.models';
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';

2
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts

@ -37,7 +37,7 @@ import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { IAliasController } from '@core/api/widget-api.models';
import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models';
import { EntityAliasSelectCallbacks } from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component.models';
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { EntityType } from '@shared/models/entity-type.models';

2
ui-ngx/src/app/modules/home/components/widget/config/target-device.component.ts

@ -31,7 +31,7 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com
import { TargetDevice, TargetDeviceType } from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { IAliasController } from '@core/api/widget-api.models';
import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models';
import { EntityAliasSelectCallbacks } from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({

3
ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts

@ -24,7 +24,6 @@ import { DataKeyConfigDialogComponent } from '@home/components/widget/config/dat
import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component';
import { DatasourceComponent } from '@home/components/widget/config/datasource.component';
import { DatasourcesComponent } from '@home/components/widget/config/datasources.component';
import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-select.component';
import { FilterSelectComponent } from '@home/components/filter/filter-select.component';
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component';
@ -45,7 +44,6 @@ import { TargetDeviceComponent } from '@home/components/widget/config/target-dev
DatasourceComponent,
DatasourcesComponent,
TargetDeviceComponent,
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowStyleComponent,
TimewindowStylePanelComponent,
@ -67,7 +65,6 @@ import { TargetDeviceComponent } from '@home/components/widget/config/target-dev
DatasourceComponent,
DatasourcesComponent,
TargetDeviceComponent,
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowStyleComponent,
TimewindowStylePanelComponent,

10
ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts

@ -23,7 +23,7 @@ import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { DataKey, DatasourceType, WidgetConfigMode, widgetType } from '@shared/models/widget.models';
import { DataKey, DatasourceType, Widget, WidgetConfigMode, widgetType } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { isDefinedAndNotNull } from '@core/utils';
import { IAliasController } from '@core/api/widget-api.models';
@ -67,6 +67,10 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
return this.widgetConfigComponent.widgetConfigCallbacks;
}
get functionsOnly(): boolean {
return this.widgetConfigComponent.functionsOnly;
}
get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType;
}
@ -75,6 +79,10 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
return this.widgetConfigComponent.widgetEditMode;
}
get widget(): Widget {
return this.widgetConfigComponent.widget;
}
widgetConfigChangedEmitter = new EventEmitter<WidgetConfigComponentData>();
widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable();

9
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts

@ -75,6 +75,9 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
@Input()
callbacks: WidgetConfigCallbacks;
@Input()
functionsOnly: boolean;
@Input()
dashboard: Dashboard;
@ -140,6 +143,11 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
this.definedSettingsComponent.dataKeyCallbacks = this.callbacks;
}
}
if (propName === 'functionsOnly') {
if (this.definedSettingsComponent) {
this.definedSettingsComponent.functionsOnly = this.functionsOnly;
}
}
if (propName === 'widgetConfig') {
if (this.definedSettingsComponent) {
this.definedSettingsComponent.widgetConfig = this.widgetConfig;
@ -229,6 +237,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
this.definedSettingsComponent = this.definedSettingsComponentRef.instance;
this.definedSettingsComponent.aliasController = this.aliasController;
this.definedSettingsComponent.callbacks = this.callbacks;
this.definedSettingsComponent.functionsOnly = this.functionsOnly;
this.definedSettingsComponent.dataKeyCallbacks = this.callbacks;
this.definedSettingsComponent.dashboard = this.dashboard;
this.definedSettingsComponent.widget = this.widget;

170
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts

@ -0,0 +1,170 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
CirclesDataLayerSettings,
MapDataLayerSettings, mapDataSourceSettingsToDatasource,
MarkersDataLayerSettings, PolygonsDataLayerSettings, TbMapDatasource
} from '@home/components/widget/lib/maps/map.models';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { FormattedData } from '@shared/models/widget.models';
import { Observable, of } from 'rxjs';
import { guid } from '@core/utils';
import L from 'leaflet';
abstract class TbDataLayerItem<S extends MapDataLayerSettings, L extends TbMapDataLayer<S>> {
protected layer: L.Layer;
}
export enum MapDataLayerType {
marker = 'marker',
polygon = 'polygon',
circle = 'circle'
}
export abstract class TbMapDataLayer<S extends MapDataLayerSettings> {
protected datasource: TbMapDatasource;
protected mapDataId = guid();
protected constructor(protected map: TbMap<any>,
protected settings: S) {
}
public setup(): Observable<void> {
this.datasource = mapDataSourceSettingsToDatasource(this.settings);
this.datasource.dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : [];
this.mapDataId = this.datasource.mapDataIds[0];
this.datasource = this.setupDatasource(this.datasource);
return this.doSetup();
}
public getDatasource(): TbMapDatasource {
return this.datasource;
}
public updateData(dsData: FormattedData<TbMapDatasource>[]) {
const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId));
this.onData(layerData, dsData);
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
return datasource;
}
public abstract dataLayerType(): MapDataLayerType;
protected abstract doSetup(): Observable<void>;
protected abstract onData(layerData: FormattedData<TbMapDatasource>[], dsData: FormattedData<TbMapDatasource>[]);
}
export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings> {
constructor(protected map: TbMap<any>,
protected settings: MarkersDataLayerSettings) {
super(map, settings);
}
public dataLayerType(): MapDataLayerType {
return MapDataLayerType.marker;
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys.push(this.settings.xKey, this.settings.yKey);
return datasource;
}
protected doSetup(): Observable<void> {
return of(null);
}
protected onData(layerData: FormattedData<TbMapDatasource>[], dsData: FormattedData<TbMapDatasource>[]) {
layerData.forEach((data, index) => {
console.log(`[${this.mapDataId}][${index}]: Markers layer data updated!`);
console.log(data);
this.markerData(data, dsData);
});
}
private markerData(data: FormattedData, dsData: FormattedData[]) {
const xKeyVal = data[this.settings.xKey.label];
const yKeyVal = data[this.settings.yKey.label];
}
}
export class TbPolygonsDataLayer extends TbMapDataLayer<PolygonsDataLayerSettings> {
constructor(protected map: TbMap<any>,
protected settings: PolygonsDataLayerSettings) {
super(map, settings);
}
public dataLayerType(): MapDataLayerType {
return MapDataLayerType.polygon;
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys.push(this.settings.polygonKey);
return datasource;
}
protected doSetup(): Observable<void> {
return of(null);
}
protected onData(layerData: FormattedData<TbMapDatasource>[], dsData: FormattedData<TbMapDatasource>[]) {
layerData.forEach((data, index) => {
console.log(`[${this.mapDataId}][${index}]: Polygons layer data updated!`);
console.log(data);
});
}
}
export class TbCirclesDataLayer extends TbMapDataLayer<CirclesDataLayerSettings> {
constructor(protected map: TbMap<any>,
protected settings: CirclesDataLayerSettings) {
super(map, settings);
}
public dataLayerType(): MapDataLayerType {
return MapDataLayerType.circle;
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys.push(this.settings.circleKey);
return datasource;
}
protected doSetup(): Observable<void> {
return of(null);
}
protected onData(layerData: FormattedData<TbMapDatasource>[], dsData: FormattedData<TbMapDatasource>[]) {
layerData.forEach((data, index) => {
console.log(`[${this.mapDataId}][${index}]: Circles layer data updated!`);
console.log(data);
});
}
}

213
ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts

@ -14,11 +14,11 @@
/// limitations under the License.
///
import { DataKey, DatasourceType } from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DataKey, Datasource, DatasourceType } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { mergeDeep } from '@core/utils';
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { guid, mergeDeep } from '@core/utils';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { materialColors } from '@shared/models/material.models';
export enum MapType {
geoMap = 'geoMap',
@ -27,33 +27,133 @@ export enum MapType {
export interface MapDataSourceSettings {
dsType: DatasourceType;
dsEntityType?: EntityType;
dsEntityId?: string;
dsDeviceId?: string;
dsEntityAliasId?: string;
dsFilterId?: string;
}
export interface TbMapDatasource extends Datasource {
mapDataIds: string[];
}
export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings): TbMapDatasource => {
return {
type: settings.dsType,
deviceId: settings.dsDeviceId,
entityAliasId: settings.dsEntityAliasId,
filterId: settings.dsFilterId,
dataKeys: [],
mapDataIds: [guid()]
};
};
export interface MapDataLayerSettings extends MapDataSourceSettings {
additionalDataKeys?: DataKey[];
group?: string;
}
export type MapDataLayerType = 'markers' | 'polygons' | 'circles';
export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapDataLayerType): boolean => {
if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) {
return false;
}
switch (dataLayer.dsType) {
case DatasourceType.function:
break;
case DatasourceType.device:
if (!dataLayer.dsDeviceId) {
return false;
}
break;
case DatasourceType.entity:
if (!dataLayer.dsEntityAliasId) {
return false;
}
break;
}
switch (type) {
case 'markers':
const markersDataLayer = dataLayer as MarkersDataLayerSettings;
if (!markersDataLayer.xKey?.type || !markersDataLayer.xKey?.name ||
!markersDataLayer.yKey?.type || !markersDataLayer.xKey?.name) {
return false;
}
break;
case 'polygons':
const polygonsDataLayer = dataLayer as PolygonsDataLayerSettings;
if (!polygonsDataLayer.polygonKey?.type || !polygonsDataLayer.polygonKey?.name) {
return false;
}
break;
case 'circles':
const circlesDataLayer = dataLayer as CirclesDataLayerSettings;
if (!circlesDataLayer.circleKey?.type || !circlesDataLayer.circleKey?.name) {
return false;
}
break;
}
return true;
};
export const mapDataLayerValidator = (type: MapDataLayerType): ValidatorFn => {
return (control: AbstractControl): ValidationErrors | null => {
const layer: MapDataLayerSettings = control.value;
if (!mapDataLayerValid(layer, type)) {
return {
layer: true
};
}
return null;
};
};
export interface MarkersDataLayerSettings extends MapDataLayerSettings {
xKey: DataKey;
yKey: DataKey;
}
export const defaultMarkersDataLayerSettings = (mapType: MapType): MarkersDataLayerSettings => ({
dsType: DatasourceType.entity,
const defaultMarkerLatitudeFunction = 'var value = prevValue || 15.833293;\n' +
'if (time % 5000 < 500) {\n' +
' value += Math.random() * 0.05 - 0.025;\n' +
'}\n' +
'return value;';
const defaultMarkerLongitudeFunction = 'var value = prevValue || -90.454350;\n' +
'if (time % 5000 < 500) {\n' +
' value += Math.random() * 0.05 - 0.025;\n' +
'}\n' +
'return value;';
const defaultMarkerXPosFunction = 'var value = prevValue || 0.2;\n' +
'if (time % 5000 < 500) {\n' +
' value += Math.random() * 0.05 - 0.025;\n' +
'}\n' +
'return value;';
const defaultMarkerYPosFunction = 'var value = prevValue || 0.3;\n' +
'if (time % 5000 < 500) {\n' +
' value += Math.random() * 0.05 - 0.025;\n' +
'}\n' +
'return value;';
export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly = false): MarkersDataLayerSettings => ({
dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity,
xKey: {
name: MapType.geoMap === mapType ? 'latitude' : 'xPos',
name: functionsOnly ? 'f(x)' : (MapType.geoMap === mapType ? 'latitude' : 'xPos'),
label: MapType.geoMap === mapType ? 'latitude' : 'xPos',
type: DataKeyType.attribute
type: functionsOnly ? DataKeyType.function : DataKeyType.attribute,
funcBody: functionsOnly ? (MapType.geoMap === mapType ? defaultMarkerLatitudeFunction : defaultMarkerXPosFunction) : undefined,
settings: {},
color: materialColors[0].value
},
yKey: {
name: MapType.geoMap === mapType ? 'longitude' : 'yPos',
name: functionsOnly ? 'f(x)' : (MapType.geoMap === mapType ? 'longitude' : 'yPos'),
label: MapType.geoMap === mapType ? 'longitude' : 'yPos',
type: DataKeyType.attribute
type: functionsOnly ? DataKeyType.function : DataKeyType.attribute,
funcBody: functionsOnly ? (MapType.geoMap === mapType ? defaultMarkerLongitudeFunction : defaultMarkerYPosFunction) : undefined,
settings: {},
color: materialColors[0].value
}
});
@ -61,25 +161,40 @@ export interface PolygonsDataLayerSettings extends MapDataLayerSettings {
polygonKey: DataKey;
}
export const defaultPolygonsDataLayerSettings: PolygonsDataLayerSettings = {
dsType: DatasourceType.entity,
export const defaultPolygonsDataLayerSettings = (functionsOnly = false): PolygonsDataLayerSettings => ({
dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity,
polygonKey: {
name: 'perimeter',
name: functionsOnly ? 'f(x)' : 'perimeter',
label: 'perimeter',
type: DataKeyType.attribute
type: functionsOnly ? DataKeyType.function : DataKeyType.attribute,
settings: {},
color: materialColors[0].value
}
};
});
export interface CirclesDataLayerSettings extends MapDataLayerSettings {
circleKey: DataKey;
}
export const defaultCirclesDataLayerSettings: CirclesDataLayerSettings = {
dsType: DatasourceType.entity,
export const defaultCirclesDataLayerSettings = (functionsOnly = false): CirclesDataLayerSettings => ({
dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity,
circleKey: {
name: 'perimeter',
name: functionsOnly ? 'f(x)' : 'perimeter',
label: 'perimeter',
type: DataKeyType.attribute
type: functionsOnly ? DataKeyType.function : DataKeyType.attribute,
settings: {},
color: materialColors[0].value
}
});
export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => {
switch (dataLayerType) {
case 'markers':
return defaultMarkersDataLayerSettings(mapType, functionsOnly);
case 'polygons':
return defaultPolygonsDataLayerSettings(functionsOnly);
case 'circles':
return defaultCirclesDataLayerSettings(functionsOnly);
}
};
@ -87,6 +202,14 @@ export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings {
dataKeys: DataKey[];
}
export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[]): TbMapDatasource[] => {
return additionalMapDataSources.map(addDs => {
const res = mapDataSourceSettingsToDatasource(addDs);
res.dataKeys = addDs.dataKeys;
return res;
});
};
export enum MapControlsPosition {
topleft = 'topleft',
topright = 'topright',
@ -428,3 +551,51 @@ export function parseCenterPosition(position: string | [number, number]): [numbe
}
return [0, 0];
}
export const mergeMapDatasources = (target: TbMapDatasource[], source: TbMapDatasource[]): TbMapDatasource[] => {
const appendDatasources: TbMapDatasource[] = [];
for (const sourceDs of source) {
let merged = false;
for (let i = 0; i < target.length; i++) {
const targetDs = target[i];
if (mapDatasourceIsSame(targetDs, sourceDs)) {
target[i] = mergeMapDatasource(targetDs, sourceDs);
merged = true;
break;
}
}
if (!merged) {
appendDatasources.push(sourceDs);
}
}
target.push(...appendDatasources);
return target;
};
const mapDatasourceIsSame = (ds1: TbMapDatasource, ds2: TbMapDatasource): boolean => {
if (ds1.type === ds2.type) {
switch (ds1.type) {
case DatasourceType.function:
return true;
case DatasourceType.device:
return ds1.deviceId === ds2.deviceId;
case DatasourceType.entity:
return ds1.entityAliasId === ds2.entityAliasId;
}
}
return false;
}
const mergeMapDatasource = (target: TbMapDatasource, source: TbMapDatasource): TbMapDatasource => {
target.mapDataIds.push(...source.mapDataIds);
const appendKeys: DataKey[] = [];
for (const sourceKey of source.dataKeys) {
const found =
target.dataKeys.find(key => key.type === sourceKey.type && key.name === sourceKey.name && key.label === sourceKey.label);
if (!found) {
appendKeys.push(sourceKey);
}
}
target.dataKeys.push(...appendKeys);
return target;
}

86
ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts

@ -15,6 +15,7 @@
///
import {
additionalMapDataSourcesToDatasources,
BaseMapSettings,
DEFAULT_ZOOM_LEVEL,
defaultGeoMapSettings,
@ -23,17 +24,27 @@ import {
ImageMapSettings,
MapSetting,
MapType,
MapZoomAction,
parseCenterPosition
MapZoomAction, mergeMapDatasources,
parseCenterPosition, TbMapDatasource
} from '@home/components/widget/lib/maps/map.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { mergeDeep, mergeDeepIgnoreArray } from '@core/utils';
import { formattedDataFormDatasourceData, isDefinedAndNotNull, mergeDeepIgnoreArray } from '@core/utils';
import { DeepPartial } from '@shared/models/common';
import L from 'leaflet';
import { forkJoin, Observable, of } from 'rxjs';
import { TbMapLayer } from '@home/components/widget/lib/maps/map-layer';
import { map, switchMap, tap } from 'rxjs/operators';
import '@home/components/widget/lib/maps/leaflet/leaflet-tb';
import {
MapDataLayerType,
TbCirclesDataLayer,
TbMapDataLayer,
TbMarkersDataLayer,
TbPolygonsDataLayer
} from '@home/components/widget/lib/maps/map-data-layer';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { FormattedData, widgetType } from '@shared/models/widget.models';
import { EntityDataPageLink } from '@shared/models/query/query.models';
export abstract class TbMap<S extends BaseMapSettings> {
@ -54,6 +65,8 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected defaultCenterPosition: [number, number];
protected bounds: L.LatLngBounds;
protected dataLayers: TbMapDataLayer<any>[];
protected mapElement: HTMLElement;
private readonly mapResize$: ResizeObserver;
@ -112,6 +125,73 @@ export abstract class TbMap<S extends BaseMapSettings> {
} else {
this.bounds = new L.LatLngBounds(null, null);
}
this.setupDataLayers();
}
private setupDataLayers() {
this.dataLayers = [];
if (this.settings.markers) {
this.dataLayers.push(...this.settings.markers.map(settings => new TbMarkersDataLayer(this, settings)));
}
if (this.settings.polygons) {
this.dataLayers.push(...this.settings.polygons.map(settings => new TbPolygonsDataLayer(this, settings)));
}
if (this.settings.circles) {
this.dataLayers.push(...this.settings.circles.map(settings => new TbCirclesDataLayer(this, settings)));
}
if (this.dataLayers.length) {
const setup = this.dataLayers.map(dl => dl.setup());
forkJoin(setup).subscribe(
() => {
let datasources: TbMapDatasource[];
for (const layerType of (Object.keys(MapDataLayerType) as MapDataLayerType[])) {
const typeDatasources = this.dataLayers.filter(dl => dl.dataLayerType() === layerType).map(dl => dl.getDatasource());
if (!datasources) {
datasources = typeDatasources;
} else {
datasources = mergeMapDatasources(datasources, typeDatasources);
}
}
const additionalDatasources = additionalMapDataSourcesToDatasources(this.settings.additionalDataSources);
datasources = mergeMapDatasources(datasources, additionalDatasources);
const dataLayersSubscriptionOptions: WidgetSubscriptionOptions = {
datasources,
hasDataPageLink: true,
useDashboardTimewindow: false,
type: widgetType.latest,
callbacks: {
onDataUpdated: (subscription) => {
this.update(subscription);
}
}
};
this.ctx.subscriptionApi.createSubscription(dataLayersSubscriptionOptions, false).subscribe(
(dataLayersSubscription) => {
let pageSize = this.settings.mapPageSize;
if (isDefinedAndNotNull(this.ctx.widgetConfig.pageSize)) {
pageSize = Math.max(pageSize, this.ctx.widgetConfig.pageSize);
}
const pageLink: EntityDataPageLink = {
page: 0,
pageSize,
textSearch: null,
dynamic: true
};
dataLayersSubscription.paginatedDataSubscriptionUpdated.subscribe(() => {
// this.map.resetState();
});
dataLayersSubscription.subscribeAllForPaginatedData(pageLink, null);
}
);
}
);
}
}
private update(subscription: IWidgetSubscription) {
const dsData = formattedDataFormDatasourceData<TbMapDatasource>(subscription.data,
undefined, undefined, el => el.datasource.entityId + el.datasource.mapDataIds[0]);
this.dataLayers.forEach(dl => dl.updateData(dsData));
}
private resize() {

53
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html

@ -15,9 +15,14 @@
limitations under the License.
-->
<mat-form-field [formGroup]="selectEntityAliasFormGroup" class="mat-block">
<mat-label *ngIf="showLabel">{{ 'entity.entity-alias' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ !showLabel ? ('entity.entity-alias' | translate) : ''}}"
<mat-form-field [formGroup]="selectEntityAliasFormGroup"
[class.tb-inline-field]="inlineField"
[class.flex]="inlineField"
class="mat-block"
[appearance]="inlineField ? 'outline' : appearance"
[subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing">
<mat-label *ngIf="showLabel && !inlineField">{{ 'entity.entity-alias' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ (!showLabel || inlineField) ? ('entity.entity-alias' | translate) : ''}}"
#entityAliasInput
formControlName="entityAlias"
(focusin)="onFocus()"
@ -25,21 +30,31 @@
(keydown)="entityAliasEnter($event)"
(keypress)="entityAliasEnter($event)"
[matAutocomplete]="entityAliasAutocomplete">
<button *ngIf="selectEntityAliasFormGroup.get('entityAlias').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<button *ngIf="selectEntityAliasFormGroup.get('entityAlias').value?.id && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Edit"
matTooltip="{{ 'device-profile.edit' | translate }}"
matTooltipPosition="above"
(click)="editEntityAlias($event)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<button *ngIf="!selectEntityAliasFormGroup.get('entityAlias').value && !disabled"
<div matSuffix class="mat-mdc-form-field-icon-suffix flex flex-row">
<button *ngIf="selectEntityAliasFormGroup.get('entityAlias').value"
type="button"
mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<button *ngIf="selectEntityAliasFormGroup.get('entityAlias').value?.id"
type="button"
mat-icon-button aria-label="Edit"
matTooltip="{{ 'entity.edit-alias' | translate }}"
matTooltipPosition="above"
(click)="editEntityAlias($event)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<mat-icon matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'entity.alias-required' | translate"
*ngIf="inlineField && !modelValue && tbRequired
&& selectEntityAliasFormGroup.get('entityAlias').touched"
class="tb-error">
warning
</mat-icon>
</div>
<button *ngIf="!inlineField && !selectEntityAliasFormGroup.get('entityAlias').value && !disabled"
style="margin-right: 8px;"
type="button"
matSuffix mat-button color="primary"
@ -69,7 +84,7 @@
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="!modelValue && tbRequired">
<mat-error *ngIf="!inlineField && !modelValue && tbRequired">
{{ 'entity.alias-required' | translate }}
</mat-error>
</mat-form-field>

0
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.models.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.models.ts

0
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.scss

11
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts

@ -37,6 +37,7 @@ import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autoc
import { EntityAliasSelectCallbacks } from './entity-alias-select.component.models';
import { ENTER } from '@angular/cdk/keycodes';
import { ErrorStateMatcher } from '@angular/material/core';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({
selector: 'tb-entity-alias-select',
@ -80,6 +81,16 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
inlineField: boolean;
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@ViewChild('entityAliasInput', {static: true}) entityAliasInput: ElementRef;
entityAliasList: Array<EntityAlias> = [];

129
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html

@ -0,0 +1,129 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="dataLayerFormGroup" class="tb-form-table-row tb-map-data-layer-row">
<div class="tb-source-field">
<mat-form-field class="tb-ds-type-field tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="dsType">
<mat-option *ngFor="let type of datasourceTypes" [value]="type">
{{ datasourceTypesTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-entity-autocomplete
class="tb-device-field"
*ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.device"
required
inlineField
placeholder="{{ 'device.select-device' | translate }}"
[entityType]="EntityType.DEVICE"
formControlName="dsDeviceId">
</tb-entity-autocomplete>
<tb-entity-alias-select
class="tb-entity-alias-field"
*ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.entity"
inlineField
tbRequired
[aliasController]="aliasController"
formControlName="dsEntityAliasId"
[callbacks]="entityAliasSelectCallbacks">
</tb-entity-alias-select>
</div>
<tb-data-key-input
class="tb-x-pos-field"
*ngIf="dataLayerType === 'markers'"
required
requiredText="{{ mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.latitude-key-required' : 'widgets.maps.data-layer.marker.x-pos-key-required' }}"
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="aliasController"
[dataKeyType]="functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="dataKeyCallbacks"
[generateKey]="generateDataKey"
(keyEdit)="editKey('xKey')"
formControlName="xKey">
</tb-data-key-input>
<tb-data-key-input
class="tb-y-pos-field"
*ngIf="dataLayerType === 'markers'"
required
requiredText="{{ mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.longitude-key-required' : 'widgets.maps.data-layer.marker.y-pos-key-required' }}"
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="aliasController"
[dataKeyType]="functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="dataKeyCallbacks"
[generateKey]="generateDataKey"
(keyEdit)="editKey('yKey')"
formControlName="yKey">
</tb-data-key-input>
<tb-data-key-input
class="tb-key-field"
*ngIf="dataLayerType === 'polygons'"
required
requiredText="widgets.maps.data-layer.polygon.polygon-key-required"
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="aliasController"
[dataKeyType]="functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="dataKeyCallbacks"
[generateKey]="generateDataKey"
(keyEdit)="editKey('polygonKey')"
formControlName="polygonKey">
</tb-data-key-input>
<tb-data-key-input
class="tb-key-field"
*ngIf="dataLayerType === 'circles'"
required
requiredText="widgets.maps.data-layer.circle.circle-key-required"
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="aliasController"
[dataKeyType]="functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="dataKeyCallbacks"
[generateKey]="generateDataKey"
(keyEdit)="editKey('circleKey')"
formControlName="circleKey">
</tb-data-key-input>
<div class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button
#matButton
(click)="editDataLayer($event, matButton)"
matTooltip="{{ editDataLayerText | translate }}"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
<div class="tb-remove-button">
<button type="button"
mat-icon-button
(click)="dataLayerRemoved.emit()"
matTooltip="{{ removeDataLayerText | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</div>

45
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.scss

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../../scss/constants';
.tb-form-table-row.tb-map-data-layer-row {
.tb-source-field {
flex: 1 1 50%;
display: flex;
gap: 12px;
.tb-ds-type-field, .tb-device-field, .tb-entity-alias-field {
flex: 1;
}
}
.tb-x-pos-field {
flex: 1 1 25%;
}
.tb-y-pos-field {
flex: 1 1 25%;
}
.tb-key-field {
flex: 1 1 50%;
}
.tb-remove-button {
width: 40px;
min-width: 40px;
}
}

380
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts

@ -0,0 +1,380 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
DestroyRef,
EventEmitter,
forwardRef,
Input,
OnInit,
Output,
Renderer2,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { TranslateService } from '@ngx-translate/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
CirclesDataLayerSettings,
MapDataLayerSettings,
MapDataLayerType,
MapType,
MarkersDataLayerSettings,
PolygonsDataLayerSettings
} from '@home/components/widget/lib/maps/map.models';
import {
DataKey,
DataKeyConfigMode,
DatasourceType,
datasourceTypeTranslationMap,
widgetType
} from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
} from '@home/components/widget/config/data-key-config-dialog.component';
import { deepClone } from '@core/utils';
import { MatDialog } from '@angular/material/dialog';
import {
EntityAliasSelectCallbacks
} from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component.models';
@Component({
selector: 'tb-map-data-layer-row',
templateUrl: './map-data-layer-row.component.html',
styleUrls: ['./map-data-layer-row.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MapDataLayerRowComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
DatasourceType = DatasourceType;
DataKeyType = DataKeyType;
EntityType = EntityType;
MapType = MapType;
datasourceTypes: Array<DatasourceType> = [];
datasourceTypesTranslations = datasourceTypeTranslationMap;
@Input()
disabled: boolean;
@Input()
mapType: MapType = MapType.geoMap;
@Input()
dataLayerType: MapDataLayerType = 'markers';
get functionsOnly(): boolean {
return this.mapSettingsComponent.functionsOnly;
}
get aliasController(): IAliasController {
return this.mapSettingsComponent.aliasController;
}
get dataKeyCallbacks(): DataKeysCallbacks {
return this.mapSettingsComponent.callbacks;
}
public get entityAliasSelectCallbacks(): EntityAliasSelectCallbacks {
return this.mapSettingsComponent.callbacks;
}
@Output()
dataLayerRemoved = new EventEmitter();
generateDataKey = this._generateDataKey.bind(this);
dataLayerFormGroup: UntypedFormGroup;
modelValue: MapDataLayerSettings;
editDataLayerText: string;
removeDataLayerText: string;
private propagateChange = (_val: any) => {};
constructor(private mapSettingsComponent: MapSettingsComponent,
private fb: UntypedFormBuilder,
private dialog: MatDialog,
private translate: TranslateService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private destroyRef: DestroyRef) {
}
ngOnInit() {
if (this.functionsOnly) {
this.datasourceTypes = [DatasourceType.function];
} else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity];
}
this.dataLayerFormGroup = this.fb.group({
dsType: [null, [Validators.required]],
dsDeviceId: [null, [Validators.required]],
dsEntityAliasId: [null, [Validators.required]]
});
switch (this.dataLayerType) {
case 'markers':
this.editDataLayerText = 'widgets.maps.data-layer.marker.marker-configuration';
this.removeDataLayerText = 'widgets.maps.data-layer.marker.remove-marker';
this.dataLayerFormGroup.addControl('xKey', this.fb.control(null, Validators.required));
this.dataLayerFormGroup.addControl('yKey', this.fb.control(null, Validators.required));
break;
case 'polygons':
this.editDataLayerText = 'widgets.maps.data-layer.polygon.polygon-configuration';
this.removeDataLayerText = 'widgets.maps.data-layer.polygon.remove-polygon';
this.dataLayerFormGroup.addControl('polygonKey', this.fb.control(null, Validators.required));
break;
case 'circles':
this.editDataLayerText = 'widgets.maps.data-layer.circle.circle-configuration';
this.removeDataLayerText = 'widgets.maps.data-layer.circle.remove-circle';
this.dataLayerFormGroup.addControl('circleKey', this.fb.control(null, Validators.required));
break;
}
this.dataLayerFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => this.updateModel()
);
this.dataLayerFormGroup.get('dsType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
(newDsType: DatasourceType) => this.onDsTypeChanged(newDsType)
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.dataLayerFormGroup.disable({emitEvent: false});
} else {
this.dataLayerFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: MapDataLayerSettings): void {
this.modelValue = value;
this.dataLayerFormGroup.patchValue(
{
dsType: value?.dsType,
dsDeviceId: value?.dsDeviceId,
dsEntityAliasId: value?.dsEntityAliasId
}, {emitEvent: false}
);
switch (this.dataLayerType) {
case 'markers':
const markersDataLayer = value as MarkersDataLayerSettings;
this.dataLayerFormGroup.patchValue(
{
xKey: markersDataLayer?.xKey,
yKey: markersDataLayer?.yKey
}, {emitEvent: false}
);
break;
case 'polygons':
const polygonsDataLayer = value as PolygonsDataLayerSettings;
this.dataLayerFormGroup.patchValue(
{
polygonKey: polygonsDataLayer?.polygonKey
}, {emitEvent: false}
);
break;
case 'circles':
const circlesDataLayer = value as CirclesDataLayerSettings;
this.dataLayerFormGroup.patchValue(
{
circleKey: circlesDataLayer?.circleKey
}, {emitEvent: false}
);
break;
}
this.updateValidators();
this.cd.markForCheck();
}
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') {
const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value;
this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataKey: deepClone(targetDataKey),
dataKeyConfigMode: DataKeyConfigMode.general,
aliasController: this.aliasController,
widgetType: widgetType.latest,
deviceId: this.dataLayerFormGroup.get('dsDeviceId').value,
entityAliasId: this.dataLayerFormGroup.get('dsEntityAliasId').value,
showPostProcessing: true,
callbacks: this.mapSettingsComponent.callbacks,
hideDataKeyColor: true,
hideDataKeyDecimals: true,
hideDataKeyUnits: true,
widget: this.mapSettingsComponent.widget,
dashboard: null,
dataKeySettingsForm: null,
dataKeySettingsDirective: null
}
}).afterClosed().subscribe((updatedDataKey) => {
if (updatedDataKey) {
this.dataLayerFormGroup.get(keyType).patchValue(updatedDataKey);
}
});
}
editDataLayer($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
/*const ctx: any = {
mapLayerSettings: deepClone(this.modelValue)
};
const mapLayerSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, MapLayerSettingsPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
mapLayerSettingsPanelPopover.tbComponentRef.instance.popover = mapLayerSettingsPanelPopover;
mapLayerSettingsPanelPopover.tbComponentRef.instance.mapLayerSettingsApplied.subscribe((layer) => {
mapLayerSettingsPanelPopover.hide();
this.layerFormGroup.patchValue(
layer,
{emitEvent: false});
this.updateValidators();
this.updateModel();
});*/
}
}
private _generateDataKey(key: DataKey): DataKey {
key = this.dataKeyCallbacks.generateDataKey(key.name, key.type, null, false,
null);
return key;
}
private onDsTypeChanged(newDsType: DatasourceType) {
let updateModel = false;
switch (this.dataLayerType) {
case 'markers':
const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value;
if (this.updateDataKeyToNewDsType(xKey, newDsType)) {
this.dataLayerFormGroup.get('xKey').patchValue(xKey, {emitEvent: false});
updateModel = true;
}
const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value;
if (this.updateDataKeyToNewDsType(yKey, newDsType)) {
this.dataLayerFormGroup.get('yKey').patchValue(yKey, {emitEvent: false});
updateModel = true;
}
break;
case 'polygons':
const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value;
if (this.updateDataKeyToNewDsType(polygonKey, newDsType)) {
this.dataLayerFormGroup.get('polygonKey').patchValue(polygonKey, {emitEvent: false});
updateModel = true;
}
break;
case 'circles':
const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value;
if (this.updateDataKeyToNewDsType(circleKey, newDsType)) {
this.dataLayerFormGroup.get('circleKey').patchValue(circleKey, {emitEvent: false});
updateModel = true;
}
break;
}
this.updateValidators();
if (updateModel) {
this.updateModel();
}
}
private updateDataKeyToNewDsType(dataKey: DataKey, newDsType: DatasourceType): boolean {
if (newDsType === DatasourceType.function) {
if (dataKey.type !== DataKeyType.function) {
dataKey.type = DataKeyType.function;
return true;
}
} else {
if (dataKey.type === DataKeyType.function) {
dataKey.type = DataKeyType.attribute;
return true;
}
}
return false;
}
private updateValidators() {
const dsType: DatasourceType = this.dataLayerFormGroup.get('dsType').value;
if (dsType === DatasourceType.function) {
this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else if (dsType === DatasourceType.device) {
this.dataLayerFormGroup.get('dsDeviceId').enable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else {
this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').enable({emitEvent: false});
}
}
private updateModel() {
this.modelValue = {...this.modelValue, ...this.dataLayerFormGroup.value};
this.propagateChange(this.modelValue);
}
protected readonly datasourceType = DatasourceType;
}

51
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html

@ -0,0 +1,51 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-panel no-padding no-border">
<div class="tb-form-table tb-map-data-layers">
<div class="tb-form-table-header no-padding-right">
<div class="tb-form-table-header-cell tb-source-header" translate>widgets.maps.data-layer.source</div>
<div *ngIf="dataLayerType === 'markers'" class="tb-form-table-header-cell tb-x-pos-header">
{{ (mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.latitude-key' : 'widgets.maps.data-layer.marker.x-pos-key') | translate }}
</div>
<div *ngIf="dataLayerType === 'markers'" class="tb-form-table-header-cell tb-y-pos-header">
{{ (mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.longitude-key' : 'widgets.maps.data-layer.marker.y-pos-key') | translate }}
</div>
<div *ngIf="dataLayerType === 'polygons'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.polygon.polygon-key</div>
<div *ngIf="dataLayerType === 'circles'" class="tb-form-table-header-cell tb-key-header" translate>widgets.maps.data-layer.circle.circle-key</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
<div *ngIf="dataLayersFormArray().controls.length; else noDataLayers" class="tb-form-table-body">
<div *ngFor="let dataLayerControl of dataLayersFormArray().controls; trackBy: trackByDataLayer; let $index = index;">
<tb-map-data-layer-row class="flex-1"
[mapType]="mapType"
[dataLayerType]="dataLayerType"
[formControl]="dataLayerControl"
(dataLayerRemoved)="removeDataLayer($index)">
</tb-map-data-layer-row>
</div>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addDataLayer()">
{{ addDataLayerText | translate }}
</button>
</div>
</div>
<ng-template #noDataLayers>
<span class="tb-prompt flex items-center justify-center">{{ noDataLayersText | translate }}</span>
</ng-template>

42
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.scss

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-map-data-layers {
.tb-form-table-header-cell {
&.tb-source-header {
flex: 1 1 50%;
}
&.tb-x-pos-header {
flex: 1 1 25%;
}
&.tb-y-pos-header {
flex: 1 1 25%;
}
&.tb-key-header {
flex: 1 1 50%;
}
&.tb-actions-header {
width: 80px;
min-width: 80px;
}
}
.tb-form-table-body {
tb-map-data-layer-row {
overflow: hidden;
}
}
}

177
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts

@ -0,0 +1,177 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, DestroyRef, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator
} from '@angular/forms';
import { mergeDeep } from '@core/utils';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
defaultMapDataLayerSettings,
MapDataLayerSettings,
MapDataLayerType,
mapDataLayerValid,
mapDataLayerValidator,
MapType
} from '@home/components/widget/lib/maps/map.models';
import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component';
@Component({
selector: 'tb-map-data-layers',
templateUrl: './map-data-layers.component.html',
styleUrls: ['./map-data-layers.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MapDataLayersComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MapDataLayersComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Validator {
MapType = MapType;
@Input()
disabled: boolean;
@Input()
mapType: MapType = MapType.geoMap;
@Input()
dataLayerType: MapDataLayerType = 'markers';
get functionsOnly(): boolean {
return this.mapSettingsComponent.functionsOnly;
}
dataLayersFormGroup: UntypedFormGroup;
addDataLayerText: string;
noDataLayersText: string;
private propagateChange = (_val: any) => {};
constructor(private mapSettingsComponent: MapSettingsComponent,
private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit() {
switch (this.dataLayerType) {
case 'markers':
this.addDataLayerText = 'widgets.maps.data-layer.marker.add-marker';
this.noDataLayersText = 'widgets.maps.data-layer.marker.no-markers';
break;
case 'polygons':
this.addDataLayerText = 'widgets.maps.data-layer.polygon.add-polygon';
this.noDataLayersText = 'widgets.maps.data-layer.polygon.no-polygons';
break;
case 'circles':
this.addDataLayerText = 'widgets.maps.data-layer.circle.add-circle';
this.noDataLayersText = 'widgets.maps.data-layer.circle.no-circles';
break;
}
this.dataLayersFormGroup = this.fb.group({
dataLayers: [this.fb.array([]), []]
});
this.dataLayersFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => {
let layers: MapDataLayerSettings[] = this.dataLayersFormGroup.get('dataLayers').value;
if (layers) {
layers = layers.filter(layer => mapDataLayerValid(layer, this.dataLayerType));
}
this.propagateChange(layers);
}
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.dataLayersFormGroup.disable({emitEvent: false});
} else {
this.dataLayersFormGroup.enable({emitEvent: false});
}
}
writeValue(value: MapDataLayerSettings[] | undefined): void {
const dataLayers: MapDataLayerSettings[] = value || [];
this.dataLayersFormGroup.setControl('dataLayers', this.prepareDataLayersFormArray(dataLayers), {emitEvent: false});
}
public validate(c: UntypedFormControl) {
const valid = this.dataLayersFormGroup.valid;
return valid ? null : {
dataLayers: {
valid: false,
},
};
}
dataLayersFormArray(): UntypedFormArray {
return this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray;
}
trackByDataLayer(index: number, dataLayerControl: AbstractControl): any {
return dataLayerControl;
}
removeDataLayer(index: number) {
(this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray).removeAt(index);
}
addDataLayer() {
const dataLayer = mergeDeep<MapDataLayerSettings>({} as MapDataLayerSettings,
defaultMapDataLayerSettings(this.mapType, this.dataLayerType, this.functionsOnly));
const dataLayersArray = this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray;
const dataLayerControl = this.fb.control(dataLayer, [mapDataLayerValidator(this.dataLayerType)]);
dataLayersArray.push(dataLayerControl);
}
private prepareDataLayersFormArray(dataLayers: MapDataLayerSettings[]): UntypedFormArray {
const dataLayersControls: Array<AbstractControl> = [];
dataLayers.forEach((dataLayer) => {
dataLayersControls.push(this.fb.control(dataLayer, [mapDataLayerValidator(this.dataLayerType)]));
});
return this.fb.array(dataLayersControls);
}
}

14
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html

@ -34,12 +34,24 @@
<div class="tb-form-panel-title">
{{ 'widgets.maps.overlays.overlays' | translate }}
</div>
<tb-toggle-select [(ngModel)]="overlaysMode"
<tb-toggle-select [(ngModel)]="dataLayerMode"
[ngModelOptions]="{ standalone: true }">
<tb-toggle-option value="markers">{{ 'widgets.maps.overlays.markers' | translate }}</tb-toggle-option>
<tb-toggle-option value="polygons">{{ 'widgets.maps.overlays.polygons' | translate }}</tb-toggle-option>
<tb-toggle-option value="circles">{{ 'widgets.maps.overlays.circles' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-map-data-layers [class.!hidden]="dataLayerMode !== 'markers'"
formControlName="markers"
dataLayerType="markers"
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
<tb-map-data-layers [class.!hidden]="dataLayerMode !== 'polygons'"
formControlName="polygons"
dataLayerType="polygons"
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
<tb-map-data-layers [class.!hidden]="dataLayerMode !== 'circles'"
formControlName="circles"
dataLayerType="circles"
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
</div>
</ng-container>

22
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts

@ -22,9 +22,14 @@ import {
UntypedFormBuilder, UntypedFormControl,
UntypedFormGroup, Validator
} from '@angular/forms';
import { ImageSourceType, MapSetting, MapType } from '@home/components/widget/lib/maps/map.models';
import { ImageSourceType, MapDataLayerType, MapSetting, MapType } from '@home/components/widget/lib/maps/map.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { merge } from 'rxjs';
import { coerceBoolean } from '@shared/decorators/coercion';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { Widget } from '@shared/models/widget.models';
@Component({
selector: 'tb-map-settings',
@ -50,13 +55,26 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
functionsOnly = false;
@Input()
aliasController: IAliasController;
@Input()
callbacks: WidgetConfigCallbacks;
@Input()
widget: Widget;
private modelValue: MapSetting;
private propagateChange = null;
public mapSettingsFormGroup: UntypedFormGroup;
overlaysMode: 'markers' | 'polygons' | 'circles' = 'markers';
dataLayerMode: MapDataLayerType = 'markers';
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {

14
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -189,6 +189,12 @@ import { MapLayerRowComponent } from '@home/components/widget/lib/settings/commo
import {
MapLayerSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/map/map-layer-settings-panel.component';
import { MapDataLayersComponent } from '@home/components/widget/lib/settings/common/map/map-data-layers.component';
import { MapDataLayerRowComponent } from '@home/components/widget/lib/settings/common/map/map-data-layer-row.component';
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
import {
EntityAliasSelectComponent
} from '@home/components/widget/lib/settings/common/alias/entity-alias-select.component';
@NgModule({
declarations: [
@ -262,7 +268,10 @@ import {
MapLayerSettingsPanelComponent,
MapLayerRowComponent,
MapLayersComponent,
MapSettingsComponent
MapDataLayerRowComponent,
MapDataLayersComponent,
MapSettingsComponent,
EntityAliasSelectComponent
],
imports: [
CommonModule,
@ -337,7 +346,8 @@ import {
DynamicFormSelectItemRowComponent,
DynamicFormComponent,
DynamicFormArrayComponent,
MapSettingsComponent
MapSettingsComponent,
EntityAliasSelectComponent
],
providers: [
ColorSettingsComponentService,

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/map/map-widget-settings.component.html

@ -16,7 +16,13 @@
-->
<ng-container [formGroup]="mapWidgetSettingsForm">
<tb-map-settings formControlName="mapSettings"></tb-map-settings>
<tb-map-settings
[aliasController]="aliasController"
[callbacks]="callbacks"
[functionsOnly]="functionsOnly"
[widget]="widget"
formControlName="mapSettings">
</tb-map-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">

1
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -303,6 +303,7 @@
<tb-widget-settings
[aliasController]="aliasController"
[callbacks]="widgetConfigCallbacks"
[functionsOnly]="functionsOnly"
[dashboard]="dashboard"
[widget]="widget"
[widgetConfig]="modelValue"

22
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html

@ -15,11 +15,18 @@
limitations under the License.
-->
<mat-form-field [formGroup]="selectEntityFormGroup" class="mat-block" [appearance]="appearance" [subscriptSizing]="subscriptSizing"
<mat-form-field [formGroup]="selectEntityFormGroup"
[class.tb-inline-field]="inlineField"
[class.flex]="inlineField"
[class.tb-suffix-absolute]="inlineField && !selectEntityFormGroup.get('entity').value"
class="mat-block"
[appearance]="inlineField ? 'outline' : appearance"
[subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"
[class]="additionalClasses">
<mat-label>{{ label | translate }}</mat-label>
<mat-label *ngIf="!inlineField">{{ label | translate }}</mat-label>
<input matInput type="text"
#entityInput
placeholder="{{ placeholder }}"
formControlName="entity"
(focusin)="onFocus()"
[required]="required"
@ -40,6 +47,15 @@
(click)="createNewEntity($event)">
<span style="white-space: nowrap">{{ 'entity.create-new' | translate }}</span>
</button>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="requiredErrorText | translate"
*ngIf="inlineField && selectEntityFormGroup.get('entity').hasError('required')
&& selectEntityFormGroup.get('entity').touched"
class="tb-error">
warning
</mat-icon>
<mat-autocomplete class="tb-autocomplete"
#entityAutocomplete="matAutocomplete"
[displayWith]="displayEntityFn">
@ -59,7 +75,7 @@
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="selectEntityFormGroup.get('entity').hasError('required')">
<mat-error *ngIf="!inlineField && selectEntityFormGroup.get('entity').hasError('required')">
{{ requiredErrorText | translate }}
</mat-error>
</mat-form-field>

7
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -116,6 +116,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
@Input()
requiredText: string;
@Input()
placeholder: string;
@Input()
@coerceBoolean()
useFullEntityId: boolean;
@ -123,6 +126,10 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
@coerceBoolean()
inlineField: boolean;
@Input()
@coerceBoolean()
required: boolean;

7
ui-ngx/src/app/shared/models/widget.models.ts

@ -510,8 +510,8 @@ export const datasourcesHasOnlyComparisonAggregation = (datasources?: Array<Data
return true;
};
export interface FormattedData {
$datasource: Datasource;
export interface FormattedData<D extends Datasource = Datasource> {
$datasource: D;
entityName: string;
deviceName: string;
entityId: string;
@ -861,6 +861,7 @@ export interface IWidgetSettingsComponent {
aliasController: IAliasController;
callbacks: WidgetConfigCallbacks;
dataKeyCallbacks: DataKeysCallbacks;
functionsOnly: boolean;
dashboard: Dashboard;
widget: Widget;
widgetConfig: WidgetConfigComponentData;
@ -882,6 +883,8 @@ export abstract class WidgetSettingsComponent extends PageComponent implements
dataKeyCallbacks: DataKeysCallbacks;
functionsOnly: boolean;
dashboard: Dashboard;
widget: Widget;

34
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -2332,6 +2332,7 @@
"alias-required": "Entity alias is required.",
"remove-alias": "Remove entity alias",
"add-alias": "Add entity alias",
"edit-alias": "Edit entity alias",
"entity-list": "Entity list",
"entity-type": "Entity type",
"entity-types": "Entity types",
@ -6866,6 +6867,39 @@
"polygons": "Polygons",
"circles": "Circles"
},
"data-layer": {
"source": "Source",
"marker": {
"latitude-key": "Latitude key",
"longitude-key": "Longitude key",
"x-pos-key": "X position key",
"y-pos-key": "Y position key",
"latitude-key-required": "Latitude key required",
"longitude-key-required": "Longitude key required",
"x-pos-key-required": "X position key required",
"y-pos-key-required": "Y position key required",
"no-markers": "No markers configured",
"add-marker": "Add marker",
"marker-configuration": "Marker configuration",
"remove-marker": "Remove marker"
},
"polygon": {
"polygon-key": "Polygon key",
"polygon-key-required": "Polygon key required",
"no-polygons": "No polygons configured",
"add-polygon": "Add polygon",
"polygon-configuration": "Polygon configuration",
"remove-polygon": "Remove polygon"
},
"circle": {
"circle-key": "Circle key",
"circle-key-required": "Circle key required",
"no-circles": "No circles configured",
"add-circle": "Add circle",
"circle-configuration": "Circle configuration",
"remove-circle": "Remove circle"
}
},
"select-entity": "Select entity",
"select-entity-hint": "Hint: after selection click at the map to set position",
"tooltips": {

Loading…
Cancel
Save