Browse Source

UI: Merkers data layer configuration.

pull/12723/head
Igor Kulikov 2 years ago
parent
commit
d8ea4f680b
  1. 23
      ui-ngx/src/app/core/utils.ts
  2. 16
      ui-ngx/src/app/modules/common/modules-map.ts
  3. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  4. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  5. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts
  6. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts
  7. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts
  8. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  9. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  10. 8
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  11. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts
  12. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  13. 2
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  14. 17
      ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
  15. 2
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
  16. 86
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts
  17. 332
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-data-layer.ts
  18. 166
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.models.ts
  19. 32
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss
  20. 96
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts
  21. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts
  22. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.ts
  23. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-bar-settings.component.ts
  24. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.ts
  25. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts
  26. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts
  27. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts
  28. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts
  29. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html
  30. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.models.ts
  31. 11
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts
  32. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/gradient.component.ts
  33. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html
  34. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.scss
  35. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts
  36. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html
  37. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts
  38. 13
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.html
  39. 27
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.scss
  40. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.ts
  41. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.html
  42. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.models.ts
  43. 31
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.scss
  44. 62
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.ts
  45. 198
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.html
  46. 41
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.scss
  47. 239
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-dialog.component.ts
  48. 40
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.html
  49. 126
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layer-row.component.ts
  50. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.html
  51. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-data-layers.component.ts
  52. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html
  53. 29
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.models.ts
  54. 60
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts
  55. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts
  56. 28
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  57. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html
  58. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.scss
  59. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.ts
  60. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html
  61. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts
  62. 2
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  63. 2
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  64. 2
      ui-ngx/src/app/shared/components/string-items-list.component.html
  65. 22
      ui-ngx/src/app/shared/components/string-items-list.component.scss
  66. 7
      ui-ngx/src/app/shared/components/string-items-list.component.ts
  67. 2
      ui-ngx/src/app/shared/models/widget.models.ts
  68. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  69. 23
      ui-ngx/src/typings/leaflet-extend-tb.d.ts

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

