Browse Source

UI: New map widgets - introduce additional datasources for map data layer.

pull/13080/head
Igor Kulikov 2 years ago
parent
commit
606c971973
  1. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/circles-data-layer.ts
  2. 45
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts
  3. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts
  4. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/polygons-data-layer.ts
  5. 23
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/trips-data-layer.ts
  6. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts
  7. 10
      ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts
  8. 37
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html
  9. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts
  10. 80
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.html
  11. 56
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.scss
  12. 207
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts
  13. 43
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.html
  14. 53
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.scss
  15. 147
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.ts
  16. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html
  17. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html
  18. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts
  19. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts
  20. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.html
  21. 24
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.scss
  22. 74
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-source-row.component.ts
  23. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.html
  24. 23
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.scss
  25. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-sources.component.ts
  26. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html
  27. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  28. 94
      ui-ngx/src/app/shared/models/widget/maps/map-export.models.ts
  29. 37
      ui-ngx/src/app/shared/models/widget/maps/map.models.ts
  30. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -22,7 +22,7 @@ import {
TbMapDatasource
} from '@shared/models/widget/maps/map.models';
import L from 'leaflet';
import { FormattedData } from '@shared/models/widget.models';
import { DataKey, FormattedData } from '@shared/models/widget.models';
import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { Observable } from 'rxjs';
@ -213,9 +213,8 @@ export class TbCirclesDataLayer extends TbShapesDataLayer<CirclesDataLayerSettin
);
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys.push(this.settings.circleKey);
return datasource;
protected getDataKeys(): DataKey[] {
return [this.settings.circleKey];
}
protected defaultBaseSettings(map: TbMap<any>): Partial<CirclesDataLayerSettings> {

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

@ -41,7 +41,7 @@ import L from 'leaflet';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { forkJoin, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { DataKey, FormattedData } from '@shared/models/widget.models';
import { DataKey, DatasourceType, FormattedData } from '@shared/models/widget.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { WidgetContext } from '@home/models/widget-component.models';
@ -170,7 +170,7 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay
protected settings: S;
protected datasource: TbMapDatasource;
protected dataSources: TbMapDatasource[];
protected mapDataId: string;
@ -202,13 +202,19 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay
}
public setup(): Observable<any> {
this.datasource = mapDataSourceSettingsToDatasource(this.settings);
this.datasource.dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : [];
const colorRangeKeys = this.allColorSettings().filter(settings => settings.type === DataLayerColorType.range && settings.rangeKey)
.map(settings => settings.rangeKey);
this.datasource.dataKeys.push(...colorRangeKeys);
this.mapDataId = this.datasource.mapDataIds[0];
this.datasource = this.setupDatasource(this.datasource);
this.dataSources = [];
const datasource = mapDataSourceSettingsToDatasource(this.settings);
this.mapDataId = datasource.mapDataIds[0];
this.dataSources.push(datasource);
if (this.settings.additionalDataSources?.length && datasource.type !== DatasourceType.function) {
this.dataSources.push(...this.settings.additionalDataSources.map(ds => mapDataSourceSettingsToDatasource(ds, this.mapDataId)));
}
const dataKeys = this.calculateDataKeys();
const latestDataKeys = this.calculateLatestDataKeys();
this.dataSources.forEach(ds => ds.dataKeys.push(...dataKeys));
if (latestDataKeys?.length) {
this.dataSources.forEach(ds => ds.latestDataKeys.push(...latestDataKeys));
}
return forkJoin(
[
this.dataLayerLabelProcessor ? this.dataLayerLabelProcessor.setup() : of(null),
@ -241,8 +247,8 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay
return this.map.type();
}
public getDatasource(): TbMapDatasource {
return this.datasource;
public getDataSources(): TbMapDatasource[] {
return this.dataSources;
}
public getDataLayerContainer(): L.FeatureGroup {
@ -289,8 +295,21 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay
return L.featureGroup([], {snapIgnore: true});
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
return datasource;
protected calculateDataKeys(): DataKey[] {
const dataKeys = this.settings.additionalDataKeys ? [...this.settings.additionalDataKeys] : [];
const colorRangeKeys = this.allColorSettings().filter(settings => settings.type === DataLayerColorType.range && settings.rangeKey)
.map(settings => settings.rangeKey);
dataKeys.push(...colorRangeKeys);
dataKeys.push(...this.getDataKeys());
return dataKeys;
}
protected calculateLatestDataKeys(): DataKey[] {
return [];
}
protected getDataKeys(): DataKey[] {
return [];
}
protected allColorSettings(): DataLayerColorSettings[] {

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

@ -37,7 +37,7 @@ import {
TbMapDatasource
} from '@shared/models/widget/maps/map.models';
import L, { FeatureGroup } from 'leaflet';
import { FormattedData } from '@shared/models/widget.models';
import { DataKey, FormattedData } from '@shared/models/widget.models';
import { forkJoin, Observable, of } from 'rxjs';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import {
@ -628,9 +628,8 @@ export class TbMarkersDataLayer extends TbLatestMapDataLayer<MarkersDataLayerSet
}
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys.push(this.settings.xKey, this.settings.yKey);
return datasource;
protected getDataKeys(): DataKey[] {
return [this.settings.xKey, this.settings.yKey];
}
protected allColorSettings(): DataLayerColorSettings[] {

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

@ -21,7 +21,7 @@ import {
TbMapDatasource, TbPolyData, TbPolygonCoordinates, TbPolygonRawCoordinates
} from '@shared/models/widget/maps/map.models';
import L from 'leaflet';
import { FormattedData } from '@shared/models/widget.models';
import { DataKey, FormattedData } from '@shared/models/widget.models';
import { TbShapesDataLayer } from '@home/components/widget/lib/maps/data-layer/shapes-data-layer';
import { TbMap } from '@home/components/widget/lib/maps/map';
import { Observable } from 'rxjs';
@ -410,9 +410,8 @@ export class TbPolygonsDataLayer extends TbShapesDataLayer<PolygonsDataLayerSett
);
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys.push(this.settings.polygonKey);
return datasource;
protected getDataKeys(): DataKey[] {
return [this.settings.polygonKey];
}
protected defaultBaseSettings(map: TbMap<any>): Partial<PolygonsDataLayerSettings> {

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

@ -535,22 +535,33 @@ export class TbTripsDataLayer extends TbMapDataLayer<TripsDataLayerSettings, TbT
};
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
datasource.dataKeys = [this.settings.xKey, this.settings.yKey];
protected calculateDataKeys(): DataKey[] {
const dataKeys = [this.settings.xKey, this.settings.yKey];
const additionalKeys = this.allColorSettings().filter(settings => settings.type === DataLayerColorType.range && settings.rangeKey)
.map(settings => settings.rangeKey);
.map(settings => settings.rangeKey);
if (this.settings.additionalDataKeys?.length) {
additionalKeys.push(...this.settings.additionalDataKeys);
}
if (additionalKeys.length) {
const tsKeys = additionalKeys.filter(key => key.type === DataKeyType.timeseries);
dataKeys.push(...tsKeys);
}
return dataKeys;
}
protected calculateLatestDataKeys(): DataKey[] {
const additionalKeys = this.allColorSettings().filter(settings => settings.type === DataLayerColorType.range && settings.rangeKey)
.map(settings => settings.rangeKey);
if (this.settings.additionalDataKeys?.length) {
additionalKeys.push(...this.settings.additionalDataKeys);
}
if (additionalKeys.length) {
const latestKeys = additionalKeys.filter(key => key.type !== DataKeyType.timeseries);
datasource.dataKeys.push(...tsKeys);
if (latestKeys.length) {
datasource.latestDataKeys = latestKeys;
return latestKeys;
}
}
return datasource;
return [];
}
protected allColorSettings(): DataLayerColorSettings[] {

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

@ -308,7 +308,8 @@ export abstract class TbMap<S extends BaseMapSettings> {
() => {
let datasources: TbMapDatasource[];
for (const layerType of mapDataLayerTypes) {
const typeDatasources = this.latestDataLayers.filter(dl => dl.dataLayerType() === layerType).map(dl => dl.getDatasource());
const typeDatasources = this.latestDataLayers.filter(dl => dl.dataLayerType() === layerType)
.map(dl => dl.getDataSources()).flat();
if (!datasources) {
datasources = typeDatasources;
} else {
@ -349,7 +350,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
);
}
if (this.tripDataLayers.length) {
const tripDatasources = this.tripDataLayers.map(dl => dl.getDatasource());
const tripDatasources = this.tripDataLayers.map(dl => dl.getDataSources()).flat();
const tripDataLayersSubscriptionOptions: WidgetSubscriptionOptions = {
datasources: tripDatasources,
hasDataPageLink: true,
@ -926,7 +927,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
private calculateCurrentTime(minTime: number, maxTime: number): number {
if (minTime !== this.minTime || maxTime !== this.maxTime) {
if (this.minTime >= this.currentTime || isUndefined(this.currentTime)) {
if (this.minTime >= this.currentTime || isUndefined(this.currentTime) || this.currentTime === Infinity) {
return this.minTime;
} else if (this.maxTime <= this.currentTime) {
return this.maxTime;

10
ui-ngx/src/app/modules/home/components/widget/lib/maps/panels/map-timeline-panel.component.ts

@ -202,7 +202,10 @@ export class MapTimelinePanelComponent implements OnInit, OnDestroy {
public moveNext() {
if (this.index < this.maxTimeIndex) {
if (this.settings.snapToRealLocation) {
const anchorIndex = this.findIndex(this.currentTime, this.anchors) + 1;
let anchorIndex = this.findIndex(this.currentTime, this.anchors) + 1;
if (anchorIndex >= this.anchors.length) {
anchorIndex = this.anchors.length - 1;
}
this.index = Math.floor((this.anchors[anchorIndex] - this.minValue) / this.settings.timeStep);
} else {
this.index++;
@ -214,7 +217,10 @@ export class MapTimelinePanelComponent implements OnInit, OnDestroy {
public movePrev() {
if (this.index > this.minTimeIndex) {
if (this.settings.snapToRealLocation) {
const anchorIndex = this.findIndex(this.currentTime, this.anchors) - 1;
let anchorIndex = this.findIndex(this.currentTime, this.anchors) - 1;
if (anchorIndex < 0) {
anchorIndex = 0;
}
this.index = Math.floor((this.anchors[anchorIndex] - this.minValue) / this.settings.timeStep);
} else {
this.index--;

37
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html

@ -15,9 +15,14 @@
limitations under the License.
-->
<mat-form-field [formGroup]="selectFilterFormGroup" class="mat-block" [appearance]="appearance" [subscriptSizing]="subscriptSizing">
<mat-label *ngIf="showLabel">{{ 'filter.filter' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ !showLabel ? ('filter.filter' | translate) : ''}}"
<mat-form-field [formGroup]="selectFilterFormGroup"
[class.tb-inline-field]="inlineField"
[class.flex]="inlineField"
class="mat-block"
[appearance]="inlineField ? 'outline' : appearance"
[subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing">
<mat-label *ngIf="showLabel && !inlineField">{{ 'filter.filter' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ (!showLabel || inlineField) ? ('filter.filter' | translate) : ''}}"
#filterInput
formControlName="filter"
(focusin)="onFocus()"
@ -25,13 +30,23 @@
(keydown)="filterEnter($event)"
(keypress)="filterEnter($event)"
[matAutocomplete]="filterAutocomplete">
<button *ngIf="selectFilterFormGroup.get('filter').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<button *ngIf="!selectFilterFormGroup.get('filter').value && !disabled"
<div matSuffix class="mat-mdc-form-field-icon-suffix flex flex-row">
<button *ngIf="selectFilterFormGroup.get('filter').value && !disabled"
type="button"
mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-icon matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'filter.filter-required' | translate"
*ngIf="inlineField && !modelValue && tbRequired
&& selectFilterFormGroup.get('filter').touched"
class="tb-error">
warning
</mat-icon>
</div>
<button *ngIf="!inlineField && !selectFilterFormGroup.get('filter').value && !disabled"
style="margin-right: 8px;"
type="button"
matSuffix mat-button color="primary"
@ -61,7 +76,7 @@
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="!modelValue && tbRequired">
<mat-error *ngIf="!inlineField && !modelValue && tbRequired">
{{ 'filter.filter-required' | translate }}
</mat-error>
</mat-form-field>

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts

@ -70,6 +70,10 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte
@coerceBoolean()
showLabel: boolean;
@Input()
@coerceBoolean()
inlineField: boolean;
@Input()
appearance: MatFormFieldAppearance = 'fill';

80
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.html

@ -0,0 +1,80 @@
<!--
Copyright © 2016-2025 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]="dataSourceFormGroup" class="tb-form-table-row tb-additional-map-data-source-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>
<mat-form-field
*ngIf="dataSourceFormGroup.get('dsType').value === DatasourceType.function"
class="tb-label-field tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="dsLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-entity-autocomplete
class="tb-device-field"
*ngIf="dataSourceFormGroup.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="dataSourceFormGroup.get('dsType').value === DatasourceType.entity"
inlineField
tbRequired
[aliasController]="context.aliasController"
formControlName="dsEntityAliasId"
[callbacks]="context.callbacks">
</tb-entity-alias-select>
</div>
<tb-data-keys
class="tb-data-keys-field"
formControlName="dataKeys"
inlineField
hideDataKeyColor
hideDataKeyDecimals
hideDataKeyUnits
disableDrag
[required]="true"
[widgetType]="widgetType.latest"
[widget]="context.widget"
[datasourceType]="dataSourceFormGroup.get('dsType').value"
[entityAliasId]="dataSourceFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataSourceFormGroup.get('dsDeviceId').value"
[aliasController]="context.aliasController"
[callbacks]="context.callbacks"
[generateKey]="generateAdditionalDataKey">
</tb-data-keys>
<div class="tb-form-table-row-cell-buttons">
<div class="tb-remove-button">
<button type="button"
mat-icon-button
(click)="dataSourceRemoved.emit()"
matTooltip="{{ 'widgets.maps.data-layer.remove-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</div>

56
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.scss

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2025 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-additional-map-data-source-row {
.tb-source-field {
flex: 1 1 50%;
display: flex;
gap: 12px;
.tb-ds-type-field, .tb-label-field, .tb-device-field, .tb-entity-alias-field {
flex: 1;
}
}
.tb-data-keys-field {
flex: 1 1 50%;
min-width: 0;
}
.tb-remove-button {
width: 40px;
min-width: 40px;
}
@media #{$mat-lt-lg} {
.tb-source-field {
flex-direction: column;
flex: 1 1 30%;
}
.tb-data-keys-field {
flex: 1 1 70%;
}
@media #{$mat-lt-md} {
.tb-source-field {
flex: 1 1 50%;
}
.tb-data-keys-field {
flex: 1 1 50%;
}
}
}
}

207
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-source-row.component.ts

@ -0,0 +1,207 @@
///
/// Copyright © 2016-2025 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,
ViewEncapsulation
} from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AdditionalMapDataSourceSettings } from '@shared/models/widget/maps/map.models';
import { DataKey, 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 { genNextLabelForDataKeys } from '@core/utils';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
@Component({
selector: 'tb-additional-map-data-source-row',
templateUrl: './additional-map-data-source-row.component.html',
styleUrls: ['./additional-map-data-source-row.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AdditionalMapDataSourceRowComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class AdditionalMapDataSourceRowComponent implements ControlValueAccessor, OnInit {
DatasourceType = DatasourceType;
DataKeyType = DataKeyType;
EntityType = EntityType;
widgetType = widgetType;
datasourceTypes: Array<DatasourceType> = [];
datasourceTypesTranslations = datasourceTypeTranslationMap;
@Input()
disabled: boolean;
@Input()
context: MapSettingsContext;
@Output()
dataSourceRemoved = new EventEmitter();
dataSourceFormGroup: UntypedFormGroup;
generateAdditionalDataKey = this.generateDataKey.bind(this);
modelValue: AdditionalMapDataSourceSettings;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef,
private destroyRef: DestroyRef) {
}
ngOnInit() {
if (this.context.functionsOnly) {
this.datasourceTypes = [DatasourceType.function];
} else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity];
}
this.dataSourceFormGroup = this.fb.group({
dsType: [null, [Validators.required]],
dsLabel: [null, []],
dsDeviceId: [null, [Validators.required]],
dsEntityAliasId: [null, [Validators.required]],
dataKeys: [null, [Validators.required]]
});
this.dataSourceFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => this.updateModel()
);
this.dataSourceFormGroup.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.dataSourceFormGroup.disable({emitEvent: false});
} else {
this.dataSourceFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: AdditionalMapDataSourceSettings): void {
this.modelValue = value;
this.dataSourceFormGroup.patchValue(
{
dsType: value?.dsType,
dsLabel: value?.dsLabel,
dsDeviceId: value?.dsDeviceId,
dsEntityAliasId: value?.dsEntityAliasId,
dataKeys: value?.dataKeys
}, {emitEvent: false}
);
this.updateValidators();
this.cd.markForCheck();
}
private generateDataKey(key: DataKey): DataKey {
const dataKey = this.context.callbacks.generateDataKey(key.name, key.type, null, false, null);
const dataKeys: DataKey[] = this.dataSourceFormGroup.get('dataKeys').value || [];
dataKey.label = genNextLabelForDataKeys(dataKey.label, dataKeys);
return dataKey;
}
private onDsTypeChanged(newDsType: DatasourceType) {
let updateModel = false;
const dataKeys: DataKey[] = this.dataSourceFormGroup.get('dataKeys').value;
if (dataKeys?.length) {
for (const key of dataKeys) {
updateModel = this.updateDataKeyToNewDsType(key, newDsType) || updateModel;
}
if (updateModel) {
this.dataSourceFormGroup.get('dataKeys').patchValue(dataKeys, {emitEvent: false});
}
}
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.dataSourceFormGroup.get('dsType').value;
if (dsType === DatasourceType.function) {
this.dataSourceFormGroup.get('dsLabel').enable({emitEvent: false});
this.dataSourceFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataSourceFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else if (dsType === DatasourceType.device) {
this.dataSourceFormGroup.get('dsLabel').disable({emitEvent: false});
this.dataSourceFormGroup.get('dsDeviceId').enable({emitEvent: false});
this.dataSourceFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else {
this.dataSourceFormGroup.get('dsLabel').disable({emitEvent: false});
this.dataSourceFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataSourceFormGroup.get('dsEntityAliasId').enable({emitEvent: false});
}
}
private updateModel() {
this.modelValue = {...this.modelValue, ...this.dataSourceFormGroup.value};
this.propagateChange(this.modelValue);
}
}

43
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.html

@ -0,0 +1,43 @@
<!--
Copyright © 2016-2025 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-additional-map-data-sources">
<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 class="tb-form-table-header-cell tb-data-keys-header" translate>widgets.maps.data-layer.data-keys</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
<div *ngIf="dataSourcesFormArray().controls.length; else noDataSources" class="tb-form-table-body">
<div *ngFor="let dataSourceControl of dataSourcesFormArray().controls; trackBy: trackByDataSource; let $index = index;">
<tb-additional-map-data-source-row class="flex-1"
[context]="context"
[formControl]="dataSourceControl"
(dataSourceRemoved)="removeDataSource($index)">
</tb-additional-map-data-source-row>
</div>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addDataSource()">
{{ 'widgets.maps.data-layer.add-datasource' | translate }}
</button>
</div>
</div>
<ng-template #noDataSources>
<span class="tb-prompt flex items-center justify-center">{{ 'widgets.maps.data-layer.no-datasources' | translate }}</span>
</ng-template>

53
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.scss

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2025 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-additional-map-data-sources {
.tb-form-table-header-cell {
&.tb-source-header {
flex: 1 1 50%;
}
&.tb-data-keys-header {
flex: 1 1 50%;
}
&.tb-actions-header {
width: 40px;
min-width: 40px;
}
@media #{$mat-lt-lg} {
&.tb-source-header {
flex: 1 1 30%;
}
&.tb-data-keys-header {
flex: 1 1 70%;
}
@media #{$mat-lt-md} {
&.tb-source-header {
flex: 1 1 50%;
}
&.tb-data-keys-header {
flex: 1 1 50%;
}
}
}
}
.tb-form-table-body {
tb-additional-map-data-source-row {
overflow: hidden;
}
}
}

147
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/additional-map-data-sources.component.ts

@ -0,0 +1,147 @@
///
/// Copyright © 2016-2025 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 {
AdditionalMapDataSourceSettings,
additionalMapDataSourceValid,
additionalMapDataSourceValidator,
defaultAdditionalMapDataSourceSettings
} from '@shared/models/widget/maps/map.models';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
@Component({
selector: 'tb-additional-map-data-sources',
templateUrl: './additional-map-data-sources.component.html',
styleUrls: ['./additional-map-data-sources.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AdditionalMapDataSourcesComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AdditionalMapDataSourcesComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class AdditionalMapDataSourcesComponent implements ControlValueAccessor, OnInit, Validator {
@Input()
disabled: boolean;
@Input()
context: MapSettingsContext;
dataSourcesFormGroup: UntypedFormGroup;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit() {
this.dataSourcesFormGroup = this.fb.group({
dataSources: [this.fb.array([]), []]
});
this.dataSourcesFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => {
let dataSources: AdditionalMapDataSourceSettings[] = this.dataSourcesFormGroup.get('dataSources').value;
if (dataSources) {
dataSources = dataSources.filter(dataSource => additionalMapDataSourceValid(dataSource));
}
this.propagateChange(dataSources);
}
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.dataSourcesFormGroup.disable({emitEvent: false});
} else {
this.dataSourcesFormGroup.enable({emitEvent: false});
}
}
writeValue(value: AdditionalMapDataSourceSettings[] | undefined): void {
const dataSources: AdditionalMapDataSourceSettings[] = value || [];
this.dataSourcesFormGroup.setControl('dataSources', this.prepareDataSourcesFormArray(dataSources), {emitEvent: false});
}
public validate(c: UntypedFormControl) {
const valid = this.dataSourcesFormGroup.valid;
return valid ? null : {
dataSources: {
valid: false,
},
};
}
dataSourcesFormArray(): UntypedFormArray {
return this.dataSourcesFormGroup.get('dataSources') as UntypedFormArray;
}
trackByDataSource(index: number, dataSourceControl: AbstractControl): any {
return dataSourceControl;
}
removeDataSource(index: number) {
(this.dataSourcesFormGroup.get('dataSources') as UntypedFormArray).removeAt(index);
}
addDataSource() {
const dataSource = mergeDeep<AdditionalMapDataSourceSettings>({} as AdditionalMapDataSourceSettings,
defaultAdditionalMapDataSourceSettings(this.context.functionsOnly));
const dataSourcesArray = this.dataSourcesFormGroup.get('dataSources') as UntypedFormArray;
const dataSourceControl = this.fb.control(dataSource, [additionalMapDataSourceValidator]);
dataSourcesArray.push(dataSourceControl);
}
private prepareDataSourcesFormArray(dataSources: AdditionalMapDataSourceSettings[]): UntypedFormArray {
const dataSourcesControls: Array<AbstractControl> = [];
dataSources.forEach((dataSource) => {
dataSourcesControls.push(this.fb.control(dataSource, [additionalMapDataSourceValidator]));
});
return this.fb.array(dataSourcesControls);
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/data-layer-color-settings-panel.component.html

@ -39,7 +39,7 @@
<div class="tb-data-layer-color-settings-panel-body" [class.!hidden]="colorSettingsFormGroup.get('type').value !== DataLayerColorType.constant">
</div>
<div class="tb-data-layer-color-settings-panel-body" [class.!hidden]="colorSettingsFormGroup.get('type').value !== DataLayerColorType.range">
<tb-data-key-input
<tb-data-key-input style="padding-top: 6px;"
inlineField="false"
appearance="outline"
subscriptSizing="fixed"

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

@ -65,6 +65,18 @@
[callbacks]="context.callbacks">
</tb-filter-select>
</div>
<mat-expansion-panel *ngIf="dataLayerFormGroup.get('dsType').value !== DatasourceType.function" class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description class="flex items-stretch justify-start" translate>
widgets.maps.data-layer.more-datasources
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-map-data-sources formControlName="additionalDataSources"
[context]="context">
</tb-map-data-sources>
</ng-template>
</mat-expansion-panel>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'datakey.keys' | translate }}</div>

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

@ -179,6 +179,7 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
dsDeviceId: [this.settings.dsDeviceId, [Validators.required]],
dsEntityAliasId: [this.settings.dsEntityAliasId, [Validators.required]],
dsFilterId: [this.settings.dsFilterId, []],
additionalDataSources: [this.settings.additionalDataSources, []],
additionalDataKeys: [this.settings.additionalDataKeys, []],
label: [this.settings.label, []],
tooltip: [this.settings.tooltip, []],
@ -345,14 +346,17 @@ export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDia
private updateValidators() {
const dsType: DatasourceType = this.dataLayerFormGroup.get('dsType').value;
if (dsType === DatasourceType.function) {
this.dataLayerFormGroup.get('additionalDataSources').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsLabel').enable({emitEvent: false});
this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else if (dsType === DatasourceType.device) {
this.dataLayerFormGroup.get('additionalDataSources').enable({emitEvent: false});
this.dataLayerFormGroup.get('dsLabel').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsDeviceId').enable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else {
this.dataLayerFormGroup.get('additionalDataSources').enable({emitEvent: false});
this.dataLayerFormGroup.get('dsLabel').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataLayerFormGroup.get('dsEntityAliasId').enable({emitEvent: false});

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

@ -336,6 +336,9 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
private updateModel() {
this.modelValue = {...this.modelValue, ...this.dataLayerFormGroup.value};
if (this.modelValue.dsType === DatasourceType.function) {
delete this.modelValue.additionalDataSources;
}
this.propagateChange(this.modelValue);
}
}

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

@ -24,11 +24,6 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field
*ngIf="dataSourceFormGroup.get('dsType').value === DatasourceType.function"
class="tb-label-field tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="dsLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-entity-autocomplete
class="tb-device-field"
*ngIf="dataSourceFormGroup.get('dsType').value === DatasourceType.device"
@ -48,24 +43,13 @@
[callbacks]="context.callbacks">
</tb-entity-alias-select>
</div>
<tb-data-keys
class="tb-data-keys-field"
formControlName="dataKeys"
<tb-filter-select
class="tb-filter-field"
inlineField
hideDataKeyColor
hideDataKeyDecimals
hideDataKeyUnits
disableDrag
[required]="true"
[widgetType]="widgetType.latest"
[widget]="context.widget"
[datasourceType]="dataSourceFormGroup.get('dsType').value"
[entityAliasId]="dataSourceFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataSourceFormGroup.get('dsDeviceId').value"
[aliasController]="context.aliasController"
[callbacks]="context.callbacks"
[generateKey]="generateAdditionalDataKey">
</tb-data-keys>
formControlName="dsFilterId"
[callbacks]="context.callbacks">
</tb-filter-select>
<div class="tb-form-table-row-cell-buttons">
<div class="tb-remove-button">
<button type="button"

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

@ -18,16 +18,16 @@
.tb-form-table-row.tb-map-data-source-row {
.tb-source-field {
flex: 1 1 50%;
flex: 1 1 70%;
display: flex;
gap: 12px;
.tb-ds-type-field, .tb-label-field, .tb-device-field, .tb-entity-alias-field {
.tb-ds-type-field, .tb-device-field, .tb-entity-alias-field {
flex: 1;
}
}
.tb-data-keys-field {
flex: 1 1 50%;
.tb-filter-field {
flex: 1 1 30%;
min-width: 0;
}
@ -36,21 +36,13 @@
min-width: 40px;
}
@media #{$mat-lt-lg} {
@media #{$mat-lt-sm} {
.tb-source-field {
flex: 1 1 60%;
flex-direction: column;
flex: 1 1 30%;
}
.tb-data-keys-field {
flex: 1 1 70%;
}
@media #{$mat-lt-md} {
.tb-source-field {
flex: 1 1 50%;
}
.tb-data-keys-field {
flex: 1 1 50%;
}
.tb-filter-field {
flex: 1 1 40%;
}
}
}

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

@ -33,11 +33,9 @@ import {
Validators
} from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AdditionalMapDataSourceSettings } from '@shared/models/widget/maps/map.models';
import { DataKey, DatasourceType, datasourceTypeTranslationMap, widgetType } from '@shared/models/widget.models';
import { MapDataSourceSettings } from '@shared/models/widget/maps/map.models';
import { 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 { genNextLabelForDataKeys } from '@core/utils';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
@Component({
@ -56,13 +54,12 @@ import { MapSettingsContext } from '@home/components/widget/lib/settings/common/
export class MapDataSourceRowComponent implements ControlValueAccessor, OnInit {
DatasourceType = DatasourceType;
DataKeyType = DataKeyType;
EntityType = EntityType;
widgetType = widgetType;
datasourceTypes: Array<DatasourceType> = [];
datasourceTypes: Array<DatasourceType> = [DatasourceType.device, DatasourceType.entity];
datasourceTypesTranslations = datasourceTypeTranslationMap;
@Input()
@ -76,9 +73,7 @@ export class MapDataSourceRowComponent implements ControlValueAccessor, OnInit {
dataSourceFormGroup: UntypedFormGroup;
generateAdditionalDataKey = this.generateDataKey.bind(this);
modelValue: AdditionalMapDataSourceSettings;
modelValue: MapDataSourceSettings;
private propagateChange = (_val: any) => {};
@ -88,17 +83,11 @@ export class MapDataSourceRowComponent implements ControlValueAccessor, OnInit {
}
ngOnInit() {
if (this.context.functionsOnly) {
this.datasourceTypes = [DatasourceType.function];
} else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity];
}
this.dataSourceFormGroup = this.fb.group({
dsType: [null, [Validators.required]],
dsLabel: [null, []],
dsDeviceId: [null, [Validators.required]],
dsEntityAliasId: [null, [Validators.required]],
dataKeys: [null, [Validators.required]]
dsFilterId: [null, []]
});
this.dataSourceFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
@ -108,7 +97,7 @@ export class MapDataSourceRowComponent implements ControlValueAccessor, OnInit {
this.dataSourceFormGroup.get('dsType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
(newDsType: DatasourceType) => this.onDsTypeChanged(newDsType)
() => this.updateValidators()
);
}
@ -129,72 +118,27 @@ export class MapDataSourceRowComponent implements ControlValueAccessor, OnInit {
}
}
writeValue(value: AdditionalMapDataSourceSettings): void {
writeValue(value: MapDataSourceSettings): void {
this.modelValue = value;
this.dataSourceFormGroup.patchValue(
{
dsType: value?.dsType,
dsLabel: value?.dsLabel,
dsDeviceId: value?.dsDeviceId,
dsEntityAliasId: value?.dsEntityAliasId,
dataKeys: value?.dataKeys
dsFilterId: value?.dsFilterId
}, {emitEvent: false}
);
this.updateValidators();
this.cd.markForCheck();
}
private generateDataKey(key: DataKey): DataKey {
const dataKey = this.context.callbacks.generateDataKey(key.name, key.type, null, false, null);
const dataKeys: DataKey[] = this.dataSourceFormGroup.get('dataKeys').value || [];
dataKey.label = genNextLabelForDataKeys(dataKey.label, dataKeys);
return dataKey;
}
private onDsTypeChanged(newDsType: DatasourceType) {
let updateModel = false;
const dataKeys: DataKey[] = this.dataSourceFormGroup.get('dataKeys').value;
if (dataKeys?.length) {
for (const key of dataKeys) {
updateModel = this.updateDataKeyToNewDsType(key, newDsType) || updateModel;
}
if (updateModel) {
this.dataSourceFormGroup.get('dataKeys').patchValue(dataKeys, {emitEvent: false});
}
}
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.dataSourceFormGroup.get('dsType').value;
if (dsType === DatasourceType.function) {
this.dataSourceFormGroup.get('dsLabel').enable({emitEvent: false});
this.dataSourceFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataSourceFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else if (dsType === DatasourceType.device) {
this.dataSourceFormGroup.get('dsLabel').disable({emitEvent: false});
if (dsType === DatasourceType.device) {
this.dataSourceFormGroup.get('dsDeviceId').enable({emitEvent: false});
this.dataSourceFormGroup.get('dsEntityAliasId').disable({emitEvent: false});
} else {
this.dataSourceFormGroup.get('dsLabel').disable({emitEvent: false});
this.dataSourceFormGroup.get('dsDeviceId').disable({emitEvent: false});
this.dataSourceFormGroup.get('dsEntityAliasId').enable({emitEvent: false});
}

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

@ -19,15 +19,15 @@
<div class="tb-form-table tb-map-data-sources">
<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 class="tb-form-table-header-cell tb-data-keys-header" translate>widgets.maps.data-layer.data-keys</div>
<div class="tb-form-table-header-cell tb-filter-header" translate>widgets.maps.data-layer.filter</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
<div *ngIf="dataSourcesFormArray().controls.length; else noDataSources" class="tb-form-table-body">
<div *ngFor="let dataSourceControl of dataSourcesFormArray().controls; trackBy: trackByDataSource; let $index = index;">
<tb-map-data-source-row class="flex-1"
[context]="context"
[formControl]="dataSourceControl"
(dataSourceRemoved)="removeDataSource($index)">
[context]="context"
[formControl]="dataSourceControl"
(dataSourceRemoved)="removeDataSource($index)">
</tb-map-data-source-row>
</div>
</div>

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

@ -13,36 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../../scss/constants';
.tb-map-data-sources {
.tb-form-table-header-cell {
&.tb-source-header {
flex: 1 1 50%;
flex: 1 1 70%;
}
&.tb-data-keys-header {
flex: 1 1 50%;
&.tb-filter-header {
flex: 1 1 30%;
}
&.tb-actions-header {
width: 40px;
min-width: 40px;
}
@media #{$mat-lt-lg} {
&.tb-source-header {
flex: 1 1 30%;
}
&.tb-data-keys-header {
flex: 1 1 70%;
}
@media #{$mat-lt-md} {
&.tb-source-header {
flex: 1 1 50%;
}
&.tb-data-keys-header {
flex: 1 1 50%;
}
}
}
}
.tb-form-table-body {

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

@ -29,10 +29,10 @@ import {
import { mergeDeep } from '@core/utils';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
AdditionalMapDataSourceSettings,
additionalMapDataSourceValid,
additionalMapDataSourceValidator,
defaultAdditionalMapDataSourceSettings
defaultMapDataSourceSettings,
MapDataSourceSettings,
mapDataSourceValid,
mapDataSourceValidator
} from '@shared/models/widget/maps/map.models';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
@ -78,9 +78,9 @@ export class MapDataSourcesComponent implements ControlValueAccessor, OnInit, Va
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => {
let dataSources: AdditionalMapDataSourceSettings[] = this.dataSourcesFormGroup.get('dataSources').value;
let dataSources: MapDataSourceSettings[] = this.dataSourcesFormGroup.get('dataSources').value;
if (dataSources) {
dataSources = dataSources.filter(dataSource => additionalMapDataSourceValid(dataSource));
dataSources = dataSources.filter(dataSource => mapDataSourceValid(dataSource));
}
this.propagateChange(dataSources);
}
@ -103,8 +103,8 @@ export class MapDataSourcesComponent implements ControlValueAccessor, OnInit, Va
}
}
writeValue(value: AdditionalMapDataSourceSettings[] | undefined): void {
const dataSources: AdditionalMapDataSourceSettings[] = value || [];
writeValue(value: MapDataSourceSettings[] | undefined): void {
const dataSources: MapDataSourceSettings[] = value || [];
this.dataSourcesFormGroup.setControl('dataSources', this.prepareDataSourcesFormArray(dataSources), {emitEvent: false});
}
@ -130,17 +130,17 @@ export class MapDataSourcesComponent implements ControlValueAccessor, OnInit, Va
}
addDataSource() {
const dataSource = mergeDeep<AdditionalMapDataSourceSettings>({} as AdditionalMapDataSourceSettings,
defaultAdditionalMapDataSourceSettings(this.context.functionsOnly));
const dataSource = mergeDeep<MapDataSourceSettings>({} as MapDataSourceSettings,
defaultMapDataSourceSettings);
const dataSourcesArray = this.dataSourcesFormGroup.get('dataSources') as UntypedFormArray;
const dataSourceControl = this.fb.control(dataSource, [additionalMapDataSourceValidator]);
const dataSourceControl = this.fb.control(dataSource, [mapDataSourceValidator]);
dataSourcesArray.push(dataSourceControl);
}
private prepareDataSourcesFormArray(dataSources: AdditionalMapDataSourceSettings[]): UntypedFormArray {
private prepareDataSourcesFormArray(dataSources: MapDataSourceSettings[]): UntypedFormArray {
const dataSourcesControls: Array<AbstractControl> = [];
dataSources.forEach((dataSource) => {
dataSourcesControls.push(this.fb.control(dataSource, [additionalMapDataSourceValidator]));
dataSourcesControls.push(this.fb.control(dataSource, [mapDataSourceValidator]));
});
return this.fb.array(dataSourcesControls);
}

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

@ -73,9 +73,9 @@
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'widgets.maps.data-layer.additional-datasources-hint' | translate }}">
{{ 'widgets.maps.data-layer.additional-datasources' | translate }}
</div>
<tb-map-data-sources formControlName="additionalDataSources"
<tb-additional-map-data-sources formControlName="additionalDataSources"
[context]="context">
</tb-map-data-sources>
</tb-additional-map-data-sources>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">

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

@ -226,10 +226,10 @@ import { MarkerShapesComponent } from '@home/components/widget/lib/settings/comm
import {
MarkerClusteringSettingsComponent
} from '@home/components/widget/lib/settings/common/map/marker-clustering-settings.component';
import { MapDataSourcesComponent } from '@home/components/widget/lib/settings/common/map/map-data-sources.component';
import { AdditionalMapDataSourcesComponent } from '@home/components/widget/lib/settings/common/map/additional-map-data-sources.component';
import {
MapDataSourceRowComponent
} from '@home/components/widget/lib/settings/common/map/map-data-source-row.component';
AdditionalMapDataSourceRowComponent
} from '@home/components/widget/lib/settings/common/map/additional-map-data-source-row.component';
import {
ImageMapSourceSettingsComponent
} from '@home/components/widget/lib/settings/common/map/image-map-source-settings.component';
@ -248,6 +248,10 @@ import {
import {
MarkerIconShapesComponent
} from '@home/components/widget/lib/settings/common/map/marker-icon-shapes.component';
import { MapDataSourcesComponent } from '@home/components/widget/lib/settings/common/map/map-data-sources.component';
import {
MapDataSourceRowComponent
} from '@home/components/widget/lib/settings/common/map/map-data-source-row.component';
@NgModule({
declarations: [
@ -339,6 +343,8 @@ import {
MapDataLayersComponent,
MapDataSourceRowComponent,
MapDataSourcesComponent,
AdditionalMapDataSourceRowComponent,
AdditionalMapDataSourcesComponent,
TripTimelineSettingsComponent,
MapSettingsComponent,
EntityAliasSelectComponent,

94
ui-ngx/src/app/shared/models/widget/maps/map-export.models.ts

@ -18,14 +18,25 @@ import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models
import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { DatasourceType, Widget } from '@shared/models/widget.models';
import { BaseMapSettings, MapDataSourceSettings, MapType } from '@shared/models/widget/maps/map.models';
import {
BaseMapSettings,
MapDataLayerSettings,
MapDataSourceSettings,
MapType
} from '@shared/models/widget/maps/map.models';
import { WidgetExportDefinition } from '@shared/models/widget/widget-export.models';
interface ExportDataSourceInfo {
aliases: {[dataLayerIndex: number]: EntityAliasInfo};
filters: {[dataLayerIndex: number]: FilterInfo};
interface AliasFilterPair {
alias?: EntityAliasInfo,
filter?: FilterInfo
}
interface MapDataLayerDsInfo extends AliasFilterPair {
additionalDsInfo?: {[dsIndex: number]: AliasFilterPair}
}
type ExportDataSourceInfo = {[dataLayerIndex: number]: MapDataLayerDsInfo};
interface MapDatasourcesInfo {
trips?: ExportDataSourceInfo;
markers?: ExportDataSourceInfo;
@ -96,43 +107,67 @@ export const MapExportDefinition: WidgetExportDefinition<MapDatasourcesInfo> = {
};
const updateMapDatasourceFromExportInfo = (entityAliases: EntityAliases,
filters: Filters, settings: MapDataSourceSettings[], info: ExportDataSourceInfo): void => {
if (info.aliases) {
for (const dsIndexStr of Object.keys(info.aliases)) {
const dsIndex = Number(dsIndexStr);
if (settings[dsIndex] && settings[dsIndex].dsType === DatasourceType.entity) {
const aliasInfo = info.aliases[dsIndex];
settings[dsIndex].dsEntityAliasId = getEntityAliasId(entityAliases, aliasInfo);
filters: Filters, settings: MapDataLayerSettings[] | MapDataSourceSettings[],
info: MapDataLayerDsInfo | {[dsIndex: number]: AliasFilterPair}): void => {
for (const dsIndexStr of Object.keys(info)) {
const dsIndex = Number(dsIndexStr);
const dsInfo = info[dsIndex];
if (settings[dsIndex]) {
if (settings[dsIndex].dsType === DatasourceType.entity) {
if (settings[dsIndex].dsType === DatasourceType.entity) {
if (dsInfo.alias) {
settings[dsIndex].dsEntityAliasId = getEntityAliasId(entityAliases, dsInfo.alias);
}
if (dsInfo.filter) {
settings[dsIndex].dsFilterId = getFilterId(filters, dsInfo.filter);
}
}
}
}
}
if (info.filters) {
for (const dsIndexStr of Object.keys(info.filters)) {
const dsIndex = Number(dsIndexStr);
if (settings[dsIndex] && settings[dsIndex].dsType === DatasourceType.entity) {
const filterInfo = info.filters[dsIndex];
settings[dsIndex].dsFilterId = getFilterId(filters, filterInfo);
if (dsInfo.additionalDsInfo && (settings[dsIndex] as MapDataLayerSettings).additionalDataSources?.length) {
updateMapDatasourceFromExportInfo(entityAliases,
filters, (settings[dsIndex] as MapDataLayerSettings).additionalDataSources, dsInfo.additionalDsInfo);
}
}
}
}
const prepareExportDataSourcesInfo = (dashboard: Dashboard, settings: MapDataSourceSettings[]): ExportDataSourceInfo => {
const info: ExportDataSourceInfo = {
aliases: {},
filters: {}
};
const prepareExportDataSourcesInfo = (dashboard: Dashboard, settings: MapDataLayerSettings[] | MapDataSourceSettings[]): ExportDataSourceInfo => {
const info: ExportDataSourceInfo = {};
settings.forEach((dsSettings, index) => {
prepareExportDataSourceInfo(dashboard, info, dsSettings, index);
});
return info;
}
const prepareExportDataSourceInfo = (dashboard: Dashboard, info: ExportDataSourceInfo, settings: MapDataSourceSettings, index: number): void => {
const prepareExportDataSourceInfo = (dashboard: Dashboard, info: ExportDataSourceInfo, settings: MapDataLayerSettings | MapDataSourceSettings, index: number): void => {
const dsInfo: MapDataLayerDsInfo = {};
const aliasAndFilter = prepareAliasAndFilterPair(dashboard, settings);
if (aliasAndFilter) {
dsInfo.alias = aliasAndFilter.alias;
dsInfo.filter = aliasAndFilter.filter;
}
if ((settings as MapDataLayerSettings).additionalDataSources?.length && settings.dsType !== DatasourceType.function) {
(settings as MapDataLayerSettings).additionalDataSources.forEach((ds, index) => {
const dsAliasAndFilter = prepareAliasAndFilterPair(dashboard, ds);
if (dsAliasAndFilter) {
if (!dsInfo.additionalDsInfo) {
dsInfo.additionalDsInfo = {};
}
dsInfo.additionalDsInfo[index] = dsAliasAndFilter;
}
});
}
if (!!dsInfo.alias || !!dsInfo.filter || !!dsInfo.additionalDsInfo) {
info[index] = dsInfo;
}
}
const prepareAliasAndFilterPair = (dashboard: Dashboard, settings: MapDataSourceSettings): AliasFilterPair => {
const aliasAndFilter: AliasFilterPair = {};
if (settings.dsType === DatasourceType.entity) {
const entityAlias = dashboard.configuration.entityAliases[settings.dsEntityAliasId];
if (entityAlias) {
info.aliases[index] = {
aliasAndFilter.alias = {
alias: entityAlias.alias,
filter: entityAlias.filter
};
@ -140,7 +175,7 @@ const prepareExportDataSourceInfo = (dashboard: Dashboard, info: ExportDataSourc
if (settings.dsFilterId && dashboard.configuration.filters) {
const filter = dashboard.configuration.filters[settings.dsFilterId];
if (filter) {
info.filters[index] = {
aliasAndFilter.filter = {
filter: filter.filter,
keyFilters: filter.keyFilters,
editable: filter.editable
@ -148,4 +183,9 @@ const prepareExportDataSourceInfo = (dashboard: Dashboard, info: ExportDataSourc
}
}
}
if (!!aliasAndFilter.alias || !!aliasAndFilter.filter) {
return aliasAndFilter;
} else {
return null;
}
}

37
ui-ngx/src/app/shared/models/widget/maps/map.models.ts

@ -42,6 +42,7 @@ import { map } from 'rxjs/operators';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { MarkerIconContainer, MarkerShape } from '@shared/models/widget/maps/marker-shape.models';
import { ColorRange, DateFormatSettings, simpleDateFormat } from '@shared/models/widget-settings.models';
import _ from 'lodash';
export enum MapType {
geoMap = 'geoMap',
@ -60,7 +61,7 @@ export interface TbMapDatasource extends Datasource {
mapDataIds: string[];
}
export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings): TbMapDatasource => {
export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings, id = guid()): TbMapDatasource => {
return {
type: settings.dsType,
name: settings.dsLabel,
@ -68,7 +69,8 @@ export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSetting
entityAliasId: settings.dsEntityAliasId,
filterId: settings.dsFilterId,
dataKeys: [],
mapDataIds: [guid()]
latestDataKeys: [],
mapDataIds: [id]
};
};
@ -132,6 +134,7 @@ export interface DataLayerEditSettings {
}
export interface MapDataLayerSettings extends MapDataSourceSettings {
additionalDataSources?: MapDataSourceSettings[];
additionalDataKeys?: DataKey[];
label: DataLayerPatternSettings;
tooltip: DataLayerTooltipSettings;
@ -586,6 +589,34 @@ export const additionalMapDataSourcesToDatasources = (additionalMapDataSources:
});
};
export const mapDataSourceValid = (dataSource: MapDataSourceSettings): boolean => {
if (!dataSource.dsType || ![DatasourceType.device, DatasourceType.entity].includes(dataSource.dsType)) {
return false;
}
if (dataSource.dsType === DatasourceType.device && !dataSource.dsDeviceId) {
return false;
}
if (dataSource.dsType === DatasourceType.entity && !dataSource.dsEntityAliasId) {
return false;
}
return true;
};
export const mapDataSourceValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const dataSource: MapDataSourceSettings = control.value;
if (!mapDataSourceValid(dataSource)) {
return {
dataSource: true
};
}
return null;
};
export const defaultMapDataSourceSettings = {
dsType: DatasourceType.entity,
dsLabel: ''
};
export const additionalMapDataSourceValid = (dataSource: AdditionalMapDataSourceSettings): boolean => {
if (!dataSource.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataSource.dsType)) {
return false;
@ -1191,7 +1222,7 @@ const mapDatasourceIsSame = (ds1: TbMapDatasource, ds2: TbMapDatasource): boolea
}
const mergeMapDatasource = (target: TbMapDatasource, source: TbMapDatasource): TbMapDatasource => {
target.mapDataIds.push(...source.mapDataIds);
target.mapDataIds = _.union(target.mapDataIds, source.mapDataIds);
const appendKeys: DataKey[] = [];
for (const sourceKey of source.dataKeys) {
const found =

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

@ -7984,9 +7984,11 @@
},
"data-layer": {
"source": "Source",
"filter": "Filter",
"additional-data-keys": "Additional data keys",
"additional-datasources": "Additional datasources",
"additional-datasources-hint": "Datasource for accessing attributes or telemetry from entities not displayed on the map, usable in map overlay functions.",
"more-datasources": "More datasources",
"data-keys": "Data keys",
"add-datasource": "Add datasource",
"no-datasources": "No datasources configured",

Loading…
Cancel
Save