@ -17,7 +17,7 @@
import _ from 'lodash';
import { from, Observable, of, ReplaySubject, Subject } from 'rxjs';
import { catchError, finalize, share } from 'rxjs/operators';
import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models';
import { DataKey, Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models';
import { EntityId } from '@shared/models/id/entity-id';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { baseDetailsPageByEntityType, EntityType } from '@shared/models/entity-type.models';
@ -846,7 +846,7 @@ function prepareMessageFromData(data): string {
}
}
export function genNextLabel(name: string, datasources: Datasource[]): string {
export const genNextLabel = (name: string, datasources: Datasource[]): string => {
let label = name;
let i = 1;
let matches = false;
@ -880,6 +880,25 @@ export function genNextLabel(name: string, datasources: Datasource[]): string {
return label;
}
export const genNextLabelForDataKeys = (name: string, dataKeys: DataKey[]): string => {
let label = name;
let i = 1;
let matches = false;
if (dataKeys) {
do {
matches = false;
dataKeys.forEach((dataKey) => {
if (dataKey?.label === label) {
i++;
label = name + ' ' + i;
matches = true;
}
});
} while (matches)
}
return label;
}
export const getOS = (): string => {
const userAgent = window.navigator.userAgent.toLowerCase();
const macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos|mac_powerpc)/i;

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

@ -232,9 +232,9 @@ import * as EntityAliasDialogComponent from '@home/components/alias/entity-alias
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/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';
import * as DataKeysComponent from '@home/components/widget/lib/settings/common/key/data-keys.component';
import * as DataKeyConfigDialogComponent from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component';
import * as DataKeyConfigComponent from '@home/components/widget/lib/settings/common/key/data-key-config.component';
import * as LegendConfigComponent from '@home/components/widget/lib/settings/common/legend-config.component';
import * as ManageWidgetActionsComponent from '@home/components/widget/action/manage-widget-actions.component';
import * as WidgetActionDialogComponent from '@home/components/widget/action/widget-action-dialog.component';
@ -268,7 +268,7 @@ import * as ComplexFilterPredicateDialogComponent from '@home/components/filter/
import * as KeyFilterDialogComponent from '@home/components/filter/key-filter-dialog.component';
import * as FiltersDialogComponent from '@home/components/filter/filters-dialog.component';
import * as FilterDialogComponent from '@home/components/filter/filter-dialog.component';
import * as FilterSelectComponent from '@home/components/filter/filter-select.component';
import * as FilterSelectComponent from '@home/components/widget/lib/settings/common/filter/filter-select.component';
import * as FiltersEditComponent from '@home/components/filter/filters-edit.component';
import * as FiltersEditPanelComponent from '@home/components/filter/filters-edit-panel.component';
import * as UserFilterDialogComponent from '@home/components/filter/user-filter-dialog.component';
@ -578,9 +578,9 @@ class ModulesMap implements IModulesMap {
'@home/components/entity/entity-filter.component': EntityFilterComponent,
'@home/components/relation/relation-filters.component': RelationFiltersComponent,
'@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,
'@home/components/widget/lib/settings/common/key/data-keys.component': DataKeysComponent,
'@home/components/widget/lib/settings/common/key/data-key-config-dialog.component': DataKeyConfigDialogComponent,
'@home/components/widget/lib/settings/common/key/data-key-config.component': DataKeyConfigComponent,
'@home/components/widget/lib/settings/common/legend-config.component': LegendConfigComponent,
'@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent,
'@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent,
@ -610,7 +610,7 @@ class ModulesMap implements IModulesMap {
'@home/components/filter/key-filter-dialog.component': KeyFilterDialogComponent,
'@home/components/filter/filters-dialog.component': FiltersDialogComponent,
'@home/components/filter/filter-dialog.component': FilterDialogComponent,
'@home/components/filter/filter-select.component': FilterSelectComponent,
'@home/components/widget/lib/settings/common/filter/filter-select.component': FilterSelectComponent,
'@home/components/filter/filters-edit.component': FiltersEditComponent,
'@home/components/filter/filters-edit-panel.component': FiltersEditPanelComponent,
'@home/components/filter/user-filter-dialog.component': UserFilterDialogComponent,

2
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -29,7 +29,7 @@ import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-compo
import { isDefined, isDefinedAndNotNull } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
export interface AddWidgetDialogData {
dashboard: Dashboard;

2
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts

@ -27,7 +27,7 @@ import { WidgetConfigComponentData } from '../../models/widget-component.models'
import { isDefined, isDefinedAndNotNull } from '@core/utils';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({

4
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts

@ -45,7 +45,7 @@ import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
} from '@home/components/widget/config/data-key-config-dialog.component';
} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component';
import { deepClone, formatValue } from '@core/utils';
import {
AggregatedValueCardKeyPosition,
@ -59,7 +59,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-aggregated-data-key-row',
templateUrl: './aggregated-data-key-row.component.html',
styleUrls: ['./aggregated-data-key-row.component.scss', '../../data-keys.component.scss'],
styleUrls: ['./aggregated-data-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

2
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.ts

@ -38,7 +38,7 @@ import { WidgetConfigComponent } from '@home/components/widget/widget-config.com
import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { aggregatedValueCardDefaultKeySettings } from '@home/components/widget/lib/cards/aggregated-value-card.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

2
ui-ngx/src/app/modules/home/components/widget/config/basic/chart/comparison-key-row.component.ts

@ -34,7 +34,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-comparison-key-row',
templateUrl: './comparison-key-row.component.html',
styleUrls: ['./comparison-key-row.component.scss', '../../data-keys.component.scss'],
styleUrls: ['./comparison-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

6
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts

@ -41,12 +41,12 @@ import { MatDialog } from '@angular/material/dialog';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKey, DataKeyConfigMode, DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { merge } from 'rxjs';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
} from '@home/components/widget/config/data-key-config-dialog.component';
} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component';
import { deepClone } from '@core/utils';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
@ -78,7 +78,7 @@ export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors
@Component({
selector: 'tb-data-key-row',
templateUrl: './data-key-row.component.html',
styleUrls: ['./data-key-row.component.scss', '../../data-keys.component.scss'],
styleUrls: ['./data-key-row.component.scss', '../../../lib/settings/common/key/data-keys.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

2
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts

@ -43,7 +43,7 @@ import { dataKeyRowValidator, dataKeyValid } from '@home/components/widget/confi
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models';
import { FormProperty } from '@shared/models/dynamic-form.models';

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

@ -66,6 +66,10 @@
<tb-data-keys class="tb-data-keys flex-1"
[widgetType]="widgetType"
[datasourceType]="datasourceFormGroup.get('type').value"
[hideDataKeyLabel]="hideDataKeyLabel"
[hideDataKeyColor]="hideDataKeyColor"
[hideDataKeyUnits]="hideDataKeyUnits"
[hideDataKeyDecimals]="hideDataKeyDecimals"
[maxDataKeys]="maxDataKeys"
[optDataKeys]="isDataKeysOptional(datasourceFormGroup.get('type').value)"
[simpleDataKeysLabel]="!hasAdditionalLatestDataKeys"
@ -83,6 +87,10 @@
<tb-data-keys *ngIf="hasAdditionalLatestDataKeys" class="tb-data-keys flex-1"
[widgetType]="widgetTypes.latest"
[datasourceType]="datasourceFormGroup.get('type').value"
[hideDataKeyLabel]="hideDataKeyLabel"
[hideDataKeyColor]="hideDataKeyColor"
[hideDataKeyUnits]="hideDataKeyUnits"
[hideDataKeyDecimals]="hideDataKeyDecimals"
latestDataKeys
[optDataKeys]="true"
[aliasController]="aliasController"

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

@ -15,7 +15,7 @@
///
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';
import { FilterSelectCallbacks } from '@home/components/widget/lib/settings/common/filter/filter-select.component.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
export type DatasourceCallbacks = EntityAliasSelectCallbacks & FilterSelectCallbacks & DataKeysCallbacks;

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

@ -38,8 +38,8 @@ 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/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 { FilterSelectCallbacks } from '@home/components/widget/lib/settings/common/filter/filter-select.component.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DatasourcesComponent } from '@home/components/widget/config/datasources.component';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';

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

@ -39,7 +39,7 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { deepClone } from '@core/utils';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
import { FormProperty } from '@shared/models/dynamic-form.models';

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

@ -19,14 +19,9 @@ import { CommonModule } from '@angular/common';
import { SharedModule } from '@app/shared/shared.module';
import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component';
import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component';
import { DataKeysComponent } from '@home/components/widget/config/data-keys.component';
import { DataKeyConfigDialogComponent } from '@home/components/widget/config/data-key-config-dialog.component';
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 { 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';
import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.component';
import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module';
import { TimewindowStyleComponent } from '@home/components/widget/config/timewindow-style.component';
@ -38,17 +33,12 @@ import { TargetDeviceComponent } from '@home/components/widget/config/target-dev
[
AlarmAssigneeSelectComponent,
AlarmFilterConfigComponent,
DataKeysComponent,
DataKeyConfigDialogComponent,
DataKeyConfigComponent,
DatasourceComponent,
DatasourcesComponent,
TargetDeviceComponent,
FilterSelectComponent,
TimewindowStyleComponent,
TimewindowStylePanelComponent,
TimewindowConfigPanelComponent,
WidgetSettingsComponent
TimewindowConfigPanelComponent
],
imports: [
CommonModule,
@ -59,17 +49,12 @@ import { TargetDeviceComponent } from '@home/components/widget/config/target-dev
exports: [
AlarmAssigneeSelectComponent,
AlarmFilterConfigComponent,
DataKeysComponent,
DataKeyConfigDialogComponent,
DataKeyConfigComponent,
DatasourceComponent,
DatasourcesComponent,
TargetDeviceComponent,
FilterSelectComponent,
TimewindowStyleComponent,
TimewindowStylePanelComponent,
TimewindowConfigPanelComponent,
WidgetSettingsComponent,
WidgetSettingsCommonModule
]
})

2
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts

@ -71,7 +71,7 @@ import { BehaviorSubject } from 'rxjs';
import { AggregationType } from '@shared/models/time/time.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { DeepPartial } from '@shared/models/common';
import { BarRenderSharedContext } from '@home/components/widget/lib/chart/time-series-chart-bar.models';
import { TimeSeriesChartStateValueConverter } from '@home/components/widget/lib/chart/time-series-chart-state.models';

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

@ -15,7 +15,7 @@
///
import L, { Coords, TB, TileLayerOptions } from 'leaflet';
import { guid } from '@core/utils';
import { guid } from '@core/utils';
class SidebarControl extends L.Control<TB.SidebarControlOptions> {
@ -26,6 +26,8 @@ class SidebarControl extends L.Control<TB.SidebarControlOptions> {
private map: L.Map;
private buttonContainer: JQuery<HTMLElement>;
constructor(options: TB.SidebarControlOptions) {
super(options);
this.sidebar = $('<div class="tb-map-sidebar"></div>');
@ -38,8 +40,9 @@ class SidebarControl extends L.Control<TB.SidebarControlOptions> {
}
}
addPane(pane: JQuery<HTMLElement>): this {
addPane(pane: JQuery<HTMLElement>, button: JQuery<HTMLElement>): this {
pane.hide().appendTo(this.sidebar);
button.appendTo(this.buttonContainer);
return this;
}
@ -50,6 +53,7 @@ class SidebarControl extends L.Control<TB.SidebarControlOptions> {
this.current.hide().trigger('hide');
this.currentButton.removeClass('active');
if (this.current === pane) {
if (['topleft', 'bottomleft'].includes(position)) {
this.map.panBy([-paneWidth, 0], { animate: false });
@ -58,20 +62,26 @@ class SidebarControl extends L.Control<TB.SidebarControlOptions> {
this.current = this.currentButton = $();
} else {
this.sidebar.show();
this.current = pane;
this.currentButton = button || $();
if (['topleft', 'bottomleft'].includes(position)) {
if (['topleft', 'bottomleft'].includes(position) && !this.current.length) {
this.map.panBy([paneWidth, 0], { animate: false });
}
this.current = pane;
this.currentButton = button || $();
}
this.map.invalidateSize({ pan: false, animate: false });
this.current.show().trigger('show');
this.currentButton.addClass('active');
this.map.invalidateSize({ pan: false, animate: false });
}
onAdd(map: L.Map): HTMLElement {
this.buttonContainer = $("<div>")
.attr('class', 'leaflet-bar');
return this.buttonContainer[0];
}
addTo(map: L.Map): this {
this.map = map;
return this;
return super.addTo(map);
}
}
@ -84,9 +94,7 @@ class SidebarPaneControl<O extends TB.SidebarPaneControlOptions> extends L.Contr
super(options);
}
onAdd(map: L.Map): HTMLElement {
const $container = $("<div>")
.attr('class', 'leaflet-bar');
addTo(map: L.Map): this {
this.button = $("<a>")
.attr('class', 'tb-control-button')
@ -98,7 +106,6 @@ class SidebarPaneControl<O extends TB.SidebarPaneControlOptions> extends L.Contr
if (this.options.buttonTitle) {
this.button.attr('title', this.options.buttonTitle);
}
this.button.appendTo($container);
this.$ui = $('<div>')
.attr('class', this.options.uiClass);
@ -117,13 +124,13 @@ class SidebarPaneControl<O extends TB.SidebarPaneControlOptions> extends L.Contr
this.toggle(e);
})));
this.options.sidebar.addPane(this.$ui);
this.options.sidebar.addPane(this.$ui, this.button);
this.onAddPane(map, this.button, this.$ui, (e) => {
this.toggle(e);
});
return $container[0];
return this;
}
public onAddPane(map: L.Map, button: JQuery<HTMLElement>, $ui: JQuery<HTMLElement>, toggle: (e: JQuery.MouseEventBase) => void) {}
@ -216,6 +223,53 @@ class LayersControl extends SidebarPaneControl<TB.LayersControlOptions> {
}
}
class GroupsControl extends SidebarPaneControl<TB.GroupsControlOptions> {
constructor(options: TB.GroupsControlOptions) {
super(options);
}
public onAddPane(map: L.Map, button: JQuery<HTMLElement>, $ui: JQuery<HTMLElement>, toggle: (e: JQuery.MouseEventBase) => void) {
const paneId = guid();
const groups = this.options.groups;
const baseSection = $("<div>")
.attr('class', 'tb-layers-container')
.appendTo($ui);
groups.forEach((groupData, i) => {
const id = `map-group-layer-${paneId}-${i}`;
const checkBoxContainer = $('<div class="tb-group-checkbox">')
.appendTo(baseSection);
const input = $('<input type="checkbox" class="tb-group-button" name="group">')
.prop('id', id)
.prop('checked', groupData.enabled)
.appendTo(checkBoxContainer);
$('<label class="tb-group-label">')
.prop('title', groupData.title)
.prop('for', id)
.append($('<span>').append(groupData.title))
.appendTo(checkBoxContainer);
input.on('click', (e: JQuery.MouseEventBase) => {
e.stopPropagation();
groupData.enabled = !groupData.enabled;
let changed = false;
groupData.dataLayers.forEach(
(dl) => {
changed = dl.toggleGroup(groupData.group) || changed;
}
);
if (changed) {
map.fire('layergroupchange', {group: groupData});
}
});
map.on('layeradd layerremove', function () {
input.prop('checked', groupData.enabled);
});
});
}
}
const sidebar = (options: TB.SidebarControlOptions): SidebarControl => {
return new SidebarControl(options);
}
@ -228,6 +282,10 @@ const layers = (options: TB.LayersControlOptions): LayersControl => {
return new LayersControl(options);
}
const groups = (options: TB.GroupsControlOptions): GroupsControl => {
return new GroupsControl(options);
}
class ChinaProvider extends L.TileLayer {
static chinaProviders: L.TB.TileLayer.ChinaProvidersData = {
@ -282,9 +340,11 @@ L.TB = L.TB || {
SidebarControl,
SidebarPaneControl,
LayersControl,
GroupsControl,
sidebar,
sidebarPane,
layers,
groups,
TileLayer: {
ChinaProvider
},

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

@ -16,12 +16,26 @@
import {
CirclesDataLayerSettings,
DataLayerColorSettings,
DataLayerColorType,
defaultBaseCirclesDataLayerSettings,
defaultBaseMarkersDataLayerSettings,
defaultBasePolygonsDataLayerSettings,
isCutPolygon,
isJSON,
isValidLatLng,
loadImageWithAspect,
MapDataLayerSettings,
mapDataSourceSettingsToDatasource,
MapStringFunction,
MapType,
MarkerIconInfo,
MarkerImageFunction,
MarkerImageInfo,
MarkerImageSettings,
MarkerImageType,
MarkersDataLayerSettings,
MarkerType,
PolygonsDataLayerSettings,
TbCircleData,
TbMapDatasource
@ -29,9 +43,23 @@ import {
import { TbMap } from '@home/components/widget/lib/maps/map';
import { FormattedData } from '@shared/models/widget.models';
import { Observable, of } from 'rxjs';
import { guid, isDefinedAndNotNull, isEmptyStr, isNotEmptyStr, isString } from '@core/utils';
import {
guid,
isDefined,
isDefinedAndNotNull,
isEmptyStr,
isNotEmptyStr,
isString,
mergeDeepIgnoreArray,
parseTbFunction,
safeExecuteTbFunction
} from '@core/utils';
import L, { LatLngBounds } from 'leaflet';
import { isJSON } from '@home/components/widget/lib/maps-legacy/maps-utils';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { catchError, map } from 'rxjs/operators';
import tinycolor from 'tinycolor2';
import { WidgetContext } from '@home/models/widget-component.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
abstract class TbDataLayerItem<S extends MapDataLayerSettings, L extends TbMapDataLayer<S,L>> {
@ -67,7 +95,9 @@ export enum MapDataLayerType {
circle = 'circle'
}
export abstract class TbMapDataLayer<S extends MapDataLayerSettings, L extends TbMapDataLayer<S,L>> {
export abstract class TbMapDataLayer<S extends MapDataLayerSettings, L extends TbMapDataLayer<S,L>> implements L.TB.DataLayer {
protected settings: S;
protected datasource: TbMapDatasource;
@ -77,8 +107,19 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, L extends T
protected layerItems = new Map<string, TbDataLayerItem<S,L>>();
protected groupsState: {[group: string]: boolean} = {};
protected enabled = true;
protected constructor(protected map: TbMap<any>,
protected settings: S) {
inputSettings: S) {
this.settings = mergeDeepIgnoreArray({} as S, this.defaultBaseSettings() as S, inputSettings);
if (this.settings.groups?.length) {
this.settings.groups.forEach((group) => {
this.groupsState[group] = true;
});
}
this.map.getMap().addLayer(this.featureGroup);
}
public setup(): Observable<void> {
@ -101,6 +142,31 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, L extends T
return this.featureGroup.getBounds();
}
public isEnabled(): boolean {
return this.enabled;
}
public getGroups(): string[] {
return this.settings.groups || [];
}
public toggleGroup(group: string): boolean {
if (isDefined(this.groupsState[group])) {
this.groupsState[group] = !this.groupsState[group];
const enabled = Object.values(this.groupsState).some(v => v);
if (this.enabled !== enabled) {
this.enabled = enabled;
if (this.enabled) {
this.map.getMap().addLayer(this.featureGroup);
} else {
this.map.getMap().removeLayer(this.featureGroup);
}
return true;
}
}
return false;
}
public updateData(dsData: FormattedData<TbMapDatasource>[]) {
const layerData = dsData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId));
const rawItems = layerData.filter(d => this.isValidLayerData(d));
@ -122,6 +188,10 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, L extends T
});
}
public getCtx(): WidgetContext {
return this.map.getCtx();
}
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource {
return datasource;
}
@ -132,6 +202,8 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings, L extends T
public abstract dataLayerType(): MapDataLayerType;
protected abstract defaultBaseSettings(): Partial<S>;
protected abstract doSetup(): Observable<void>;
protected abstract isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean;
@ -144,6 +216,7 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
private location: L.LatLng;
private marker: L.Marker;
private labelOffset: L.PointTuple;
constructor(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[],
@ -157,6 +230,9 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
this.marker = L.marker(this.location, {
tbMarkerData: data
});
this.updateMarkerIcon(data, dsData);
return this.marker;
}
public update(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): void {
@ -165,15 +241,192 @@ class TbMarkerDataLayerItem extends TbDataLayerItem<MarkersDataLayerSettings, Tb
this.location = position;
this.marker.setLatLng(position);
}
this.updateMarkerIcon(data, dsData);
}
private updateMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
this.dataLayer.markerIconProcessor.createMarkerIcon(data, dsData).subscribe(
(iconInfo) => {
this.marker.setIcon(iconInfo.icon);
const anchor = iconInfo.icon.options.iconAnchor;
if (anchor && Array.isArray(anchor)) {
this.labelOffset = [iconInfo.size[0] / 2 - anchor[0], 10 - anchor[1]];
} else {
this.labelOffset = [0, -iconInfo.size[1] * this.dataLayer.markerOffset[1] + 10];
}
this.updateMarkerLabel(data, dsData);
}
);
}
private updateMarkerLabel(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]) {
}
}
abstract class MarkerIconProcessor<S> {
static fromSettings(dataLayer: TbMarkersDataLayer,
settings: MarkersDataLayerSettings): MarkerIconProcessor<any> {
switch (settings.markerType) {
case MarkerType.default:
return new ColorMarkerIconProcessor(dataLayer, settings.markerColor);
case MarkerType.image:
return new ImageMarkerIconProcessor(dataLayer, settings.markerImage);
}
}
protected constructor(protected dataLayer: TbMarkersDataLayer,
protected settings: S) {}
public abstract setup(): Observable<void>;
public abstract createMarkerIcon(data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo>;
}
class ColorMarkerIconProcessor extends MarkerIconProcessor<DataLayerColorSettings> {
private markerColorFunction: CompiledTbFunction<MapStringFunction>;
private defaultMarkerIconInfo: MarkerIconInfo;
constructor(protected dataLayer: TbMarkersDataLayer,
protected settings: DataLayerColorSettings) {
super(dataLayer, settings);
}
public setup(): Observable<void> {
if (this.settings.type === DataLayerColorType.function) {
return parseTbFunction<MapStringFunction>(this.dataLayer.getCtx().http, this.settings.colorFunction, ['data', 'dsData']).pipe(
map((parsed) => {
this.markerColorFunction = parsed;
return null;
})
);
} else {
const color = tinycolor(this.settings.color);
this.defaultMarkerIconInfo = this.dataLayer.createColoredMarkerIcon(color);
return of(null)
}
}
public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo> {
if (this.settings.type === DataLayerColorType.function) {
const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]);
let color: tinycolor.Instance;
if (isDefinedAndNotNull(functionColor)) {
color = tinycolor(functionColor);
} else {
color = tinycolor(this.settings.color);
}
return of(this.dataLayer.createColoredMarkerIcon(color));
} else {
return of(this.defaultMarkerIconInfo);
}
}
}
class ImageMarkerIconProcessor extends MarkerIconProcessor<MarkerImageSettings> {
private markerImageFunction: CompiledTbFunction<MarkerImageFunction>;
private defaultMarkerIconInfo: MarkerIconInfo;
constructor(protected dataLayer: TbMarkersDataLayer,
protected settings: MarkerImageSettings) {
super(dataLayer, settings);
}
public setup(): Observable<void> {
if (this.settings.type === MarkerImageType.function) {
return parseTbFunction<MarkerImageFunction>(this.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe(
map((parsed) => {
this.markerImageFunction = parsed;
return null;
})
);
} else {
const currentImage: MarkerImageInfo = {
url: this.settings.image,
size: this.settings.imageSize || 34
};
return this.loadMarkerIconInfo(currentImage).pipe(
map((iconInfo) => {
this.defaultMarkerIconInfo = iconInfo;
return null;
}
));
}
}
public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo> {
if (this.settings.type === MarkerImageType.function) {
const currentImage: MarkerImageInfo = safeExecuteTbFunction(this.markerImageFunction, [data, this.settings.images, dsData]);
return this.loadMarkerIconInfo(currentImage);
} else {
return of(this.defaultMarkerIconInfo);
}
}
private loadMarkerIconInfo(image: MarkerImageInfo): Observable<MarkerIconInfo> {
if (image && image.url) {
return loadImageWithAspect(this.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe(
map((aspectImage) => {
if (aspectImage?.aspect) {
let width: number;
let height: number;
if (aspectImage.aspect > 1) {
width = image.size;
height = image.size / aspectImage.aspect;
} else {
width = image.size * aspectImage.aspect;
height = image.size;
}
let iconAnchor = image.markerOffset;
let popupAnchor = image.tooltipOffset;
if (!iconAnchor) {
iconAnchor = [width * this.dataLayer.markerOffset[0], height * this.dataLayer.markerOffset[1]];
}
if (!popupAnchor) {
popupAnchor = [width * this.dataLayer.tooltipOffset[0], height * this.dataLayer.tooltipOffset[1]];
}
const icon = L.icon({
iconUrl: aspectImage.url,
iconSize: [width, height],
iconAnchor,
popupAnchor
});
const iconInfo: MarkerIconInfo = {
size: [width, height],
icon
};
return iconInfo;
} else {
return this.dataLayer.createDefaultMarkerIcon();
}
}),
catchError(() => of(this.dataLayer.createDefaultMarkerIcon()))
);
} else {
return of(this.dataLayer.createDefaultMarkerIcon());
}
}
}
export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings, TbMarkersDataLayer> {
public markerIconProcessor: MarkerIconProcessor<any>;
public markerOffset: L.LatLngTuple;
public tooltipOffset: L.LatLngTuple;
constructor(protected map: TbMap<any>,
protected settings: MarkersDataLayerSettings) {
super(map, settings);
inputSettings: MarkersDataLayerSettings) {
super(map, inputSettings);
}
public dataLayerType(): MapDataLayerType {
@ -185,8 +438,23 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
return datasource;
}
protected defaultBaseSettings(): Partial<MarkersDataLayerSettings> {
return defaultBaseMarkersDataLayerSettings;
}
protected doSetup(): Observable<void> {
return of(null);
this.markerOffset = [
isDefined(this.settings.markerOffsetX) ? this.settings.markerOffsetX : 0.5,
isDefined(this.settings.markerOffsetY) ? this.settings.markerOffsetY : 1,
];
this.tooltipOffset = [0, -1];
/* this.tooltipOffset = [
isDefined(this.settings.tooltipOffsetX) ? this.settings.tooltipOffsetX : 0,
isDefined(this.settings.tooltipOffsetY) ? this.settings.tooltipOffsetY : -1,
];*/
this.markerIconProcessor = MarkerIconProcessor.fromSettings(this, this.settings);
return this.markerIconProcessor.setup();
}
protected isValidLayerData(layerData: FormattedData<TbMapDatasource>): boolean {
@ -219,6 +487,39 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
}
}
private createColorIconURI(color: tinycolor.Instance): string {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-191.35 -351.18 1083.58 1730.46">` +
`<path fill-rule="evenodd" clip-rule="evenodd" fill="#${color.toHex()}" stroke="#000" stroke-width="37" ` +
`stroke-miterlimit="10" d="M351.833 1360.78c-38.766-190.3-107.116-348.665-189.903-495.44C100.523 756.469 ` +
`29.386 655.978-36.434 550.404c-21.972-35.244-40.934-72.477-62.047-109.054-42.216-73.137-76.444-157.935-74.269-267.932 ` +
`2.125-107.473 33.208-193.685 78.03-264.173C-21-206.69 102.481-301.745 268.164-326.724c135.466-20.425 262.475 14.082 ` +
`352.543 66.747 73.6 43.038 130.596 100.528 173.92 168.28 45.22 70.716 76.36 154.26 78.971 263.233 1.337 55.83-7.805 ` +
`107.532-20.684 150.417-13.034 43.41-33.996 79.695-52.646 118.455-36.406 75.659-82.049 144.981-127.855 214.345-136.437 ` +
`206.606-264.496 417.31-320.58 706.028z"/><circle fill-rule="evenodd" ` +
`clip-rule="evenodd" cx="352.891" cy="225.779" r="183.332"/></svg>`;
return 'data:image/svg+xml;base64,' + btoa(svg);
}
public createDefaultMarkerIcon(): MarkerIconInfo {
const color = this.settings.markerColor.color || '#FE7569';
return this.createColoredMarkerIcon(tinycolor(color));
}
public createColoredMarkerIcon(color: tinycolor.Instance): MarkerIconInfo {
return {
size: [21, 34],
icon: L.icon({
iconUrl: this.createColorIconURI(color),
iconSize: [21, 34],
iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]],
popupAnchor: [0, -34],
shadowUrl: 'assets/shadow.png',
shadowSize: [40, 37],
shadowAnchor: [12, 35]
})
};
}
public extractLocation(data: FormattedData<TbMapDatasource>): L.LatLng {
const position = this.extractPosition(data);
if (position) {
@ -227,7 +528,6 @@ export class TbMarkersDataLayer extends TbMapDataLayer<MarkersDataLayerSettings,
return null;
}
}
}
class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings, TbPolygonsDataLayer> {
@ -281,8 +581,8 @@ class TbPolygonDataLayerItem extends TbDataLayerItem<PolygonsDataLayerSettings,
export class TbPolygonsDataLayer extends TbMapDataLayer<PolygonsDataLayerSettings, TbPolygonsDataLayer> {
constructor(protected map: TbMap<any>,
protected settings: PolygonsDataLayerSettings) {
super(map, settings);
inputSettings: PolygonsDataLayerSettings) {
super(map, inputSettings);
}
public dataLayerType(): MapDataLayerType {
@ -294,6 +594,10 @@ export class TbPolygonsDataLayer extends TbMapDataLayer<PolygonsDataLayerSetting
return datasource;
}
protected defaultBaseSettings(): Partial<PolygonsDataLayerSettings> {
return defaultBasePolygonsDataLayerSettings;
}
protected doSetup(): Observable<void> {
return of(null);
}
@ -356,8 +660,8 @@ class TbCircleDataLayerItem extends TbDataLayerItem<CirclesDataLayerSettings, Tb
export class TbCirclesDataLayer extends TbMapDataLayer<CirclesDataLayerSettings, TbCirclesDataLayer> {
constructor(protected map: TbMap<any>,
protected settings: CirclesDataLayerSettings) {
super(map, settings);
inputSettings: CirclesDataLayerSettings) {
super(map, inputSettings);
}
public dataLayerType(): MapDataLayerType {
@ -369,6 +673,10 @@ export class TbCirclesDataLayer extends TbMapDataLayer<CirclesDataLayerSettings,
return datasource;
}
protected defaultBaseSettings(): Partial<CirclesDataLayerSettings> {
return defaultBaseCirclesDataLayerSettings;
}
protected doSetup(): Observable<void> {
return of(null);
}

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

@ -14,12 +14,16 @@
/// limitations under the License.
///
import { DataKey, Datasource, DatasourceType } from '@shared/models/widget.models';
import { DataKey, Datasource, DatasourceType, FormattedData } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { guid, isDefinedAndNotNull, isString, mergeDeep } from '@core/utils';
import { guid, hashCode, isDefinedAndNotNull, isString, mergeDeep } from '@core/utils';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { materialColors } from '@shared/models/material.models';
import L from 'leaflet';
import L, { BaseIconOptions, Icon } from 'leaflet';
import { TbFunction } from '@shared/models/js-function.models';
import { Observable, Observer, of, switchMap } from 'rxjs';
import { map } from 'rxjs/operators';
import { ImagePipe } from '@shared/pipe/image.pipe';
export enum MapType {
geoMap = 'geoMap',
@ -50,7 +54,7 @@ export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSetting
export interface MapDataLayerSettings extends MapDataSourceSettings {
additionalDataKeys?: DataKey[];
group?: string;
groups?: string[];
}
export type MapDataLayerType = 'markers' | 'polygons' | 'circles';
@ -109,9 +113,43 @@ export const mapDataLayerValidator = (type: MapDataLayerType): ValidatorFn => {
};
};
export enum MarkerType {
default = 'default',
image = 'image'
}
export enum DataLayerColorType {
constant = 'constant',
function = 'function'
}
export interface DataLayerColorSettings {
type: DataLayerColorType;
color: string;
colorFunction?: TbFunction;
}
export enum MarkerImageType {
image = 'image',
function = 'function'
}
export interface MarkerImageSettings {
type: MarkerImageType;
image?: string;
imageSize?: number;
imageFunction?: TbFunction;
images?: string[];
}
export interface MarkersDataLayerSettings extends MapDataLayerSettings {
xKey: DataKey;
yKey: DataKey;
markerType: MarkerType;
markerColor: DataLayerColorSettings;
markerImage?: MarkerImageSettings;
markerOffsetX: number;
markerOffsetY: number;
}
const defaultMarkerLatitudeFunction = 'var value = prevValue || 15.833293;\n' +
@ -138,7 +176,7 @@ const defaultMarkerYPosFunction = 'var value = prevValue || 0.3;\n' +
'}\n' +
'return value;';
export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly = false): MarkersDataLayerSettings => ({
export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly = false): MarkersDataLayerSettings => mergeDeep({
dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity,
xKey: {
name: functionsOnly ? 'f(x)' : (MapType.geoMap === mapType ? 'latitude' : 'xPos'),
@ -156,13 +194,23 @@ export const defaultMarkersDataLayerSettings = (mapType: MapType, functionsOnly
settings: {},
color: materialColors[0].value
}
});
} as MarkersDataLayerSettings, defaultBaseMarkersDataLayerSettings as MarkersDataLayerSettings);
export const defaultBaseMarkersDataLayerSettings: Partial<MarkersDataLayerSettings> = {
markerType: MarkerType.default,
markerColor: {
type: DataLayerColorType.constant,
color: '#FE7569'
},
markerOffsetX: 0.5,
markerOffsetY: 1
};
export interface PolygonsDataLayerSettings extends MapDataLayerSettings {
polygonKey: DataKey;
}
export const defaultPolygonsDataLayerSettings = (functionsOnly = false): PolygonsDataLayerSettings => ({
export const defaultPolygonsDataLayerSettings = (functionsOnly = false): PolygonsDataLayerSettings => mergeDeep({
dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity,
polygonKey: {
name: functionsOnly ? 'f(x)' : 'perimeter',
@ -171,13 +219,17 @@ export const defaultPolygonsDataLayerSettings = (functionsOnly = false): Polygon
settings: {},
color: materialColors[0].value
}
});
} as PolygonsDataLayerSettings, defaultBasePolygonsDataLayerSettings as PolygonsDataLayerSettings);
export const defaultBasePolygonsDataLayerSettings: Partial<PolygonsDataLayerSettings> = {
}
export interface CirclesDataLayerSettings extends MapDataLayerSettings {
circleKey: DataKey;
}
export const defaultCirclesDataLayerSettings = (functionsOnly = false): CirclesDataLayerSettings => ({
export const defaultCirclesDataLayerSettings = (functionsOnly = false): CirclesDataLayerSettings => mergeDeep({
dsType: functionsOnly ? DatasourceType.function : DatasourceType.entity,
circleKey: {
name: functionsOnly ? 'f(x)' : 'perimeter',
@ -186,7 +238,11 @@ export const defaultCirclesDataLayerSettings = (functionsOnly = false): CirclesD
settings: {},
color: materialColors[0].value
}
});
} as CirclesDataLayerSettings, defaultBaseCirclesDataLayerSettings as CirclesDataLayerSettings);
export const defaultBaseCirclesDataLayerSettings: Partial<CirclesDataLayerSettings> = {
}
export const defaultMapDataLayerSettings = (mapType: MapType, dataLayerType: MapDataLayerType, functionsOnly = false): MapDataLayerSettings => {
switch (dataLayerType) {
@ -542,12 +598,39 @@ export type MapSetting = GeoMapSettings & ImageMapSettings;
export const defaultMapSettings: MapSetting = defaultGeoMapSettings;
export interface MarkerImageInfo {
url: string;
size: number;
markerOffset?: [number, number];
tooltipOffset?: [number, number];
}
export interface MarkerIconInfo {
icon: Icon<BaseIconOptions>;
size: [number, number];
}
export type MapStringFunction = (data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[]) => string;
export type MarkerImageFunction = (data: FormattedData<TbMapDatasource>, markerImages: string[],
dsData: FormattedData<TbMapDatasource>[]) => MarkerImageInfo;
export interface TbCircleData {
latitude: number;
longitude: number;
radius: number;
}
export const isJSON = (data: string): boolean => {
try {
const parseData = JSON.parse(data);
return !Array.isArray(parseData);
} catch (e) {
return false;
}
}
export const isValidLatitude = (latitude: any): boolean =>
isDefinedAndNotNull(latitude) &&
!isString(latitude) &&
@ -622,9 +705,14 @@ const mapDatasourceIsSame = (ds1: TbMapDatasource, ds2: TbMapDatasource): boolea
case DatasourceType.function:
return true;
case DatasourceType.device:
return ds1.deviceId === ds2.deviceId;
case DatasourceType.entity:
return ds1.entityAliasId === ds2.entityAliasId;
if (ds1.filterId === ds2.filterId) {
if (ds1.type === DatasourceType.device) {
return ds1.deviceId === ds2.deviceId;
} else {
return ds1.entityAliasId === ds2.entityAliasId;
}
}
}
}
return false;
@ -643,3 +731,57 @@ const mergeMapDatasource = (target: TbMapDatasource, source: TbMapDatasource): T
target.dataKeys.push(...appendKeys);
return target;
}
const imageAspectMap: {[key: string]: ImageWithAspect} = {};
const imageLoader = (imageUrl: string): Observable<HTMLImageElement> => new Observable((observer: Observer<HTMLImageElement>) => {
const image = document.createElement('img'); // support IE
image.style.position = 'absolute';
image.style.left = '-99999px';
image.style.top = '-99999px';
image.onload = () => {
observer.next(image);
document.body.removeChild(image);
observer.complete();
};
image.onerror = err => {
observer.error(err);
document.body.removeChild(image);
observer.complete();
};
document.body.appendChild(image);
image.src = imageUrl;
});
const loadImageAspect = (imageUrl: string): Observable<number> =>
imageLoader(imageUrl).pipe(map(image => image.width / image.height));
export interface ImageWithAspect {
url: string;
aspect: number;
}
export const loadImageWithAspect = (imagePipe: ImagePipe, imageUrl: string): Observable<ImageWithAspect> => {
if (imageUrl?.length) {
const hash = hashCode(imageUrl);
let imageWithAspect = imageAspectMap[hash];
if (imageWithAspect) {
return of(imageWithAspect);
} else {
return imagePipe.transform(imageUrl, {asString: true, ignoreLoadingImage: true}).pipe(
switchMap((res) => {
const url = res as string;
return loadImageAspect(url).pipe(
map((aspect) => {
imageWithAspect = {url, aspect};
imageAspectMap[hash] = imageWithAspect;
return imageWithAspect;
})
);
})
);
}
} else {
return of(null);
}
};

32
ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss

@ -45,12 +45,15 @@
&.tb-layers {
background-image: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 9.5L0.75 5L9 0.5L17.25 5L9 9.5ZM9 12.5L1.18125 8.24375L2.75625 7.38125L9 10.7938L15.2438 7.38125L16.8188 8.24375L9 12.5ZM9 15.5L1.18125 11.2438L2.75625 10.3813L9 13.7938L15.2438 10.3813L16.8188 11.2438L9 15.5Z" fill="%235B5B5B"/></svg>');
}
&.tb-groups {
background-image: url('data:image/svg+xml,<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5 3A2 2 0 0 0 3 5H5M7 3V5H9V3M11 3V5H13V3M15 3V5H17V3M19 3V5H21A2 2 0 0 0 19 3M3 7V9H5V7M7 7V11H11V7M13 7V11H17V7M19 7V9H21V7M3 11V13H5V11M19 11V13H21V11M7 13V17H11V13M13 13V17H17V13M3 15V17H5V15M19 15V17H21V15M3 19A2 2 0 0 0 5 21V19M7 19V21H9V19M11 19V21H13V19M15 19V21H17V19M19 19V21A2 2 0 0 0 21 19Z" fill="%235B5B5B"/></svg>');
}
}
}
}
}
.tb-map-sidebar {
.tb-layers {
.tb-layers, .tb-groups {
height: 100%;
width: 220px;
max-width: 220px;
@ -140,6 +143,33 @@
}
}
}
.tb-group-checkbox {
width: 100%;
display: flex;
align-items: center;
gap: 0;
cursor: pointer;
.tb-group-button {
width: 20px;
height: 20px;
min-width: 20px;
min-height: 20px;
margin: 4px;
cursor: pointer;
accent-color: var(--mdc-checkbox-selected-icon-color, var(--mat-app-primary));
}
.tb-group-label {
padding-left: 12px;
font-size: 14px;
font-weight: 400;
color: rgba(0, 0, 0, 0.76);
user-select: none;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}

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

@ -49,6 +49,7 @@ import {
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { widgetType } from '@shared/models/widget.models';
import { EntityDataPageLink } from '@shared/models/query/query.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
export abstract class TbMap<S extends BaseMapSettings> {
@ -67,7 +68,6 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected map: L.Map;
protected defaultCenterPosition: [number, number];
protected bounds: L.LatLngBounds;
protected ignoreUpdateBounds = false;
protected southWest = new L.LatLng(-Projection.SphericalMercator['MAX_LATITUDE'], -180);
@ -77,6 +77,8 @@ export abstract class TbMap<S extends BaseMapSettings> {
protected mapElement: HTMLElement;
protected sidebar: L.TB.SidebarControl;
private readonly mapResize$: ResizeObserver;
protected constructor(protected ctx: WidgetContext,
@ -129,9 +131,6 @@ export abstract class TbMap<S extends BaseMapSettings> {
});
if (this.settings.useDefaultCenterPosition) {
this.map.panTo(this.defaultCenterPosition);
this.bounds = this.map.getBounds();
} else {
this.bounds = new L.LatLngBounds(null, null);
}
this.setupDataLayers();
}
@ -148,9 +147,39 @@ export abstract class TbMap<S extends BaseMapSettings> {
this.dataLayers.push(...this.settings.circles.map(settings => new TbCirclesDataLayer(this, settings)));
}
if (this.dataLayers.length) {
const groupsMap = new Map<string, L.TB.GroupData>();
const customTranslate = this.ctx.$injector.get(CustomTranslatePipe);
this.dataLayers.forEach(dl => {
dl.getGroups().forEach(group => {
let groupData = groupsMap.get(group);
if (!groupData) {
groupData = {
title: customTranslate.transform(group),
group,
enabled: true,
dataLayers: []
};
groupsMap.set(group, groupData);
}
groupData.dataLayers.push(dl);
});
});
// TODO: Groups
this.dataLayers.forEach(dl => this.map.addLayer(dl.getFeatureGroup()));
const groupDataLayers = Array.from(groupsMap.values());
if (groupDataLayers.length) {
const sidebar = this.getSidebar();
L.TB.groups({
groups: groupDataLayers,
sidebar,
position: this.settings.controlsPosition,
uiClass: 'tb-groups',
paneTitle: this.ctx.translate.instant('widgets.maps.data-layer.groups'),
buttonTitle: this.ctx.translate.instant('widgets.maps.data-layer.groups'),
}).addTo(this.map);
this.map.on('layergroupchange', () => {
this.updateBounds();
});
}
const setup = this.dataLayers.map(dl => dl.setup());
forkJoin(setup).subscribe(
@ -213,28 +242,32 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
private updateBounds() {
const bounds = new L.LatLngBounds(null, null);
this.dataLayers.forEach(dl => bounds.extend(dl.getBounds()));
const mapBounds = this.map.getBounds();
if (bounds.isValid() && (!this.bounds || !this.bounds.isValid() || !this.bounds.equals(bounds)
&& this.settings.fitMapBounds ? !mapBounds.contains(bounds) : false)) {
this.bounds = bounds;
if (!this.ignoreUpdateBounds) {
this.fitBounds(bounds);
const enabledDataLayers = this.dataLayers.filter(dl => dl.isEnabled());
const dataLayersBounds = enabledDataLayers.map(dl => dl.getBounds()).filter(b => b.isValid());
let bounds: L.LatLngBounds;
if (dataLayersBounds.length) {
bounds = new L.LatLngBounds(null, null);
dataLayersBounds.forEach(b => bounds.extend(b));
const mapBounds = this.map.getBounds();
if (bounds.isValid() && this.settings.fitMapBounds && !mapBounds.contains(bounds)) {
if (!this.ignoreUpdateBounds) {
this.fitBounds(bounds);
}
}
}
}
private fitBounds(bounds: LatLngBounds, padding?: PointExpression) {
private fitBounds(bounds: LatLngBounds) {
if (bounds.isValid()) {
this.bounds = !!this.bounds ? this.bounds.extend(bounds) : bounds;
if (!this.settings.fitMapBounds && this.settings.defaultZoomLevel) {
this.map.setZoom(this.settings.defaultZoomLevel, { animate: false });
if (this.settings.useDefaultCenterPosition) {
this.map.panTo(this.defaultCenterPosition, { animate: false });
}
else {
this.map.panTo(this.bounds.getCenter());
this.map.panTo(bounds.getCenter());
}
} else {
this.map.once('zoomend', () => {
@ -247,9 +280,9 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
});
if (this.settings.useDefaultCenterPosition) {
this.bounds = this.bounds.extend(this.defaultCenterPosition);
bounds = bounds.extend(this.defaultCenterPosition);
}
this.map.fitBounds(this.bounds, { padding: padding || [50, 50], animate: false });
this.map.fitBounds(bounds, { padding: [10, 10], animate: false });
this.map.invalidateSize();
}
}
@ -265,6 +298,25 @@ export abstract class TbMap<S extends BaseMapSettings> {
return of(null);
}
protected getSidebar(): L.TB.SidebarControl {
if (!this.sidebar) {
this.sidebar = L.TB.sidebar({
container: $(this.containerElement),
position: this.settings.controlsPosition,
paneWidth: 220
}).addTo(this.map);
}
return this.sidebar;
}
public getCtx(): WidgetContext {
return this.ctx;
}
public getMap(): L.Map {
return this.map;
}
public type(): MapType {
return this.settings.mapType;
}
@ -317,11 +369,7 @@ class TbGeoMap extends TbMap<GeoMapSettings> {
layer.layer.addTo(this.map);
this.map.attributionControl.setPrefix(layer.attributionPrefix);
if (layers.length > 1) {
const sidebar = L.TB.sidebar({
container: $(this.containerElement),
position: this.settings.controlsPosition,
paneWidth: 220
}).addTo(this.map);
const sidebar = this.getSidebar();
L.TB.layers({
layers,
sidebar,

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-line-settings.component.ts

@ -32,7 +32,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component';
import { DataKeyConfigComponent } from '@home/components/widget/lib/settings/common/key/data-key-config.component';
import {
chartLabelPositions,
chartLabelPositionTranslations,

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/advanced-range.component.ts

@ -27,7 +27,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { IAliasController } from '@core/api/widget-api.models';
import { AdvancedColorRange } from '@shared/models/widget-settings.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/chart-bar-settings.component.ts

@ -26,7 +26,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component';
import { DataKeyConfigComponent } from '@home/components/widget/lib/settings/common/key/data-key-config.component';
import {
ChartBarSettings,
ChartLabelPosition,

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component.ts

@ -44,7 +44,7 @@ import {
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKey, Datasource, DatasourceType, WidgetConfig } from '@shared/models/widget.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { deepClone } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-thresholds-panel.component.ts

@ -35,7 +35,7 @@ import {
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { mergeDeep } from '@core/utils';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { DataKey, Datasource, WidgetConfig } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { coerceBoolean } from '@shared/decorators/coercion';

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-range-list.component.ts

@ -39,7 +39,7 @@ import { deepClone, isDefinedAndNotNull, isUndefined } from '@core/utils';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { IAliasController } from '@core/api/widget-api.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
export function advancedRangeValidator(control: AbstractControl): ValidationErrors | null {

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts

@ -32,7 +32,7 @@ import { WidgetService } from '@core/http/widget.service';
import { ColorSettingsComponent } from '@home/components/widget/lib/settings/common/color-settings.component';
import { IAliasController } from '@core/api/widget-api.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

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

@ -34,7 +34,7 @@ import {
import { IAliasController } from '@core/api/widget-api.models';
import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
@Injectable()

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

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-form-field [formGroup]="selectFilterFormGroup" class="mat-block">
<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) : ''}}"
#filterInput

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

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

@ -35,8 +35,10 @@ import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { ENTER } from '@angular/cdk/keycodes';
import { ErrorStateMatcher } from '@angular/material/core';
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models';
import { FilterSelectCallbacks } from './filter-select.component.models';
import { Filter } from '@shared/models/query/query.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({
selector: 'tb-filter-select',
@ -65,8 +67,15 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte
callbacks: FilterSelectCallbacks;
@Input()
@coerceBoolean()
showLabel: boolean;
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@ViewChild('filterAutocomplete') filterAutocomplete: MatAutocomplete;
@ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger;

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

@ -33,7 +33,7 @@ import { IAliasController } from '@core/api/widget-api.models';
import { DomSanitizer } from '@angular/platform-browser';
import { coerceBoolean } from '@shared/decorators/coercion';
import { isDefinedAndNotNull } from '@core/utils';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
@Component({

0
ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html

0
ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.scss

2
ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts

@ -30,7 +30,7 @@ import {
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { DataKey, DataKeyConfigMode, Widget, widgetType } from '@shared/models/widget.models';
import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component';
import { DataKeyConfigComponent } from './data-key-config.component';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { ToggleHeaderOption } from '@shared/components/toggle-header.component';

0
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html

2
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts

@ -42,7 +42,7 @@ import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeySettingsFunction } from './data-keys.component.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable, of } from 'rxjs';
import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators';

13
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.html

@ -15,8 +15,12 @@
limitations under the License.
-->
<mat-form-field class="tb-inline-field tb-key-field" subscriptSizing="dynamic"
[class.tb-suffix-absolute]="!keysFormControl.value?.length">
<mat-form-field class="tb-key-field" subscriptSizing="dynamic"
[class]="{'tb-inline-field': inlineField,
'tb-suffix-absolute': (inlineField && !keysFormControl.value?.length)}"
[appearance]="inlineField ? 'fill' : appearance"
[subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing">
<mat-label *ngIf="!inlineField && label">{{ label }}</mat-label>
<mat-chip-grid #chipList [formControl]="keysFormControl">
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue?.type"
[removable]="removable"
@ -81,7 +85,7 @@
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="requiredText | translate"
*ngIf="requiredText && keysFormControl.hasError('required')
*ngIf="inlineField && requiredText && keysFormControl.hasError('required')
&& keysFormControl.touched"
class="tb-error">
warning
@ -145,6 +149,9 @@
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="!inlineField && requiredText && keysFormControl.hasError('required')">
{{ requiredText | translate }}
</mat-error>
</mat-form-field>
<ng-template #keyName>
<ng-container *ngIf="dataKeyHasPostprocessing(); else keyName">

27
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.scss

@ -14,7 +14,7 @@
* limitations under the License.
*/
.tb-data-key-input {
.mat-mdc-form-field.tb-inline-field.tb-key-field {
.mat-mdc-form-field.tb-key-field {
width: 100%;
&.mat-form-field-appearance-fill {
.mdc-text-field--filled.mdc-text-field--disabled {
@ -23,14 +23,25 @@
}
}
}
.mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) {
padding-left: 8px;
padding-right: 0;
.mat-mdc-text-field-wrapper {
&:not(.mdc-text-field--outlined) {
padding-left: 8px;
padding-right: 0;
.mat-mdc-form-field-infix {
padding-top: 0;
padding-bottom: 6px;
}
}
&.mdc-text-field--outlined {
.mat-mdc-form-field-infix {
padding-top: 12px;
padding-bottom: 12px;
}
.mdc-evolution-chip-set__chips {
margin-left: 0;
}
}
.mat-mdc-form-field-infix {
padding-top: 0;
padding-bottom: 6px;
.mdc-evolution-chip-set .mdc-evolution-chip {
margin: 0;
}

18
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/data-key-input.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-input.component.ts

@ -50,13 +50,14 @@ import { UtilsService } from '@core/services/utils.service';
import { alarmFields } from '@shared/models/alarm.models';
import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators';
import { AggregationType } from '@shared/models/time/time.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks } from './data-keys.component.models';
import { IAliasController } from '@core/api/widget-api.models';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({
selector: 'tb-data-key-input',
templateUrl: './data-key-input.component.html',
styleUrls: ['./data-key-input.component.scss', '../../../config/data-keys.component.scss'],
styleUrls: ['./data-key-input.component.scss', './data-keys.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -83,6 +84,19 @@ export class DataKeyInputComponent implements ControlValueAccessor, OnInit, OnCh
@Input()
disabled: boolean;
@Input()
label: string;
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
@coerceBoolean()
inlineField = true;
@Input()
@coerceBoolean()
required = false;

8
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.html

@ -15,8 +15,10 @@
limitations under the License.
-->
<mat-form-field [formGroup]="keysListFormGroup" class="mat-block">
<mat-label>{{placeholder}}</mat-label>
<mat-form-field [formGroup]="keysListFormGroup" class="mat-block tb-keys-field"
[appearance]="appearance"
[subscriptSizing]="subscriptSizing">
<mat-label>{{ label ? label : placeholder}}</mat-label>
<mat-chip-grid #chipList formControlName="keys" [required]="required">
<div class="tb-datakeys-container"
[dndDropzone]="[dndId]"
@ -46,7 +48,7 @@
<div dndHandle *ngIf="!disabled && !dragDisabled" class="tb-chip-drag-handle">
<mat-icon class="tb-mat-18">drag_indicator</mat-icon>
</div>
<div class="tb-chip-labels">
<div class="tb-chip-labels" [class.tb-transparent]="dragDisabled">
<div class="tb-chip-label tb-chip-text tb-chip-icon">
<ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"

0
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.models.ts

31
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.scss

@ -13,6 +13,34 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-data-keys {
.mat-mdc-form-field.tb-keys-field {
width: 100%;
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined {
.mat-mdc-form-field-infix {
padding-top: 12px;
padding-bottom: 12px;
.mdc-evolution-chip-set .mdc-evolution-chip {
margin: 0;
}
.mdc-evolution-chip-set__chips {
margin-left: 0;
}
input.mat-mdc-chip-input {
height: 32px;
margin-left: 0;
}
}
.tb-datakeys-container {
gap: 8px;
}
}
}
}
}
.tb-datakeys-container {
display: flex;
flex-wrap: wrap;
@ -60,6 +88,9 @@
background: rgba(0, 0, 0, 0.04);
border-radius: 100px;
padding: 2px 10px;
&.tb-transparent {
background: transparent;
}
.tb-chip-label {
font-weight: normal;
font-size: 14px;

62
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-keys.component.ts

@ -19,7 +19,7 @@ import {
Component,
DestroyRef,
ElementRef,
forwardRef,
forwardRef, HostBinding,
Input,
OnChanges,
OnInit,
@ -60,19 +60,19 @@ import { MatDialog } from '@angular/material/dialog';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
} from '@home/components/widget/config/data-key-config-dialog.component';
} from './data-key-config-dialog.component';
import { deepClone, guid, isDefinedAndNotNull, isObject, isUndefined } from '@core/utils';
import { Dashboard } from '@shared/models/dashboard.models';
import { AggregationType } from '@shared/models/time/time.models';
import { DndDropEvent } from 'ngx-drag-drop/lib/dnd-dropzone.directive';
import { moveItemInArray } from '@angular/cdk/drag-drop';
import { coerceBoolean } from '@shared/decorators/coercion';
import { DatasourceComponent } from '@home/components/widget/config/datasource.component';
import { ColorPickerPanelComponent } from '@shared/components/color-picker/color-picker-panel.component';
import { TbPopoverService } from '@shared/components/popover.service';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormProperty } from '@shared/models/dynamic-form.models';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({
selector: 'tb-data-keys',
@ -98,21 +98,37 @@ import { FormProperty } from '@shared/models/dynamic-form.models';
})
export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChanges, ErrorStateMatcher, Validator {
public get hideDataKeyLabel(): boolean {
return this.datasourceComponent.hideDataKeyLabel;
}
@HostBinding('class')
hostClass = 'tb-data-keys';
public get hideDataKeyColor(): boolean {
return this.datasourceComponent.hideDataKeyColor;
}
@Input()
label: string;
public get hideDataKeyUnits(): boolean {
return this.datasourceComponent.hideDataKeyUnits;
}
@Input()
appearance: MatFormFieldAppearance = 'fill';
public get hideDataKeyDecimals(): boolean {
return this.datasourceComponent.hideDataKeyDecimals;
}
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
@coerceBoolean()
hideDataKeyLabel: boolean;
@Input()
@coerceBoolean()
hideDataKeyColor: boolean;
@Input()
@coerceBoolean()
hideDataKeyUnits: boolean;
@Input()
@coerceBoolean()
hideDataKeyDecimals: boolean;
@Input()
@coerceBoolean()
disableDrag = false;
widgetTypes = widgetType;
dataKeyTypes = DataKeyType;
@ -138,6 +154,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
}
@Input()
@coerceBoolean()
optDataKeys: boolean;
@Input()
@ -175,6 +192,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
@Input()
deviceId: string;
@Input()
generateKey: (key: DataKey) => DataKey = (key) => key;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue || !this.optDataKeys || this.isCountDatasource;
@ -222,7 +242,6 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
private keysValidator = this._keysValidator.bind(this);
constructor(@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private datasourceComponent: DatasourceComponent,
private translate: TranslateService,
private utils: UtilsService,
private dialog: MatDialog,
@ -463,8 +482,13 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
}
private addFromChipValue(chip: DataKey) {
const key = this.callbacks.generateDataKey(chip.name, chip.type, this.dataKeySettingsForm, this.latestDataKeys,
this.datakeySettingsFunction);
let key: DataKey;
if (this.generateKey) {
key = this.generateKey(chip);
} else {
key = this.callbacks.generateDataKey(chip.name, chip.type, this.dataKeySettingsForm, this.latestDataKeys,
this.datakeySettingsFunction);
}
this.addKey(key);
}
@ -687,7 +711,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
}
get dragDisabled(): boolean {
return this.keys.length < 2;
return this.keys.length < 2 || this.disableDrag;
}
get maxDataKeysSet(): boolean {

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

@ -0,0 +1,198 @@
<!--
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.
-->
<form (ngSubmit)="save()" class="tb-map-data-layer-dialog" style="width: 750px;">
<mat-toolbar color="primary">
<h2 translate>{{dialogTitle}}</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content class="tb-form-panel no-border no-padding" [formGroup]="dataLayerFormGroup">
<div class="tb-form-panel">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'widget-config.datasource' | translate }}</div>
<tb-toggle-select formControlName="dsType">
<tb-toggle-option *ngFor="let type of datasourceTypes" [value]="type">{{ datasourceTypesTranslations.get(type) | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="flex flex-col">
<tb-entity-autocomplete
*ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.device"
required
appearance="outline"
placeholder="{{ 'device.select-device' | translate }}"
[entityType]="EntityType.DEVICE"
formControlName="dsDeviceId">
</tb-entity-autocomplete>
<tb-entity-alias-select
*ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.entity"
tbRequired
appearance="outline"
[aliasController]="context.aliasController"
formControlName="dsEntityAliasId"
[callbacks]="context.callbacks">
</tb-entity-alias-select>
<tb-filter-select
*ngIf="[DatasourceType.device, DatasourceType.entity].includes(dataLayerFormGroup.get('dsType').value)"
showLabel
appearance="outline"
[aliasController]="context.aliasController"
formControlName="dsFilterId"
[callbacks]="context.callbacks">
</tb-filter-select>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'datakey.keys' | translate }}</div>
<div class="flex flex-col">
<div *ngIf="dataLayerType === 'markers'" class="flex flex-row gap-3">
<tb-data-key-input
class="flex-1"
inlineField="false"
appearance="outline"
subscriptSizing="fixed"
label="{{ (mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.latitude-key' : 'widgets.maps.data-layer.marker.x-pos-key') | translate }}"
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]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('xKey')"
formControlName="xKey">
</tb-data-key-input>
<tb-data-key-input
class="flex-1"
inlineField="false"
appearance="outline"
subscriptSizing="fixed"
label="{{ (mapType === MapType.geoMap ? 'widgets.maps.data-layer.marker.longitude-key' : 'widgets.maps.data-layer.marker.y-pos-key') | translate }}"
*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]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('yKey')"
formControlName="yKey">
</tb-data-key-input>
</div>
<tb-data-key-input
*ngIf="dataLayerType === 'polygons'"
inlineField="false"
appearance="outline"
subscriptSizing="fixed"
label="{{ 'widgets.maps.data-layer.polygon.polygon-key' | translate }}"
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]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('polygonKey')"
formControlName="polygonKey">
</tb-data-key-input>
<tb-data-key-input
*ngIf="dataLayerType === 'circles'"
inlineField="false"
appearance="outline"
subscriptSizing="fixed"
label="{{ 'widgets.maps.data-layer.circle.circle-key' | translate }}"
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]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('circleKey')"
formControlName="circleKey">
</tb-data-key-input>
<tb-data-keys
formControlName="additionalDataKeys"
appearance="outline"
subscriptSizing="dynamic"
hideDataKeyColor
hideDataKeyDecimals
hideDataKeyUnits
optDataKeys
disableDrag
label="{{ 'widgets.maps.data-layer.additional-data-keys' | translate }}"
[required]="false"
[widgetType]="widgetType.latest"
[widget]="context.widget"
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="context.aliasController"
[callbacks]="context.callbacks"
[generateKey]="generateAdditionalDataKey">
</tb-data-keys>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'widget-config.appearance' | translate }}</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'widgets.maps.data-layer.groups' | translate }}</div>
<tb-string-items-list class="tb-inline-chips"
editable
placeholder="{{'widgets.maps.data-layer.groups' | translate}}"
appearance="outline"
subscriptSizing="dynamic"
formControlName="groups">
</tb-string-items-list>
</div>
</div>
<div mat-dialog-actions class="flex items-center justify-between">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || dataLayerFormGroup.invalid || !dataLayerFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,41 @@
/**
* 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-layer-dialog {
--mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12);
--mat-form-field-trailing-icon-color: rgba(0,0,0,0.38);
.tb-inline-chips {
--mat-form-field-container-vertical-padding: 12px;
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined {
padding-left: 16px;
padding-right: 16px;
.mat-mdc-form-field-infix {
.mdc-evolution-chip-set .mdc-evolution-chip {
margin: 0;
}
.mdc-evolution-chip-set__chips {
gap: 8px;
margin-left: 0;
}
input.mat-mdc-chip-input {
height: 32px;
margin-left: 0;
}
}
}
}
}
}

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

@ -0,0 +1,239 @@
///
/// 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, Inject, ViewEncapsulation } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import {
CirclesDataLayerSettings,
MapDataLayerSettings,
MapDataLayerType,
MapType,
MarkersDataLayerSettings,
PolygonsDataLayerSettings
} from '@home/components/widget/lib/maps/map.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { DataKey, DatasourceType, datasourceTypeTranslationMap, widgetType } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityType } from '@shared/models/entity-type.models';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
import { genNextLabelForDataKeys } from '@core/utils';
export interface MapDataLayerDialogData {
settings: MapDataLayerSettings;
mapType: MapType;
dataLayerType: MapDataLayerType;
context: MapSettingsContext;
}
@Component({
selector: 'tb-map-data-layer-dialog',
templateUrl: './map-data-layer-dialog.component.html',
styleUrls: ['./map-data-layer-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class MapDataLayerDialogComponent extends DialogComponent<MapDataLayerDialogComponent, MapDataLayerSettings> {
DatasourceType = DatasourceType;
EntityType = EntityType;
MapType = MapType;
DataKeyType = DataKeyType;
widgetType = widgetType;
datasourceTypes: Array<DatasourceType> = [];
datasourceTypesTranslations = datasourceTypeTranslationMap;
dataLayerFormGroup: UntypedFormGroup;
settings = this.data.settings;
mapType = this.data.mapType;
dataLayerType = this.data.dataLayerType;
context = this.data.context;
generateAdditionalDataKey = this.generateDataKey.bind(this);
dialogTitle: string;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MapDataLayerDialogData,
public dialogRef: MatDialogRef<MapDataLayerDialogComponent, MapDataLayerSettings>,
private fb: FormBuilder,
private destroyRef: DestroyRef) {
super(store, router, dialogRef);
if (this.context.functionsOnly) {
this.datasourceTypes = [DatasourceType.function];
} else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity];
}
this.dataLayerFormGroup = this.fb.group({
dsType: [this.settings.dsType, [Validators.required]],
dsDeviceId: [this.settings.dsDeviceId, [Validators.required]],
dsEntityAliasId: [this.settings.dsEntityAliasId, [Validators.required]],
dsFilterId: [this.settings.dsFilterId, []],
additionalDataKeys: [this.settings.additionalDataKeys, []],
groups: [this.settings.groups, []]
});
switch (this.dataLayerType) {
case 'markers':
const markersDataLayer = this.settings as MarkersDataLayerSettings;
this.dialogTitle = 'widgets.maps.data-layer.marker.marker-configuration';
this.dataLayerFormGroup.addControl('xKey', this.fb.control(markersDataLayer.xKey, Validators.required));
this.dataLayerFormGroup.addControl('yKey', this.fb.control(markersDataLayer.yKey, Validators.required));
break;
case 'polygons':
const polygonsDataLayer = this.settings as PolygonsDataLayerSettings;
this.dialogTitle = 'widgets.maps.data-layer.polygon.polygon-configuration';
this.dataLayerFormGroup.addControl('polygonKey', this.fb.control(polygonsDataLayer.polygonKey, Validators.required));
break;
case 'circles':
const circlesDataLayer = this.settings as CirclesDataLayerSettings;
this.dialogTitle = 'widgets.maps.data-layer.circle.circle-configuration';
this.dataLayerFormGroup.addControl('circleKey', this.fb.control(circlesDataLayer.circleKey, Validators.required));
break;
}
this.dataLayerFormGroup.get('dsType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
(newDsType: DatasourceType) => this.onDsTypeChanged(newDsType)
);
this.updateValidators();
}
private onDsTypeChanged(newDsType: DatasourceType) {
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});
}
const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value;
if (this.updateDataKeyToNewDsType(yKey, newDsType)) {
this.dataLayerFormGroup.get('yKey').patchValue(yKey, {emitEvent: false});
}
break;
case 'polygons':
const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value;
if (this.updateDataKeyToNewDsType(polygonKey, newDsType)) {
this.dataLayerFormGroup.get('polygonKey').patchValue(polygonKey, {emitEvent: false});
}
break;
case 'circles':
const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value;
if (this.updateDataKeyToNewDsType(circleKey, newDsType)) {
this.dataLayerFormGroup.get('circleKey').patchValue(circleKey, {emitEvent: false});
}
break;
}
this.updateValidators();
}
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});
}
}
editKey(keyType: 'xKey' | 'yKey' | 'polygonKey' | 'circleKey') {
const targetDataKey: DataKey = this.dataLayerFormGroup.get(keyType).value;
this.context.editKey(targetDataKey,
this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value).subscribe(
(updatedDataKey) => {
if (updatedDataKey) {
this.dataLayerFormGroup.get(keyType).patchValue(updatedDataKey);
}
}
);
}
private generateDataKey(key: DataKey): DataKey {
const dataKey = this.context.callbacks.generateDataKey(key.name, key.type, null, false, null);
const dataKeys: DataKey[] = [];
switch (this.dataLayerType) {
case 'markers':
const xKey: DataKey = this.dataLayerFormGroup.get('xKey').value;
if (xKey) {
dataKeys.push(xKey);
}
const yKey: DataKey = this.dataLayerFormGroup.get('yKey').value;
if (yKey) {
dataKeys.push(yKey);
}
break;
case 'polygons':
const polygonKey: DataKey = this.dataLayerFormGroup.get('polygonKey').value;
if (polygonKey) {
dataKeys.push(polygonKey);
}
break;
case 'circles':
const circleKey: DataKey = this.dataLayerFormGroup.get('circleKey').value;
if (circleKey) {
dataKeys.push(circleKey);
}
break;
}
const additionalKeys: DataKey[] = this.dataLayerFormGroup.get('additionalDataKeys').value;
if (additionalKeys) {
dataKeys.push(...additionalKeys);
}
dataKey.label = genNextLabelForDataKeys(dataKey.label, dataKeys);
return dataKey;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
const settings: MapDataLayerSettings = this.dataLayerFormGroup.getRawValue();
this.dialogRef.close(settings);
}
}

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

@ -38,9 +38,9 @@
*ngIf="dataLayerFormGroup.get('dsType').value === DatasourceType.entity"
inlineField
tbRequired
[aliasController]="aliasController"
[aliasController]="context.aliasController"
formControlName="dsEntityAliasId"
[callbacks]="entityAliasSelectCallbacks">
[callbacks]="context.callbacks">
</tb-entity-alias-select>
</div>
<tb-data-key-input
@ -51,11 +51,12 @@
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="aliasController"
[dataKeyType]="functionsOnly ? DataKeyType.function : null"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="dataKeyCallbacks"
[generateKey]="generateDataKey"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('xKey')"
formControlName="xKey">
</tb-data-key-input>
@ -67,11 +68,12 @@
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="aliasController"
[dataKeyType]="functionsOnly ? DataKeyType.function : null"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="dataKeyCallbacks"
[generateKey]="generateDataKey"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('yKey')"
formControlName="yKey">
</tb-data-key-input>
@ -83,11 +85,12 @@
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="aliasController"
[dataKeyType]="functionsOnly ? DataKeyType.function : null"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="dataKeyCallbacks"
[generateKey]="generateDataKey"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('polygonKey')"
formControlName="polygonKey">
</tb-data-key-input>
@ -99,11 +102,12 @@
[datasourceType]="dataLayerFormGroup.get('dsType').value"
[entityAliasId]="dataLayerFormGroup.get('dsEntityAliasId').value"
[deviceId]="dataLayerFormGroup.get('dsDeviceId').value"
[aliasController]="aliasController"
[dataKeyType]="functionsOnly ? DataKeyType.function : null"
[aliasController]="context.aliasController"
[widgetType]="widgetType.latest"
[dataKeyType]="context.functionsOnly ? DataKeyType.function : null"
[dataKeyTypes]="[DataKeyType.attribute, DataKeyType.timeseries]"
[callbacks]="dataKeyCallbacks"
[generateKey]="generateDataKey"
[callbacks]="context.callbacks"
[generateKey]="context.generateDataKey"
(keyEdit)="editKey('circleKey')"
formControlName="circleKey">
</tb-data-key-input>

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

@ -23,8 +23,6 @@ import {
Input,
OnInit,
Output,
Renderer2,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import {
@ -35,8 +33,6 @@ import {
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,
@ -46,27 +42,17 @@ import {
MarkersDataLayerSettings,
PolygonsDataLayerSettings
} from '@home/components/widget/lib/maps/map.models';
import {
DataKey,
DataKeyConfigMode,
DatasourceType,
datasourceTypeTranslationMap,
widgetType
} from '@shared/models/widget.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 { 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';
MapDataLayerDialogComponent,
MapDataLayerDialogData
} from '@home/components/widget/lib/settings/common/map/map-data-layer-dialog.component';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
@Component({
selector: 'tb-map-data-layer-row',
@ -90,6 +76,8 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
MapType = MapType;
widgetType = widgetType;
datasourceTypes: Array<DatasourceType> = [];
datasourceTypesTranslations = datasourceTypeTranslationMap;
@ -102,27 +90,12 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
@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;
}
@Input()
context: MapSettingsContext;
@Output()
dataLayerRemoved = new EventEmitter();
generateDataKey = this._generateDataKey.bind(this);
dataLayerFormGroup: UntypedFormGroup;
modelValue: MapDataLayerSettings;
@ -136,16 +109,12 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
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) {
if (this.context.functionsOnly) {
this.datasourceTypes = [DatasourceType.function];
} else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity];
@ -244,66 +213,37 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
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
this.context.editKey(targetDataKey,
this.dataLayerFormGroup.get('dsDeviceId').value, this.dataLayerFormGroup.get('dsEntityAliasId').value).subscribe(
(updatedDataKey) => {
if (updatedDataKey) {
this.dataLayerFormGroup.get(keyType).patchValue(updatedDataKey);
}
}).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.dialog.open<MapDataLayerDialogComponent, MapDataLayerDialogData,
MapDataLayerSettings>(MapDataLayerDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
settings: deepClone(this.modelValue),
mapType: this.mapType,
dataLayerType: this.dataLayerType,
context: this.context
}
}).afterClosed().subscribe((settings) => {
if (settings) {
this.modelValue = settings;
this.dataLayerFormGroup.patchValue(settings);
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) {
@ -375,6 +315,4 @@ export class MapDataLayerRowComponent implements ControlValueAccessor, OnInit {
this.modelValue = {...this.modelValue, ...this.dataLayerFormGroup.value};
this.propagateChange(this.modelValue);
}
protected readonly datasourceType = DatasourceType;
}

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

@ -34,6 +34,7 @@
<tb-map-data-layer-row class="flex-1"
[mapType]="mapType"
[dataLayerType]="dataLayerType"
[context]="context"
[formControl]="dataLayerControl"
(dataLayerRemoved)="removeDataLayer($index)">
</tb-map-data-layer-row>

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

@ -37,6 +37,7 @@ import {
MapType
} from '@home/components/widget/lib/maps/map.models';
import { MapSettingsComponent } from '@home/components/widget/lib/settings/common/map/map-settings.component';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
@Component({
selector: 'tb-map-data-layers',
@ -69,9 +70,8 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val
@Input()
dataLayerType: MapDataLayerType = 'markers';
get functionsOnly(): boolean {
return this.mapSettingsComponent.functionsOnly;
}
@Input()
context: MapSettingsContext;
dataLayersFormGroup: UntypedFormGroup;
@ -161,7 +161,7 @@ export class MapDataLayersComponent implements ControlValueAccessor, OnInit, Val
addDataLayer() {
const dataLayer = mergeDeep<MapDataLayerSettings>({} as MapDataLayerSettings,
defaultMapDataLayerSettings(this.mapType, this.dataLayerType, this.functionsOnly));
defaultMapDataLayerSettings(this.mapType, this.dataLayerType, this.context.functionsOnly));
const dataLayersArray = this.dataLayersFormGroup.get('dataLayers') as UntypedFormArray;
const dataLayerControl = this.fb.control(dataLayer, [mapDataLayerValidator(this.dataLayerType)]);
dataLayersArray.push(dataLayerControl);

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

@ -44,14 +44,17 @@
<tb-map-data-layers [class.!hidden]="dataLayerMode !== 'markers'"
formControlName="markers"
dataLayerType="markers"
[context]="context"
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
<tb-map-data-layers [class.!hidden]="dataLayerMode !== 'polygons'"
formControlName="polygons"
dataLayerType="polygons"
[context]="context"
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
<tb-map-data-layers [class.!hidden]="dataLayerMode !== 'circles'"
formControlName="circles"
dataLayerType="circles"
[context]="context"
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
</div>
</ng-container>

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

@ -0,0 +1,29 @@
///
/// 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 { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { DataKey, Widget } from '@shared/models/widget.models';
import { Observable } from 'rxjs';
export interface MapSettingsContext {
functionsOnly: boolean;
aliasController: IAliasController;
callbacks: WidgetConfigCallbacks;
widget: Widget;
editKey: (key: DataKey, deviceId: string, entityAliasId: string) => Observable<DataKey>;
generateDataKey: (key: DataKey) => DataKey;
}

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

@ -19,17 +19,25 @@ import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder, UntypedFormControl,
UntypedFormGroup, Validator
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator
} from '@angular/forms';
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 { merge, Observable } 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';
import { DataKey, DataKeyConfigMode, Widget, widgetType } from '@shared/models/widget.models';
import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component';
import { deepClone } from '@core/utils';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'tb-map-settings',
@ -68,6 +76,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
@Input()
widget: Widget;
context: MapSettingsContext;
private modelValue: MapSetting;
private propagateChange = null;
@ -77,10 +87,21 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
dataLayerMode: MapDataLayerType = 'markers';
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.context = {
functionsOnly: this.functionsOnly,
aliasController: this.aliasController,
callbacks: this.callbacks,
widget: this.widget,
editKey: this.editKey.bind(this),
generateDataKey: this.generateDataKey.bind(this)
};
this.mapSettingsFormGroup = this.fb.group({
mapType: [null, []],
layers: [null, []],
@ -176,4 +197,33 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
this.modelValue = this.mapSettingsFormGroup.getRawValue();
this.propagateChange(this.modelValue);
}
private editKey(key: DataKey, deviceId: string, entityAliasId: string): Observable<DataKey> {
return this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataKey: deepClone(key),
dataKeyConfigMode: DataKeyConfigMode.general,
aliasController: this.aliasController,
widgetType: widgetType.latest,
deviceId,
entityAliasId,
showPostProcessing: true,
callbacks: this.callbacks,
hideDataKeyColor: true,
hideDataKeyDecimals: true,
hideDataKeyUnits: true,
widget: this.widget,
dashboard: null,
dataKeySettingsForm: null,
dataKeySettingsDirective: null
}
}).afterClosed();
}
private generateDataKey(key: DataKey): DataKey {
return this.callbacks.generateDataKey(key.name, key.type, null, false, null);
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source-data-key.component.ts

@ -29,7 +29,7 @@ import { AppState } from '@core/core.state';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKey, Datasource, DatasourceType } from '@app/shared/models/widget.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import {
ValueSourceConfig,
ValueSourceType,

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

@ -102,7 +102,7 @@ import {
import {
TimeSeriesChartThresholdRowComponent
} from '@home/components/widget/lib/settings/common/chart/time-series-chart-threshold-row.component';
import { DataKeyInputComponent } from '@home/components/widget/lib/settings/common/data-key-input.component';
import { DataKeyInputComponent } from '@home/components/widget/lib/settings/common/key/data-key-input.component';
import { EntityAliasInputComponent } from '@home/components/widget/lib/settings/common/entity-alias-input.component';
import {
TimeSeriesChartThresholdSettingsPanelComponent
@ -191,10 +191,19 @@ import {
} 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';
import {
MapDataLayerDialogComponent
} from '@home/components/widget/lib/settings/common/map/map-data-layer-dialog.component';
import { FilterSelectComponent } from '@home/components/widget/lib/settings/common/filter/filter-select.component';
import { DataKeysComponent } from '@home/components/widget/lib/settings/common/key/data-keys.component';
import {
DataKeyConfigDialogComponent
} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component';
import { DataKeyConfigComponent } from '@home/components/widget/lib/settings/common/key/data-key-config.component';
import { WidgetSettingsComponent } from '@home/components/widget/lib/settings/common/widget/widget-settings.component';
@NgModule({
declarations: [
@ -268,10 +277,16 @@ import {
MapLayerSettingsPanelComponent,
MapLayerRowComponent,
MapLayersComponent,
MapDataLayerDialogComponent,
MapDataLayerRowComponent,
MapDataLayersComponent,
MapSettingsComponent,
EntityAliasSelectComponent
EntityAliasSelectComponent,
FilterSelectComponent,
DataKeysComponent,
DataKeyConfigDialogComponent,
DataKeyConfigComponent,
WidgetSettingsComponent
],
imports: [
CommonModule,
@ -347,7 +362,12 @@ import {
DynamicFormComponent,
DynamicFormArrayComponent,
MapSettingsComponent,
EntityAliasSelectComponent
EntityAliasSelectComponent,
FilterSelectComponent,
DataKeysComponent,
DataKeyConfigDialogComponent,
DataKeyConfigComponent,
WidgetSettingsComponent
],
providers: [
ColorSettingsComponentService,

0
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.html

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

0
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget/widget-settings.component.ts

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.html

@ -15,11 +15,10 @@
limitations under the License.
-->
<mat-form-field class="flex-1" [appearance]="inlineField ? 'outline': 'fill'"
<mat-form-field class="mat-block flex-1" [appearance]="inlineField ? 'outline': 'fill'"
[subscriptSizing]="inlineField ? 'dynamic' : 'fixed'"
[class]="{'tb-inline-field': inlineField, 'flex': inlineField,
'tb-suffix-absolute': (inlineField && !deviceKeyFormGroup.get('key').value)}"
class="mat-block"
[formGroup]="deviceKeyFormGroup">
<mat-label *ngIf="!inlineField">{{ (keyType === dataKeyType.attribute
? attributeLabel

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/tick-value.component.ts

@ -22,7 +22,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { Datasource } from '@shared/models/widget.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

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

@ -81,7 +81,7 @@ import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { coerceBoolean } from '@shared/decorators/coercion';
import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module';
import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { defaultFormProperties, FormProperty } from '@shared/models/dynamic-form.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import Timeout = NodeJS.Timeout;

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

@ -102,7 +102,7 @@ import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-
import { SharedTelemetrySubscriber, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models';
import { UserId } from '@shared/models/id/user-id';
import { UserSettingsService } from '@core/http/user-settings.service';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { UtilsService } from '@core/services/utils.service';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { FormProperty } from '@shared/models/dynamic-form.models';

2
ui-ngx/src/app/shared/components/string-items-list.component.html

@ -16,7 +16,7 @@
-->
<mat-form-field [class]="fieldClass"
[formGroup]="stringItemsForm" class="mat-block flex-1"
[formGroup]="stringItemsForm" class="tb-string-items-list mat-block flex-1"
[floatLabel]="floatLabel"
[appearance]="appearance"
[subscriptSizing]="subscriptSizing">

22
ui-ngx/src/app/shared/components/string-items-list.component.scss

@ -0,0 +1,22 @@
/**
* 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-string-items-list {
.mat-mdc-standard-chip {
.mdc-evolution-chip__cell--primary, .mat-mdc-chip-action-label {
overflow: hidden;
}
}
}

7
ui-ngx/src/app/shared/components/string-items-list.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
@ -38,14 +38,15 @@ export interface StringItemsOption {
@Component({
selector: 'tb-string-items-list',
templateUrl: './string-items-list.component.html',
styleUrls: [],
styleUrls: ['./string-items-list.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StringItemsListComponent),
multi: true
}
]
],
encapsulation: ViewEncapsulation.None
})
export class StringItemsListComponent implements ControlValueAccessor, OnInit {

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

@ -42,7 +42,7 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models'
import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/lib/settings/common/key/data-keys.component.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { TbFunction } from '@shared/models/js-function.models';
import { FormProperty, jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models';

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

@ -1349,6 +1349,7 @@
"general": "General",
"advanced": "Advanced",
"key": "Key",
"keys": "Keys",
"label": "Label",
"color": "Color",
"units": "Special symbol to show next to value",
@ -6869,6 +6870,8 @@
},
"data-layer": {
"source": "Source",
"additional-data-keys": "Additional data keys",
"groups": "Groups",
"marker": {
"latitude-key": "Latitude key",
"longitude-key": "Longitude key",

23
ui-ngx/src/typings/leaflet-extend-tb.d.ts

@ -39,7 +39,7 @@ declare module 'leaflet' {
class SidebarControl extends Control<SidebarControlOptions> {
constructor(options: SidebarControlOptions);
addPane(pane: JQuery<HTMLElement>): this;
addPane(pane: JQuery<HTMLElement>, button: JQuery<HTMLElement>): this;
togglePane(pane: JQuery<HTMLElement>, button: JQuery<HTMLElement>): void;
}
@ -70,12 +70,33 @@ declare module 'leaflet' {
constructor(options: LayersControlOptions);
}
interface DataLayer {
toggleGroup(group: string): boolean;
}
interface GroupData {
title: string;
group: string;
enabled: boolean;
dataLayers: DataLayer[];
}
interface GroupsControlOptions extends SidebarPaneControlOptions {
groups: GroupData[];
}
class GroupsControl extends SidebarPaneControl<GroupsControlOptions> {
constructor(options: GroupsControlOptions);
}
function sidebar(options: SidebarControlOptions): SidebarControl;
function sidebarPane<O extends SidebarPaneControlOptions>(options: O): SidebarPaneControl<O>;
function layers(options: LayersControlOptions): LayersControl;
function groups(options: GroupsControlOptions): GroupsControl;
namespace TileLayer {
interface ChinaProvidersData {

Loading…
Cancel
Save