Browse Source

Merge pull request #8708 from thingsboard/feature/widget-config

Widget configuration improvement + basic widget configuration mode.
pull/8721/head
Igor Kulikov 3 years ago
committed by GitHub
parent
commit
a033fd6aad
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 22
      application/src/main/data/json/system/widget_bundles/charts.json
  3. 28
      ui-ngx/src/app/core/api/alias-controller.ts
  4. 85
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  5. 20
      ui-ngx/src/app/modules/common/modules-map.ts
  6. 7
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html
  7. 17
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts
  8. 55
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  9. 44
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  10. 27
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  11. 12
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  12. 36
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html
  13. 26
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss
  14. 58
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  15. 7
      ui-ngx/src/app/modules/home/components/filter/filter-select.component.html
  16. 13
      ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts
  17. 44
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  18. 52
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html
  19. 71
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts
  20. 8
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html
  21. 12
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss
  22. 21
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts
  23. 20
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-config.scss
  24. 49
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  25. 86
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html
  26. 110
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts
  27. 36
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html
  28. 133
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts
  29. 5
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html
  30. 0
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.scss
  31. 7
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts
  32. 8
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  33. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss
  34. 30
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  35. 67
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html
  36. 1
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts
  37. 28
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss
  38. 193
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts
  39. 103
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  40. 7
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.models.ts
  41. 33
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.scss
  42. 275
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  43. 101
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.html
  44. 83
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.scss
  45. 329
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  46. 42
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html
  47. 115
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts
  48. 70
      ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
  49. 115
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  50. 169
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss
  51. 0
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html
  52. 0
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.scss
  53. 25
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts
  54. 20
      ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html
  55. 67
      ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts
  56. 32
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.html
  57. 151
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.ts
  58. 17
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  59. 4
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  60. 3
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts
  61. 0
      ui-ngx/src/app/modules/home/components/widget/lib/legend.component.html
  62. 0
      ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss
  63. 0
      ui-ngx/src/app/modules/home/components/widget/lib/legend.component.ts
  64. 29
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html
  65. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts
  66. 19
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html
  67. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.ts
  68. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.html
  69. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/legend-config.component.ts
  70. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  71. 0
      ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html
  72. 0
      ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.scss
  73. 0
      ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts
  74. 7
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  75. 12
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  76. 710
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  77. 174
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  78. 745
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  79. 31
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html
  80. 39
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss
  81. 79
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts
  82. 16
      ui-ngx/src/app/modules/home/components/widget/widget.component.html
  83. 87
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  84. 6
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  85. 11
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  86. 7
      ui-ngx/src/app/shared/components/color-input.component.html
  87. 3
      ui-ngx/src/app/shared/components/color-input.component.scss
  88. 12
      ui-ngx/src/app/shared/components/color-input.component.ts
  89. 4
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  90. 9
      ui-ngx/src/app/shared/components/material-icon-select.component.html
  91. 29
      ui-ngx/src/app/shared/components/material-icon-select.component.scss
  92. 15
      ui-ngx/src/app/shared/components/material-icon-select.component.ts
  93. 1
      ui-ngx/src/app/shared/components/public-api.ts
  94. 7
      ui-ngx/src/app/shared/components/toggle-header.component.html
  95. 30
      ui-ngx/src/app/shared/components/toggle-header.component.scss
  96. 14
      ui-ngx/src/app/shared/components/toggle-header.component.ts
  97. 8
      ui-ngx/src/app/shared/models/query/query.models.ts
  98. 47
      ui-ngx/src/app/shared/models/widget.models.ts
  99. 7
      ui-ngx/src/app/shared/shared.module.ts
  100. 32
      ui-ngx/src/assets/locale/locale.constant-en_US.json

8
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

22
application/src/main/data/json/system/widget_bundles/charts.json

File diff suppressed because one or more lines are too long

28
ui-ngx/src/app/core/api/alias-controller.ts

@ -27,7 +27,13 @@ import {
AlarmFilter, AlarmFilter,
AlarmFilterConfig, AlarmFilterConfig,
createDefaultEntityDataPageLink, createDefaultEntityDataPageLink,
Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, Filter,
FilterInfo,
filterInfoToKeyFilters,
Filters,
KeyFilter,
singleEntityDataPageLink,
singleEntityFilterFromDeviceId,
updateDatasourceFromEntityInfo updateDatasourceFromEntityInfo
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -246,6 +252,9 @@ export class AliasController implements IAliasController {
private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> {
const newDatasource = deepClone(datasource); const newDatasource = deepClone(datasource);
if (newDatasource.type === DatasourceType.device) {
newDatasource.type = DatasourceType.entity;
}
if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount
|| newDatasource.type === DatasourceType.alarmCount) { || newDatasource.type === DatasourceType.alarmCount) {
if (newDatasource.filterId) { if (newDatasource.filterId) {
@ -254,7 +263,22 @@ export class AliasController implements IAliasController {
if (newDatasource.type === DatasourceType.alarmCount) { if (newDatasource.type === DatasourceType.alarmCount) {
newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig, false); newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig, false);
} }
if (newDatasource.entityAliasId) { if (newDatasource.deviceId) {
newDatasource.entityFilter = singleEntityFilterFromDeviceId(newDatasource.deviceId);
if (forceFilter) {
return this.entityService.findSingleEntityInfoByEntityFilter(newDatasource.entityFilter,
{ignoreLoading: true, ignoreErrors: true}).pipe(
map((entity) => {
if (entity) {
updateDatasourceFromEntityInfo(newDatasource, entity, true);
}
return newDatasource;
})
);
} else {
return of(newDatasource);
}
} else if (newDatasource.entityAliasId) {
return this.getAliasInfo(newDatasource.entityAliasId).pipe( return this.getAliasInfo(newDatasource.entityAliasId).pipe(
mergeMap((aliasInfo) => { mergeMap((aliasInfo) => {
newDatasource.aliasName = aliasInfo.alias; newDatasource.aliasName = aliasInfo.alias;

85
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -29,20 +29,25 @@ import {
GridSettings, GridSettings,
WidgetLayout WidgetLayout
} from '@shared/models/dashboard.models'; } from '@shared/models/dashboard.models';
import { isDefined, isString, isUndefined } from '@core/utils'; import { deepClone, isDefined, isDefinedAndNotNull, isString, isUndefined } from '@core/utils';
import { import {
DataKey,
Datasource, Datasource,
datasourcesHasOnlyComparisonAggregation, datasourcesHasOnlyComparisonAggregation,
DatasourceType, DatasourceType,
defaultLegendConfig,
Widget, Widget,
WidgetConfig, WidgetConfig,
widgetType WidgetConfigMode,
widgetType,
WidgetTypeDescriptor
} from '@app/shared/models/widget.models'; } from '@app/shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models';
import { EntityId } from '@app/shared/models/id/entity-id'; import { EntityId } from '@app/shared/models/id/entity-id';
import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models'; import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -211,9 +216,26 @@ export class DashboardUtilsService {
public validateAndUpdateWidget(widget: Widget): Widget { public validateAndUpdateWidget(widget: Widget): Widget {
widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type); widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type);
// Temp workaround // Temp workaround
if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') {
widget.typeAlias = 'basic_timeseries'; widget.typeAlias = 'basic_timeseries';
} }
if (widget.isSystemType && widget.bundleAlias === 'charts' &&
['state_chart', 'basic_timeseries', 'timeseries_bars_flot'].includes(widget.typeAlias)) {
const widgetConfig = widget.config;
const widgetSettings = widget.config.settings;
if (isDefinedAndNotNull(widgetConfig.showLegend)) {
widgetSettings.showLegend = widgetConfig.showLegend;
delete widgetConfig.showLegend;
} else if (isUndefined(widgetSettings.showLegend)) {
widgetSettings.showLegend = true;
}
if (isDefinedAndNotNull(widgetConfig.legendConfig)) {
widgetSettings.legendConfig = widgetConfig.legendConfig;
delete widgetConfig.legendConfig;
} else if (isUndefined(widgetSettings.legendConfig)) {
widgetSettings.legendConfig = defaultLegendConfig(widget.type);
}
}
return widget; return widget;
} }
@ -225,10 +247,8 @@ export class DashboardUtilsService {
widgetConfig.datasources = []; widgetConfig.datasources = [];
} }
widgetConfig.datasources.forEach((datasource) => { widgetConfig.datasources.forEach((datasource) => {
if (datasource.type === 'device') {
datasource.type = DatasourceType.entity;
}
if (datasource.deviceAliasId) { if (datasource.deviceAliasId) {
datasource.type = DatasourceType.entity;
datasource.entityAliasId = datasource.deviceAliasId; datasource.entityAliasId = datasource.deviceAliasId;
delete datasource.deviceAliasId; delete datasource.deviceAliasId;
} }
@ -316,6 +336,55 @@ export class DashboardUtilsService {
}; };
} }
public widgetConfigFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor): WidgetConfig {
const config: WidgetConfig = JSON.parse(widgetTypeDescriptor.defaultConfig);
config.datasources = this.convertDatasourcesFromWidgetType(widgetTypeDescriptor, config, config.datasources);
if (isDefinedAndNotNull(config.alarmSource)) {
config.alarmSource = this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, config.alarmSource);
}
return config;
}
private convertDatasourcesFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor,
config: WidgetConfig, datasources?: Datasource[]): Datasource[] {
const newDatasources: Datasource[] = [];
if (datasources) {
datasources.forEach(datasource => {
newDatasources.push(this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, datasource));
});
}
return newDatasources;
}
private convertDatasourceFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, config: WidgetConfig,
datasource: Datasource): Datasource {
const newDatasource = deepClone(datasource);
if (newDatasource.type === DatasourceType.function) {
newDatasource.type = DatasourceType.entity;
if (widgetTypeDescriptor.hasBasicMode && config.configMode === WidgetConfigMode.basic) {
newDatasource.type = DatasourceType.device;
}
const dataKeys = newDatasource.dataKeys;
newDatasource.dataKeys = [];
dataKeys.forEach(dataKey => {
newDatasource.dataKeys.push(this.convertDataKeyFromWidgetType(widgetTypeDescriptor, config, dataKey));
});
}
return newDatasource;
}
private convertDataKeyFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, config: WidgetConfig, dataKey: DataKey): DataKey {
const newDataKey = deepClone(dataKey);
newDataKey.funcBody = null;
if (widgetTypeDescriptor.type === widgetType.alarm) {
newDataKey.type = DataKeyType.alarm;
} else {
newDataKey.type = DataKeyType.timeseries;
newDataKey.name = newDataKey.label;
}
return newDataKey;
}
private validateAndUpdateState(state: DashboardState) { private validateAndUpdateState(state: DashboardState) {
if (!state.layouts) { if (!state.layouts) {
state.layouts = this.createDefaultLayouts(); state.layouts = this.createDefaultLayouts();
@ -429,7 +498,7 @@ export class DashboardUtilsService {
targetLayout: DashboardLayoutId, targetLayout: DashboardLayoutId,
widget: Widget, widget: Widget,
originalColumns?: number, originalColumns?: number,
originalSize?: {sizeX: number, sizeY: number}, originalSize?: {sizeX: number; sizeY: number},
row?: number, row?: number,
column?: number): void { column?: number): void {
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
@ -502,7 +571,7 @@ export class DashboardUtilsService {
this.removeUnusedWidgets(dashboard); this.removeUnusedWidgets(dashboard);
} }
public isSingleLayoutDashboard(dashboard: Dashboard): {state: string, layout: DashboardLayoutId} { public isSingleLayoutDashboard(dashboard: Dashboard): {state: string; layout: DashboardLayoutId} {
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states; const states = dashboardConfiguration.states;
const stateKeys = Object.keys(states); const stateKeys = Object.keys(states);

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

@ -196,7 +196,7 @@ import * as AddAttributeDialogComponent from '@home/components/attribute/add-att
import * as EditAttributeValuePanelComponent from '@home/components/attribute/edit-attribute-value-panel.component'; import * as EditAttributeValuePanelComponent from '@home/components/attribute/edit-attribute-value-panel.component';
import * as DashboardComponent from '@home/components/dashboard/dashboard.component'; import * as DashboardComponent from '@home/components/dashboard/dashboard.component';
import * as WidgetComponent from '@home/components/widget/widget.component'; import * as WidgetComponent from '@home/components/widget/widget.component';
import * as LegendComponent from '@home/components/widget/legend.component'; import * as LegendComponent from '@home/components/widget/lib/legend.component';
import * as AliasesEntitySelectPanelComponent from '@home/components/alias/aliases-entity-select-panel.component'; import * as AliasesEntitySelectPanelComponent from '@home/components/alias/aliases-entity-select-panel.component';
import * as AliasesEntitySelectComponent from '@home/components/alias/aliases-entity-select.component'; import * as AliasesEntitySelectComponent from '@home/components/alias/aliases-entity-select.component';
import * as WidgetConfigComponent from '@home/components/widget/widget-config.component'; import * as WidgetConfigComponent from '@home/components/widget/widget-config.component';
@ -206,10 +206,10 @@ import * as EntityAliasDialogComponent from '@home/components/alias/entity-alias
import * as EntityFilterComponent from '@home/components/entity/entity-filter.component'; import * as EntityFilterComponent from '@home/components/entity/entity-filter.component';
import * as RelationFiltersComponent from '@home/components/relation/relation-filters.component'; import * as RelationFiltersComponent from '@home/components/relation/relation-filters.component';
import * as EntityAliasSelectComponent from '@home/components/alias/entity-alias-select.component'; import * as EntityAliasSelectComponent from '@home/components/alias/entity-alias-select.component';
import * as DataKeysComponent from '@home/components/widget/data-keys.component'; import * as DataKeysComponent from '@home/components/widget/config/data-keys.component';
import * as DataKeyConfigDialogComponent from '@home/components/widget/data-key-config-dialog.component'; import * as DataKeyConfigDialogComponent from '@home/components/widget/config/data-key-config-dialog.component';
import * as DataKeyConfigComponent from '@home/components/widget/data-key-config.component'; import * as DataKeyConfigComponent from '@home/components/widget/config/data-key-config.component';
import * as LegendConfigComponent from '@home/components/widget/legend-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 ManageWidgetActionsComponent from '@home/components/widget/action/manage-widget-actions.component';
import * as WidgetActionDialogComponent from '@home/components/widget/action/widget-action-dialog.component'; import * as WidgetActionDialogComponent from '@home/components/widget/action/widget-action-dialog.component';
import * as CustomActionPrettyResourcesTabsComponent from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; import * as CustomActionPrettyResourcesTabsComponent from '@home/components/widget/action/custom-action-pretty-resources-tabs.component';
@ -491,7 +491,7 @@ class ModulesMap implements IModulesMap {
'@home/components/attribute/edit-attribute-value-panel.component': EditAttributeValuePanelComponent, '@home/components/attribute/edit-attribute-value-panel.component': EditAttributeValuePanelComponent,
'@home/components/dashboard/dashboard.component': DashboardComponent, '@home/components/dashboard/dashboard.component': DashboardComponent,
'@home/components/widget/widget.component': WidgetComponent, '@home/components/widget/widget.component': WidgetComponent,
'@home/components/widget/legend.component': LegendComponent, '@home/components/widget/lib/legend.component': LegendComponent,
'@home/components/alias/aliases-entity-select-panel.component': AliasesEntitySelectPanelComponent, '@home/components/alias/aliases-entity-select-panel.component': AliasesEntitySelectPanelComponent,
'@home/components/alias/aliases-entity-select.component': AliasesEntitySelectComponent, '@home/components/alias/aliases-entity-select.component': AliasesEntitySelectComponent,
'@home/components/widget/widget-config.component': WidgetConfigComponent, '@home/components/widget/widget-config.component': WidgetConfigComponent,
@ -501,10 +501,10 @@ class ModulesMap implements IModulesMap {
'@home/components/entity/entity-filter.component': EntityFilterComponent, '@home/components/entity/entity-filter.component': EntityFilterComponent,
'@home/components/relation/relation-filters.component': RelationFiltersComponent, '@home/components/relation/relation-filters.component': RelationFiltersComponent,
'@home/components/alias/entity-alias-select.component': EntityAliasSelectComponent, '@home/components/alias/entity-alias-select.component': EntityAliasSelectComponent,
'@home/components/widget/data-keys.component': DataKeysComponent, '@home/components/widget/config/data-keys.component': DataKeysComponent,
'@home/components/widget/data-key-config-dialog.component': DataKeyConfigDialogComponent, '@home/components/widget/config/data-key-config-dialog.component': DataKeyConfigDialogComponent,
'@home/components/widget/data-key-config.component': DataKeyConfigComponent, '@home/components/widget/config/data-key-config.component': DataKeyConfigComponent,
'@home/components/widget/legend-config.component': LegendConfigComponent, '@home/components/widget/lib/settings/common/legend-config.component': LegendConfigComponent,
'@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent, '@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent,
'@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent, '@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent,
'@home/components/widget/action/custom-action-pretty-resources-tabs.component': CustomActionPrettyResourcesTabsComponent, '@home/components/widget/action/custom-action-pretty-resources-tabs.component': CustomActionPrettyResourcesTabsComponent,

7
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html

@ -31,6 +31,13 @@
(click)="clear()"> (click)="clear()">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
<button *ngIf="!selectEntityAliasFormGroup.get('entityAlias').value && !disabled"
style="margin-right: 8px;"
type="button"
matSuffix mat-button color="primary"
(click)="createEntityAlias($event, searchText, false)">
{{ 'entity.create-new' | translate }}
</button>
<mat-autocomplete class="tb-autocomplete" <mat-autocomplete class="tb-autocomplete"
#entityAliasAutocomplete="matAutocomplete" #entityAliasAutocomplete="matAutocomplete"
[displayWith]="displayEntityAliasFn"> [displayWith]="displayEntityAliasFn">

17
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.ts

@ -48,11 +48,11 @@ import { ErrorStateMatcher } from '@angular/material/core';
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntityAliasSelectComponent), useExisting: forwardRef(() => EntityAliasSelectComponent),
multi: true multi: true
}, }/*,
{ {
provide: ErrorStateMatcher, provide: ErrorStateMatcher,
useExisting: EntityAliasSelectComponent useExisting: EntityAliasSelectComponent
}] }*/]
}) })
export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher {
@ -237,16 +237,19 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
} }
} }
createEntityAlias($event: Event, alias: string) { createEntityAlias($event: Event, alias: string, focusOnCancel = true) {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation();
this.creatingEntityAlias = true; this.creatingEntityAlias = true;
if (this.callbacks && this.callbacks.createEntityAlias) { if (this.callbacks && this.callbacks.createEntityAlias) {
this.callbacks.createEntityAlias(alias, this.allowedEntityTypes).subscribe((newAlias) => { this.callbacks.createEntityAlias(alias, this.allowedEntityTypes).subscribe((newAlias) => {
if (!newAlias) { if (!newAlias) {
setTimeout(() => { if (focusOnCancel) {
this.entityAliasInput.nativeElement.blur(); setTimeout(() => {
this.entityAliasInput.nativeElement.focus(); this.entityAliasInput.nativeElement.blur();
}, 0); this.entityAliasInput.nativeElement.focus();
}, 0);
}
} else { } else {
this.entityAliasList.push(newAlias); this.entityAliasList.push(newAlias);
this.modelValue = newAlias.id; this.modelValue = newAlias.id;

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

@ -15,11 +15,25 @@
limitations under the License. limitations under the License.
--> -->
<form [formGroup]="widgetFormGroup" (ngSubmit)="add()" style="width: 1200px;"> <form [formGroup]="widgetFormGroup" style="width: 1200px;">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2 translate>widget.add</h2> <h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span> <span fxFlex>: {{data.widgetInfo.widgetName}}</span>
<div [tb-help]="helpLinkIdForWidgetType()"></div> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-header *ngIf="hasBasicMode" (valueChange)="widgetConfigMode = $event" ignoreMdLgSize="true"
appearance="fill-invert" [options]="[
{
name: translate.instant('widget.basic-mode'),
value: 'basic'
},
{
name: translate.instant('widget.advanced-mode'),
value: 'advanced'
}
]" [value]="widgetConfigMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<div [tb-help]="helpLinkIdForWidgetType()"></div>
</div>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
@ -35,11 +49,26 @@
[functionsOnly]="false" [functionsOnly]="false"
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic"
formControlName="widgetConfig"> formControlName="widgetConfig">
</tb-widget-config> </tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[aliasController]="aliasController"
[stateController]="stateController"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
<div class="tb-preview-panel-content">
<button mat-button
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
</tb-widget-preview>
</fieldset> </fieldset>
</div> </div>
<div mat-dialog-actions fxLayoutAlign="end center"> <div mat-dialog-actions fxLayoutAlign="space-between center">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
@ -47,10 +76,20 @@
cdkFocusInitial> cdkFocusInitial>
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<button mat-raised-button color="primary" <div fxLayout="row" fxLayoutGap="8px">
type="submit" <button *ngIf="!previewMode"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid"> mat-button color="primary"
{{ 'action.add' | translate }} type="button"
</button> [disabled]="(isLoading$ | async) || widgetFormGroup.invalid"
(click)="previewMode = true"
cdkFocusInitial>
{{ 'widget-config.preview' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid">
{{ 'action.add' | translate }}
</button>
</div>
</div> </div>
</form> </form>

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

@ -22,15 +22,17 @@ import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { Widget, widgetTypesData } from '@shared/models/widget.models'; import { Widget, WidgetConfigMode, widgetTypesData } from '@shared/models/widget.models';
import { Dashboard } from '@app/shared/models/dashboard.models'; import { Dashboard } from '@app/shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models'; import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models';
import { isDefined, isString } from '@core/utils'; import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
export interface AddWidgetDialogData { export interface AddWidgetDialogData {
dashboard: Dashboard; dashboard: Dashboard;
aliasController: IAliasController; aliasController: IAliasController;
stateController: IStateController;
widget: Widget; widget: Widget;
widgetInfo: WidgetInfo; widgetInfo: WidgetInfo;
} }
@ -38,22 +40,43 @@ export interface AddWidgetDialogData {
@Component({ @Component({
selector: 'tb-add-widget-dialog', selector: 'tb-add-widget-dialog',
templateUrl: './add-widget-dialog.component.html', templateUrl: './add-widget-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}], providers: [/*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/],
styleUrls: [] styleUrls: []
}) })
export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget> export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget>
implements OnInit, ErrorStateMatcher { implements OnInit, ErrorStateMatcher {
widgetConfigModes = WidgetConfigMode;
widgetFormGroup: UntypedFormGroup; widgetFormGroup: UntypedFormGroup;
dashboard: Dashboard; dashboard: Dashboard;
aliasController: IAliasController; aliasController: IAliasController;
stateController: IStateController;
widget: Widget; widget: Widget;
widgetConfig: WidgetConfigComponentData;
previewMode = false;
hasBasicMode = false;
get widgetConfigMode(): WidgetConfigMode {
return this.hasBasicMode ? (this.widgetConfig?.config?.configMode || WidgetConfigMode.advanced) : WidgetConfigMode.advanced;
}
set widgetConfigMode(widgetConfigMode: WidgetConfigMode) {
if (this.hasBasicMode) {
this.widgetConfig.config.configMode = widgetConfigMode;
this.widgetFormGroup.markAsDirty();
}
}
submitted = false; submitted = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
public translate: TranslateService,
@Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData, @Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddWidgetDialogComponent, Widget>, public dialogRef: MatDialogRef<AddWidgetDialogComponent, Widget>,
@ -62,6 +85,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
this.dashboard = this.data.dashboard; this.dashboard = this.data.dashboard;
this.aliasController = this.data.aliasController; this.aliasController = this.data.aliasController;
this.stateController = this.data.stateController;
this.widget = this.data.widget; this.widget = this.data.widget;
const widgetInfo = this.data.widgetInfo; const widgetInfo = this.data.widgetInfo;
@ -91,7 +115,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
latestDataKeySettingsSchema = isString(rawLatestDataKeySettingsSchema) ? latestDataKeySettingsSchema = isString(rawLatestDataKeySettingsSchema) ?
JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema; JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema;
} }
const widgetConfig: WidgetConfigComponentData = { this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config, config: this.widget.config,
layout: {}, layout: {},
widgetType: this.widget.type, widgetType: this.widget.type,
@ -103,16 +128,17 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
latestDataKeySettingsSchema, latestDataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective, settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
basicModeDirective: widgetInfo.basicModeDirective
}; };
this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false;
this.widgetFormGroup = this.fb.group({ this.widgetFormGroup = this.fb.group({
widgetConfig: [widgetConfig, []] widgetConfig: [this.widgetConfig, []]
} }
); );
} }
ngOnInit(): void { ngOnInit() {
} }
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {

27
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -357,21 +357,32 @@
<tb-details-panel *ngIf="!isEditingWidgetClosed" fxFlex <tb-details-panel *ngIf="!isEditingWidgetClosed" fxFlex
headerTitle="{{editingWidget?.config.title}}" headerTitle="{{editingWidget?.config.title}}"
headerSubtitle="{{ editingWidgetSubtitle }}" headerSubtitle="{{ editingWidgetSubtitle }}"
[isReadOnly]="false" [isReadOnly]="true"
[isAlwaysEdit]="true" (closeDetails)="onEditWidgetClosed()">
(closeDetails)="onEditWidgetClosed()" <div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
(toggleDetailsEditMode)="onRevertWidgetEdit()" <tb-toggle-header *ngIf="tbEditWidget.hasBasicMode" (valueChange)="tbEditWidget.widgetConfigMode = $event" ignoreMdLgSize="true"
(applyDetails)="saveWidget()" appearance="fill-invert" [options]="[
[theForm]="tbEditWidget.widgetFormGroup"> {
<div class="details-buttons"> name: translate.instant('widget.basic-mode'),
value: 'basic'
},
{
name: translate.instant('widget.advanced-mode'),
value: 'advanced'
}
]" [value]="tbEditWidget.widgetConfigMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<div [tb-help]="helpLinkIdForWidgetType()"></div> <div [tb-help]="helpLinkIdForWidgetType()"></div>
</div> </div>
<tb-edit-widget #tbEditWidget <tb-edit-widget #tbEditWidget
[dashboard]="dashboard" [dashboard]="dashboard"
[aliasController]="dashboardCtx.aliasController" [aliasController]="dashboardCtx.aliasController"
[stateController]="dashboardCtx.stateController"
[widgetEditMode]="widgetEditMode" [widgetEditMode]="widgetEditMode"
[widget]="editingWidget" [widget]="editingWidget"
[widgetLayout]="editingWidgetLayout"> [widgetLayout]="editingWidgetLayout"
(revertWidgetConfig)="onRevertWidgetEdit()"
(applyWidgetConfig)="saveWidget()">
</tb-edit-widget> </tb-edit-widget>
</tb-details-panel> </tb-details-panel>
<tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" fxFlex <tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" fxFlex

12
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -332,7 +332,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private mobileService: MobileService, private mobileService: MobileService,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private dialog: MatDialog, private dialog: MatDialog,
private translate: TranslateService, public translate: TranslateService,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private renderer: Renderer2, private renderer: Renderer2,
private ngZone: NgZone, private ngZone: NgZone,
@ -1142,15 +1142,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.searchBundle = ''; this.searchBundle = '';
this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe( this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe(
(widgetTypeInfo) => { (widgetTypeInfo) => {
const config: WidgetConfig = JSON.parse(widgetTypeInfo.defaultConfig); const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo);
config.title = 'New ' + widgetTypeInfo.widgetName; config.title = 'New ' + widgetTypeInfo.widgetName;
config.datasources = [];
if (isDefinedAndNotNull(config.alarmSource)) {
config.alarmSource = {
type: DatasourceType.entity,
dataKeys: config.alarmSource.dataKeys || []
};
}
let newWidget: Widget = { let newWidget: Widget = {
isSystemType: widget.isSystemType, isSystemType: widget.isSystemType,
bundleAlias: widget.bundleAlias, bundleAlias: widget.bundleAlias,
@ -1176,6 +1169,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
data: { data: {
dashboard: this.dashboard, dashboard: this.dashboard,
aliasController: this.dashboardCtx.aliasController, aliasController: this.dashboardCtx.aliasController,
stateController: this.dashboardCtx.stateController,
widget: newWidget, widget: newWidget,
widgetInfo: widgetTypeInfo widgetInfo: widgetTypeInfo
} }

36
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html

@ -22,7 +22,43 @@
[functionsOnly]="widgetEditMode" [functionsOnly]="widgetEditMode"
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[hideToggleHeader]="previewMode"
[widgetConfigMode]="widgetConfigMode"
formControlName="widgetConfig"> formControlName="widgetConfig">
<div class="tb-widget-config-header-prefix">
<button mat-button
*ngIf="previewMode"
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
<div class="tb-widget-config-header-suffix" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<button *ngIf="!previewMode" [disabled]="(isLoading$ | async) || widgetFormGroup.invalid"
mat-button color="primary"
(click)="previewMode = true">
<mat-icon>visibility</mat-icon>
{{ 'widget-config.preview' | translate }}
</button>
<button [disabled]="(isLoading$ | async) || !widgetFormGroup.dirty"
mat-stroked-button color="primary"
(click)="onRevertWidgetConfig()">
<mat-icon>close</mat-icon>
{{ 'action.decline' | translate }}
</button>
<button [disabled]="(isLoading$ | async) || widgetFormGroup.invalid || !widgetFormGroup.dirty"
mat-flat-button color="primary"
(click)="onApplyWidgetConfig()">
<mat-icon>done</mat-icon>
{{ 'action.apply' | translate }}
</button>
</div>
</tb-widget-config> </tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="widget-preview-section"
[aliasController]="aliasController"
[stateController]="stateController"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
</tb-widget-preview>
</fieldset> </fieldset>
</form> </form>

26
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2023 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.
*/
:host {
.widget-preview-section {
position: absolute;
top: 72px;
left: 16px;
right: 16px;
bottom: 16px;
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
}

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

@ -14,23 +14,23 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Dashboard, WidgetLayout } from '@shared/models/dashboard.models'; import { Dashboard, WidgetLayout } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { Widget } from '@shared/models/widget.models'; import { Widget, WidgetConfigMode } from '@shared/models/widget.models';
import { WidgetComponentService } from '@home/components/widget/widget-component.service'; import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigComponentData } from '../../models/widget-component.models'; import { WidgetConfigComponentData } from '../../models/widget-component.models';
import { isDefined, isString } from '@core/utils'; import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
@Component({ @Component({
selector: 'tb-edit-widget', selector: 'tb-edit-widget',
templateUrl: './edit-widget.component.html', templateUrl: './edit-widget.component.html',
styleUrls: [] styleUrls: ['./edit-widget.component.scss']
}) })
export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges { export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges {
@ -40,6 +40,9 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
@Input() @Input()
aliasController: IAliasController; aliasController: IAliasController;
@Input()
stateController: IStateController;
@Input() @Input()
widgetEditMode: boolean; widgetEditMode: boolean;
@ -49,10 +52,33 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
@Input() @Input()
widgetLayout: WidgetLayout; widgetLayout: WidgetLayout;
@Output()
applyWidgetConfig = new EventEmitter<void>();
@Output()
revertWidgetConfig = new EventEmitter<void>();
widgetFormGroup: UntypedFormGroup; widgetFormGroup: UntypedFormGroup;
widgetConfig: WidgetConfigComponentData; widgetConfig: WidgetConfigComponentData;
previewMode = false;
hasBasicMode = false;
get widgetConfigMode(): WidgetConfigMode {
return this.hasBasicMode ? (this.widgetConfig?.config?.configMode || WidgetConfigMode.advanced) : WidgetConfigMode.advanced;
}
set widgetConfigMode(widgetConfigMode: WidgetConfigMode) {
if (this.hasBasicMode) {
this.widgetConfig.config.configMode = widgetConfigMode;
this.widgetFormGroup.markAsDirty();
}
}
private currentWidgetConfigChanged = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private dialog: MatDialog, private dialog: MatDialog,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
@ -78,10 +104,27 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
} }
} }
if (reloadConfig) { if (reloadConfig) {
if (this.currentWidgetConfigChanged) {
this.currentWidgetConfigChanged = false;
} else {
this.previewMode = false;
}
this.loadWidgetConfig(); this.loadWidgetConfig();
} }
} }
onApplyWidgetConfig() {
if (this.widgetFormGroup.valid) {
this.currentWidgetConfigChanged = true;
this.applyWidgetConfig.emit();
}
}
onRevertWidgetConfig() {
this.currentWidgetConfigChanged = true;
this.revertWidgetConfig.emit();
}
private loadWidgetConfig() { private loadWidgetConfig() {
if (!this.widget) { if (!this.widget) {
return; return;
@ -113,6 +156,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema; JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema;
} }
this.widgetConfig = { this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config, config: this.widget.config,
layout: this.widgetLayout, layout: this.widgetLayout,
widgetType: this.widget.type, widgetType: this.widget.type,
@ -124,8 +168,10 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
latestDataKeySettingsSchema, latestDataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective, settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
basicModeDirective: widgetInfo.basicModeDirective
}; };
this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false;
this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); this.widgetFormGroup.reset({widgetConfig: this.widgetConfig});
} }
} }

7
ui-ngx/src/app/modules/home/components/filter/filter-select.component.html

@ -31,6 +31,13 @@
(click)="clear()"> (click)="clear()">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
<button *ngIf="!selectFilterFormGroup.get('filter').value && !disabled"
style="margin-right: 8px;"
type="button"
matSuffix mat-button color="primary"
(click)="createFilter($event, searchText, false)">
{{ 'filter.create-new' | translate }}
</button>
<mat-autocomplete class="tb-autocomplete" <mat-autocomplete class="tb-autocomplete"
#filterAutocomplete="matAutocomplete" #filterAutocomplete="matAutocomplete"
[displayWith]="displayFilterFn"> [displayWith]="displayFilterFn">

13
ui-ngx/src/app/modules/home/components/filter/filter-select.component.ts

@ -226,16 +226,19 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte
} }
} }
createFilter($event: Event, filter: string) { createFilter($event: Event, filter: string, focusOnCancel = true) {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation();
this.creatingFilter = true; this.creatingFilter = true;
if (this.callbacks && this.callbacks.createFilter) { if (this.callbacks && this.callbacks.createFilter) {
this.callbacks.createFilter(filter).subscribe((newFilter) => { this.callbacks.createFilter(filter).subscribe((newFilter) => {
if (!newFilter) { if (!newFilter) {
setTimeout(() => { if (focusOnCancel) {
this.filterInput.nativeElement.blur(); setTimeout(() => {
this.filterInput.nativeElement.focus(); this.filterInput.nativeElement.blur();
}, 0); this.filterInput.nativeElement.focus();
}, 0);
}
} else { } else {
this.filterList.push(newFilter); this.filterList.push(newFilter);
this.modelValue = newFilter.id; this.modelValue = newFilter.id;

44
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -36,7 +36,6 @@ import { EditAttributeValuePanelComponent } from '@home/components/attribute/edi
import { DashboardComponent } from '@home/components/dashboard/dashboard.component'; import { DashboardComponent } from '@home/components/dashboard/dashboard.component';
import { WidgetComponent } from '@home/components/widget/widget.component'; import { WidgetComponent } from '@home/components/widget/widget.component';
import { WidgetComponentService } from '@home/components/widget/widget-component.service'; import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { LegendComponent } from '@home/components/widget/legend.component';
import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component'; import { AliasesEntitySelectPanelComponent } from '@home/components/alias/aliases-entity-select-panel.component';
import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component'; import { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.component';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
@ -45,11 +44,6 @@ import { EntityFilterViewComponent } from '@home/components/entity/entity-filter
import { EntityAliasDialogComponent } from '@home/components/alias/entity-alias-dialog.component'; import { EntityAliasDialogComponent } from '@home/components/alias/entity-alias-dialog.component';
import { EntityFilterComponent } from '@home/components/entity/entity-filter.component'; import { EntityFilterComponent } from '@home/components/entity/entity-filter.component';
import { RelationFiltersComponent } from '@home/components/relation/relation-filters.component'; import { RelationFiltersComponent } from '@home/components/relation/relation-filters.component';
import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-select.component';
import { DataKeysComponent } from '@home/components/widget/data-keys.component';
import { DataKeyConfigDialogComponent } from '@home/components/widget/data-key-config-dialog.component';
import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component';
import { LegendConfigComponent } from '@home/components/widget/legend-config.component';
import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component'; import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component';
import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component'; import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component';
import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component';
@ -78,7 +72,6 @@ import { ComplexFilterPredicateDialogComponent } from '@home/components/filter/c
import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dialog.component'; import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dialog.component';
import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component'; import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component';
import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component'; import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component';
import { FilterSelectComponent } from '@home/components/filter/filter-select.component';
import { FiltersEditComponent } from '@home/components/filter/filters-edit.component'; import { FiltersEditComponent } from '@home/components/filter/filters-edit.component';
import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component'; import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component';
import { UserFilterDialogComponent } from '@home/components/filter/user-filter-dialog.component'; import { UserFilterDialogComponent } from '@home/components/filter/user-filter-dialog.component';
@ -152,8 +145,6 @@ import { AlarmDynamicValue } from '@home/components/profile/alarm/alarm-dynamic-
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.component'; import { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.component';
import { QueueFormComponent } from '@home/components/queue/queue-form.component'; import { QueueFormComponent } from '@home/components/queue/queue-form.component';
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
import { WidgetSettingsComponent } from '@home/components/widget/widget-settings.component';
import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component'; import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { EntityVersionsTableComponent } from '@home/components/vc/entity-versions-table.component'; import { EntityVersionsTableComponent } from '@home/components/vc/entity-versions-table.component';
@ -178,10 +169,14 @@ import { modulesMap } from '@modules/common/modules-map';
import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component'; import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component';
import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component'; import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component';
import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component';
import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component'; import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component';
import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component';
import { DeviceInfoFilterComponent } from '@home/components/device/device-info-filter.component'; import { DeviceInfoFilterComponent } from '@home/components/device/device-info-filter.component';
import { WidgetPreviewComponent } from '@home/components/widget/widget-preview.component';
import {
ManageWidgetActionsDialogComponent
} from '@home/components/widget/action/manage-widget-actions-dialog.component';
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module';
@NgModule({ @NgModule({
declarations: declarations:
@ -206,9 +201,7 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
AlarmTableHeaderComponent, AlarmTableHeaderComponent,
AlarmTableComponent, AlarmTableComponent,
AlarmAssigneePanelComponent, AlarmAssigneePanelComponent,
AlarmAssigneeSelectComponent,
AlarmAssigneeSelectPanelComponent, AlarmAssigneeSelectPanelComponent,
AlarmFilterConfigComponent,
AttributeTableComponent, AttributeTableComponent,
AddAttributeDialogComponent, AddAttributeDialogComponent,
EditAttributeValuePanelComponent, EditAttributeValuePanelComponent,
@ -220,18 +213,13 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
DashboardComponent, DashboardComponent,
WidgetContainerComponent, WidgetContainerComponent,
WidgetComponent, WidgetComponent,
LegendComponent,
WidgetSettingsComponent,
WidgetConfigComponent, WidgetConfigComponent,
WidgetPreviewComponent,
EntityFilterViewComponent, EntityFilterViewComponent,
EntityFilterComponent, EntityFilterComponent,
EntityAliasSelectComponent,
DataKeysComponent,
DataKeyConfigComponent,
DataKeyConfigDialogComponent,
LegendConfigComponent,
ManageWidgetActionsComponent, ManageWidgetActionsComponent,
WidgetActionDialogComponent, WidgetActionDialogComponent,
ManageWidgetActionsDialogComponent,
CustomActionPrettyResourcesTabsComponent, CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent, CustomActionPrettyEditorComponent,
MobileActionEditorComponent, MobileActionEditorComponent,
@ -253,7 +241,6 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
KeyFilterDialogComponent, KeyFilterDialogComponent,
FilterDialogComponent, FilterDialogComponent,
FiltersDialogComponent, FiltersDialogComponent,
FilterSelectComponent,
FilterTextComponent, FilterTextComponent,
FiltersEditComponent, FiltersEditComponent,
FiltersEditPanelComponent, FiltersEditPanelComponent,
@ -338,7 +325,8 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
CommonModule, CommonModule,
SharedModule, SharedModule,
SharedHomeComponentsModule, SharedHomeComponentsModule,
WidgetSettingsModule, WidgetConfigComponentsModule,
BasicWidgetConfigModule,
Lwm2mProfileComponentsModule, Lwm2mProfileComponentsModule,
SnmpDeviceProfileTransportModule, SnmpDeviceProfileTransportModule,
StatesControllerModule, StatesControllerModule,
@ -360,9 +348,7 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
RelationFiltersComponent, RelationFiltersComponent,
AlarmTableComponent, AlarmTableComponent,
AlarmAssigneePanelComponent, AlarmAssigneePanelComponent,
AlarmAssigneeSelectComponent,
AlarmAssigneeSelectPanelComponent, AlarmAssigneeSelectPanelComponent,
AlarmFilterConfigComponent,
AttributeTableComponent, AttributeTableComponent,
AliasesEntitySelectComponent, AliasesEntitySelectComponent,
AliasesEntityAutocompleteComponent, AliasesEntityAutocompleteComponent,
@ -371,18 +357,13 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
DashboardComponent, DashboardComponent,
WidgetContainerComponent, WidgetContainerComponent,
WidgetComponent, WidgetComponent,
LegendComponent,
WidgetSettingsComponent,
WidgetConfigComponent, WidgetConfigComponent,
WidgetPreviewComponent,
EntityFilterViewComponent, EntityFilterViewComponent,
EntityFilterComponent, EntityFilterComponent,
EntityAliasSelectComponent,
DataKeysComponent,
DataKeyConfigComponent,
DataKeyConfigDialogComponent,
LegendConfigComponent,
ManageWidgetActionsComponent, ManageWidgetActionsComponent,
WidgetActionDialogComponent, WidgetActionDialogComponent,
ManageWidgetActionsDialogComponent,
CustomActionPrettyResourcesTabsComponent, CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent, CustomActionPrettyEditorComponent,
MobileActionEditorComponent, MobileActionEditorComponent,
@ -403,7 +384,6 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
KeyFilterDialogComponent, KeyFilterDialogComponent,
FilterDialogComponent, FilterDialogComponent,
FiltersDialogComponent, FiltersDialogComponent,
FilterSelectComponent,
FilterTextComponent, FilterTextComponent,
FiltersEditComponent, FiltersEditComponent,
UserFilterDialogComponent, UserFilterDialogComponent,

52
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html

@ -0,0 +1,52 @@
<!--
Copyright © 2016-2023 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 [formGroup]="actionsSettings" style="width: 900px;">
<mat-toolbar color="primary">
<h2>{{ data.widgetTitle }}: {{ 'widget-config.actions' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content style="position: relative; height: 500px;">
<tb-manage-widget-actions
[callbacks]="data.callbacks"
[widgetType] = "data.widgetType"
[actionSources]="actionSources"
formControlName="actions">
</tb-manage-widget-actions>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="save()"
[disabled]="(isLoading$ | async) || actionsSettings.invalid || !actionsSettings.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

71
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts

@ -0,0 +1,71 @@
///
/// Copyright © 2016-2023 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 { widgetType } from '@shared/models/widget.models';
import {
WidgetActionCallbacks,
WidgetActionsData
} from '@home/components/widget/action/manage-widget-actions.component.models';
import { Component, Inject, OnInit } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
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 { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
export interface ManageWidgetActionsDialogData {
widgetTitle: string;
actionsData: WidgetActionsData;
callbacks: WidgetActionCallbacks;
widgetType: widgetType;
}
@Component({
selector: 'tb-manage-widget-actions-dialog',
templateUrl: './manage-widget-actions-dialog.component.html',
providers: [],
styleUrls: []
})
export class ManageWidgetActionsDialogComponent extends DialogComponent<ManageWidgetActionsDialogComponent,
WidgetActionsData> implements OnInit {
actionSources = this.data.actionsData.actionSources;
actionsSettings: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ManageWidgetActionsDialogData,
private fb: UntypedFormBuilder,
public dialogRef: MatDialogRef<ManageWidgetActionsDialogComponent, WidgetActionsData>) {
super(store, router, dialogRef);
}
ngOnInit() {
this.actionsSettings = this.fb.group({
actions: [this.data.actionsData.actionsMap, []]
});
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.dialogRef.close(this.actionsSettings.get('actions').value);
}
}

8
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html

@ -15,8 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<div class="mat-padding tb-entity-table tb-absolute-fill"> <div class="tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-entity-table-content tb-outlined-border"> <div fxFlex fxLayout="column" class="tb-entity-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> <mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<span class="tb-entity-table-title" translate>widget-config.actions</span> <span class="tb-entity-table-title" translate>widget-config.actions</span>
@ -69,7 +69,7 @@
class="tb-drop-list"> class="tb-drop-list">
<ng-container matColumnDef="actionSourceId"> <ng-container matColumnDef="actionSourceId">
<mat-header-cell *matHeaderCellDef style="width: 36px"></mat-header-cell> <mat-header-cell *matHeaderCellDef style="width: 36px"></mat-header-cell>
<mat-cell *matCellDef="let action"> <mat-cell *matCellDef="let action" class="tb-icon-cell">
<div cdkDragHandle <div cdkDragHandle
class="tb-drag-handle" class="tb-drag-handle"
style="min-width: 36px" style="min-width: 36px"
@ -95,7 +95,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="icon"> <ng-container matColumnDef="icon">
<mat-header-cell *matHeaderCellDef style="width: 40px"> {{ 'widget-config.action-icon' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef style="width: 40px"> {{ 'widget-config.action-icon' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let action" style="width: 40px"> <mat-cell *matCellDef="let action" class="tb-icon-cell" style="width: 40px">
<mat-icon>{{ action.icon }}</mat-icon> <mat-icon>{{ action.icon }}</mat-icon>
</mat-cell> </mat-cell>
</ng-container> </ng-container>

12
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss

@ -24,12 +24,6 @@
background: #fff; background: #fff;
overflow: hidden; overflow: hidden;
&.tb-outlined-border {
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
}
.tb-entity-table-title { .tb-entity-table-title {
padding-right: 20px; padding-right: 20px;
white-space: nowrap; white-space: nowrap;
@ -49,6 +43,12 @@
opacity: 1 !important; opacity: 1 !important;
} }
.mat-mdc-cell.tb-icon-cell {
.mat-icon {
vertical-align: middle;
}
}
.tb-draggable { .tb-draggable {
&.cdk-drag-placeholder { &.cdk-drag-placeholder {
opacity: 0; opacity: 0;

21
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts

@ -76,6 +76,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
@Input() callbacks: WidgetActionCallbacks; @Input() callbacks: WidgetActionCallbacks;
@Input() actionSources: {[actionSourceId: string]: WidgetActionSource};
innerValue: WidgetActionsData; innerValue: WidgetActionsData;
displayedColumns: string[]; displayedColumns: string[];
@ -89,6 +91,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
dragDisabled = true; dragDisabled = true;
private widgetResize$: ResizeObserver; private widgetResize$: ResizeObserver;
private destroyed = false;
@ViewChild('searchInput') searchInputField: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef;
@ -123,6 +126,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroyed = true;
if (this.widgetResize$) { if (this.widgetResize$) {
this.widgetResize$.disconnect(); this.widgetResize$.disconnect();
} }
@ -336,22 +340,25 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
this.disabled = isDisabled; this.disabled = isDisabled;
} }
writeValue(obj: WidgetActionsData): void { writeValue(actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>}): void {
this.innerValue = obj; this.innerValue = {
if (this.innerValue) { actionsMap: actions || {},
setTimeout(() => { actionSources: this.actionSources || {}
};
setTimeout(() => {
if (!this.destroyed) {
this.dataSource.setActions(this.innerValue); this.dataSource.setActions(this.innerValue);
if (this.viewsInited) { if (this.viewsInited) {
this.resetSortAndFilter(true); this.resetSortAndFilter(true);
} else { } else {
this.dirtyValue = true; this.dirtyValue = true;
} }
}, 0); }
} }, 0);
} }
private onActionsUpdated() { private onActionsUpdated() {
this.updateData(true); this.updateData(true);
this.propagateChange(this.innerValue); this.propagateChange(this.innerValue.actionsMap);
} }
} }

20
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-config.scss

@ -0,0 +1,20 @@
/**
* Copyright © 2016-2023 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.
*/
:host {
display: flex;
flex-direction: column;
gap: 16px;
}

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

@ -0,0 +1,49 @@
///
/// Copyright © 2016-2023 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 { NgModule, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { IBasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
import {
SimpleCardBasicConfigComponent
} from '@home/components/widget/config/basic/cards/simple-card-basic-config.component';
import {
WidgetActionsPanelComponent
} from '@home/components/widget/config/basic/common/widget-actions-panel.component';
@NgModule({
declarations: [
WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent
],
imports: [
CommonModule,
SharedModule,
WidgetConfigComponentsModule
],
exports: [
WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
}
export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetConfigComponent>} = {
'tb-simple-card-basic-config': SimpleCardBasicConfigComponent
};

86
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html

@ -0,0 +1,86 @@
<!--
Copyright © 2016-2023 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.
-->
<ng-container [formGroup]="simpleCardWidgetConfigForm">
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig"
[onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDataKeyLabel
hideDataKeyColor
hideDataKeyUnits
hideDataKeyDecimals
formControlName="datasources">
</tb-datasources>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widget-config.appearance</div>
<div class="tb-widget-config-row">
<div translate>widgets.simple-card.label</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div translate>widgets.simple-card.label-position</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="labelPosition">
<mat-option [value]="'left'">
{{ 'widgets.simple-card.label-position-left' | translate }}
</mat-option>
<mat-option [value]="'top'">
{{ 'widgets.simple-card.label-position-top' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div translate>widget-config.units-short</div>
<tb-widget-units
formControlName="units">
</tb-widget-units>
</div>
<div class="tb-widget-config-row space-between">
<div translate>widget-config.decimals-short</div>
<mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.background' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

110
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts

@ -0,0 +1,110 @@
///
/// Copyright © 2016-2023 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 } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation
} from '@shared/models/widget.models';
@Component({
selector: 'tb-simple-card-basic-config',
templateUrl: './simple-card-basic-config.component.html',
styleUrls: ['../basic-config.scss', '../../widget-config.scss']
})
export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
public get displayTimewindowConfig(): boolean {
const datasources = this.simpleCardWidgetConfigForm.get('datasources').value;
return datasourcesHasAggregation(datasources);
}
public onlyHistoryTimewindow(): boolean {
const datasources = this.simpleCardWidgetConfigForm.get('datasources').value;
return datasourcesHasOnlyComparisonAggregation(datasources);
}
simpleCardWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected configForm(): UntypedFormGroup {
return this.simpleCardWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
this.simpleCardWidgetConfigForm = this.fb.group({
timewindowConfig: [{
useDashboardTimewindow: configData.config.useDashboardTimewindow,
displayTimewindow: configData.config.useDashboardTimewindow,
timewindow: configData.config.timewindow
}, []],
datasources: [configData.config.datasources, []],
label: [this.getDataKeyLabel(configData.config.datasources), []],
labelPosition: [configData.config.settings?.labelPosition, []],
units: [configData.config.units, []],
decimals: [configData.config.decimals, []],
color: [configData.config.color, []],
backgroundColor: [configData.config.backgroundColor, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources;
this.setDataKeyLabel(config.label, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions;
this.widgetConfig.config.units = config.units;
this.widgetConfig.config.decimals = config.decimals;
this.widgetConfig.config.color = config.color;
this.widgetConfig.config.backgroundColor = config.backgroundColor;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.labelPosition = config.labelPosition;
return this.widgetConfig;
}
private getDataKeyLabel(datasources?: Datasource[]): string {
if (datasources && datasources.length) {
const dataKeys = datasources[0].dataKeys;
if (dataKeys && dataKeys.length) {
return dataKeys[0].label;
}
}
return '';
}
private setDataKeyLabel(label: string, datasources?: Datasource[]) {
if (datasources && datasources.length) {
const dataKeys = datasources[0].dataKeys;
if (dataKeys && dataKeys.length) {
dataKeys[0].label = label;
}
}
}
}

36
ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html

@ -0,0 +1,36 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-widget-config-panel" (click)="manageWidgetActions()" style="cursor: pointer;">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<div class="tb-widget-config-panel-title" translate style="padding-right: 48px;">widget-config.actions</div>
<mat-chip-listbox fxFlex>
<ng-container *ngFor="let actionSourceId of widgetActionSourceIds">
<mat-chip *ngFor="let widgetAction of widgetActionsByActionSourceId(actionSourceId)">
<mat-icon matChipAvatar>{{ widgetAction.icon }}</mat-icon>
{{ widgetAction.name }}
</mat-chip>
</ng-container>
</mat-chip-listbox>
<button mat-stroked-button color="primary" *ngIf="!hasWidgetActions">
{{ 'widget-config.add-action' | translate }}
</button>
<button mat-icon-button color="primary" *ngIf="hasWidgetActions">
<mat-icon>add</mat-icon>
</button>
</div>
</div>

133
ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts

@ -0,0 +1,133 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { WidgetActionsData } from '@home/components/widget/action/manage-widget-actions.component.models';
import { WidgetActionDescriptor } from '@shared/models/widget.models';
import {
ManageWidgetActionsDialogComponent,
ManageWidgetActionsDialogData
} from '@home/components/widget/action/manage-widget-actions-dialog.component';
import { deepClone } from '@core/utils';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'tb-widget-actions-panel',
templateUrl: './widget-actions-panel.component.html',
styleUrls: ['../../widget-config.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetActionsPanelComponent),
multi: true
}
]
})
export class WidgetActionsPanelComponent implements ControlValueAccessor, OnInit {
@Input()
disabled: boolean;
actionsFormGroup: UntypedFormGroup;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog,
private cd: ChangeDetectorRef,
private widgetConfigComponent: WidgetConfigComponent) {
}
ngOnInit() {
this.actionsFormGroup = this.fb.group({
actions: [null, []]
});
this.actionsFormGroup.get('actions').valueChanges.subscribe(
(val) => this.propagateChange(val)
);
}
writeValue(actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>}): void {
this.actionsFormGroup.get('actions').patchValue(actions || {}, {emitEvent: false});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.actionsFormGroup.disable({emitEvent: false});
} else {
this.actionsFormGroup.enable({emitEvent: false});
}
}
public get widgetActionSourceIds(): Array<string> {
const actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = this.actionsFormGroup.get('actions').value;
return actions ? Object.keys(actions) : [];
}
public widgetActionsByActionSourceId(actionSourceId: string): Array<WidgetActionDescriptor> {
const actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = this.actionsFormGroup.get('actions').value;
return actions[actionSourceId] || [];
}
public get hasWidgetActions(): boolean {
const actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = this.actionsFormGroup.get('actions').value;
if (actions) {
for (const actionSourceId of Object.keys(actions)) {
if (actions[actionSourceId] && actions[actionSourceId].length) {
return true;
}
}
}
return false;
}
public manageWidgetActions() {
const actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = this.actionsFormGroup.get('actions').value;
const actionsData: WidgetActionsData = {
actionsMap: deepClone(actions),
actionSources: this.widgetConfigComponent.modelValue.actionSources || {}
};
this.dialog.open<ManageWidgetActionsDialogComponent, ManageWidgetActionsDialogData,
{[actionSourceId: string]: Array<WidgetActionDescriptor>}>(ManageWidgetActionsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
widgetTitle: this.widgetConfigComponent.modelValue.widgetName,
callbacks: this.widgetConfigComponent.widgetConfigCallbacks,
actionsData,
widgetType: this.widgetConfigComponent.widgetType
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.actionsFormGroup.get('actions').patchValue(res);
this.cd.markForCheck();
}
}
);
}
}

5
ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html → ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html

@ -32,6 +32,7 @@
<tb-data-key-config #dataKeyConfig <tb-data-key-config #dataKeyConfig
[dataKeySettingsSchema]="data.dataKeySettingsSchema" [dataKeySettingsSchema]="data.dataKeySettingsSchema"
[dataKeySettingsDirective]="data.dataKeySettingsDirective" [dataKeySettingsDirective]="data.dataKeySettingsDirective"
[deviceId]="data.deviceId"
[entityAliasId]="data.entityAliasId" [entityAliasId]="data.entityAliasId"
[dashboard]="data.dashboard" [dashboard]="data.dashboard"
[aliasController]="data.aliasController" [aliasController]="data.aliasController"
@ -39,6 +40,10 @@
[widgetType]="data.widgetType" [widgetType]="data.widgetType"
[showPostProcessing]="data.showPostProcessing" [showPostProcessing]="data.showPostProcessing"
[callbacks]="data.callbacks" [callbacks]="data.callbacks"
[hideDataKeyLabel]="data.hideDataKeyLabel"
[hideDataKeyColor]="data.hideDataKeyColor"
[hideDataKeyUnits]="data.hideDataKeyUnits"
[hideDataKeyDecimals]="data.hideDataKeyDecimals"
formControlName="dataKey"> formControlName="dataKey">
</tb-data-key-config> </tb-data-key-config>
</div> </div>

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

7
ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts → ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts

@ -24,7 +24,7 @@ import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; import { DataKey, Widget, widgetType } from '@shared/models/widget.models';
import { DataKeysCallbacks } from './data-keys.component.models'; import { DataKeysCallbacks } from './data-keys.component.models';
import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; import { DataKeyConfigComponent } from '@home/components/widget/config/data-key-config.component';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
@ -36,9 +36,14 @@ export interface DataKeyConfigDialogData {
aliasController: IAliasController; aliasController: IAliasController;
widget: Widget; widget: Widget;
widgetType: widgetType; widgetType: widgetType;
deviceId?: string;
entityAliasId?: string; entityAliasId?: string;
showPostProcessing?: boolean; showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks; callbacks?: DataKeysCallbacks;
hideDataKeyLabel: boolean;
hideDataKeyColor: boolean;
hideDataKeyUnits: boolean;
hideDataKeyDecimals: boolean;
} }
@Component({ @Component({

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

@ -40,11 +40,11 @@
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start center" fxLayoutAlign.xs fxLayoutGap="8px"> <div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start center" fxLayoutAlign.xs fxLayoutGap="8px">
<mat-form-field fxFlex class="mat-block"> <mat-form-field *ngIf="!hideDataKeyLabel" fxFlex class="mat-block">
<mat-label translate>datakey.label</mat-label> <mat-label translate>datakey.label</mat-label>
<input matInput formControlName="label" required> <input matInput formControlName="label" required>
</mat-form-field> </mat-form-field>
<tb-color-input fxFlex <tb-color-input *ngIf="!hideDataKeyColor" fxFlex
required required
label="{{'datakey.color' | translate}}" label="{{'datakey.color' | translate}}"
icon="format_color_fill" icon="format_color_fill"
@ -53,11 +53,11 @@
</tb-color-input> </tb-color-input>
</div> </div>
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start center" fxLayoutAlign.xs fxLayoutGap="8px" *ngIf="modelValue.type !== dataKeyTypes.alarm"> <div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="start center" fxLayoutAlign.xs fxLayoutGap="8px" *ngIf="modelValue.type !== dataKeyTypes.alarm">
<mat-form-field fxFlex> <mat-form-field *ngIf="!hideDataKeyUnits" fxFlex>
<mat-label translate>datakey.units</mat-label> <mat-label translate>datakey.units</mat-label>
<input matInput formControlName="units"> <input matInput formControlName="units">
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex> <mat-form-field *ngIf="!hideDataKeyDecimals" fxFlex>
<mat-label translate>datakey.decimals</mat-label> <mat-label translate>datakey.decimals</mat-label>
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1"> <input matInput formControlName="decimals" type="number" min="0" max="15" step="1">
</mat-form-field> </mat-form-field>

2
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss → ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.scss

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../../scss/constants'; @import '../../../../../../scss/constants';
:host { :host {
.tb-datakey-config { .tb-datakey-config {

30
ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts → ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts

@ -40,7 +40,7 @@ import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators'; import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators';
@ -52,6 +52,7 @@ import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models'; import { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models';
import { genNextLabel } from '@core/utils'; import { genNextLabel } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-data-key-config', selector: 'tb-data-key-config',
@ -90,6 +91,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
comparisonResultTypeTranslations = comparisonResultTypeTranslationMap; comparisonResultTypeTranslations = comparisonResultTypeTranslationMap;
@Input()
deviceId: string;
@Input() @Input()
entityAliasId: string; entityAliasId: string;
@ -117,6 +121,22 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@Input() @Input()
showPostProcessing = true; showPostProcessing = true;
@Input()
@coerceBoolean()
hideDataKeyLabel = false;
@Input()
@coerceBoolean()
hideDataKeyColor = false;
@Input()
@coerceBoolean()
hideDataKeyUnits = false;
@Input()
@coerceBoolean()
hideDataKeyDecimals = false;
@ViewChild('keyInput') keyInput: ElementRef; @ViewChild('keyInput') keyInput: ElementRef;
@ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent; @ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent;
@ -377,9 +397,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
if (this.modelValue.type === DataKeyType.alarm) { if (this.modelValue.type === DataKeyType.alarm) {
fetchObservable = of(this.alarmKeys); fetchObservable = of(this.alarmKeys);
} else { } else {
if (this.entityAliasId) { if (this.deviceId || this.entityAliasId) {
const dataKeyTypes = [this.modelValue.type]; const dataKeyTypes = [this.modelValue.type];
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); if (this.deviceId) {
fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes);
} else {
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes);
}
} else { } else {
fetchObservable = of([]); fetchObservable = of([]);
} }

67
ui-ngx/src/app/modules/home/components/widget/data-keys.component.html → ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html

@ -15,85 +15,92 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field [formGroup]="keysListFormGroup" class="mat-block tb-chip-list"> <mat-form-field [formGroup]="keysListFormGroup" class="mat-block">
<mat-chip-grid #chipList formControlName="keys"> <mat-label>{{placeholder}}</mat-label>
<mat-chip-grid #chipList formControlName="keys" [required]="required">
<div class="tb-datakeys-container" <div class="tb-datakeys-container"
[dndDropzone]="[dndId]" [dndDropzone]="[dndId]"
[dndHorizontal]="true" [dndHorizontal]="true"
dndEffectAllowed="move" dndEffectAllowed="move"
dndAllowExternal="false" dndAllowExternal="false"
[dndDisableIf]="disabled" [dndDisableIf]="disabled || dragDisabled"
(dndDrop)="onChipDrop($event)"> (dndDrop)="onChipDrop($event)">
<mat-chip-row #placeholderChipRow <div #placeholderChipRow
dndPlaceholderRef dndPlaceholderRef
class="tb-datakey-chip tb-datakey-chip-dnd-placeholder"> class="mat-mdc-chip mat-mdc-chip-row mdc-evolution-chip mat-mdc-standard-chip tb-datakey-chip tb-datakey-chip-dnd-placeholder">
</mat-chip-row> </div>
<mat-chip-row #chipRow *ngFor="let key of keys; let $index = index" <mat-chip-row #chipRow *ngFor="let key of keys; let $index = index"
class="tb-datakey-chip" class="tb-datakey-chip"
(dndStart)="chipDragStart($index, chipRow, placeholderChipRow)" (dndStart)="chipDragStart($index, chipRow, placeholderChipRow)"
(dndEnd)="chipDragEnd()" (dndEnd)="chipDragEnd()"
[dndDraggable]="key" [dndDraggable]="key"
[dndDisableIf]="disabled" [dndDisableIf]="disabled || dragDisabled"
dndType="{{ dndId }}" dndType="{{ dndId }}"
dndEffectAllowed="move" dndEffectAllowed="move"
dndDraggingSourceClass="tb-chip-dragging" dndDraggingSourceClass="tb-chip-dragging"
[removable]="!disabled && !isCountDatasource" [removable]="!disabled && !isCountDatasource"
(removed)="remove(key)"> (removed)="remove(key)">
<div dndDragImageRef fxLayout="row" fxLayoutAlign="start center" class="tb-attribute-chip" <div dndDragImageRef fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip"
[ngClass]="{'tb-dragging-chip-image': $index === dragIndex}"> [ngClass]="{'tb-dragging-chip-image': $index === dragIndex}">
<div class="tb-absolute-fill tb-dragging-chip-image-fill"></div> <div class="tb-absolute-fill tb-dragging-chip-image-fill"></div>
<div dndHandle *ngIf="!disabled" class="tb-chip-drag-handle" style="margin-right: 5px;"> <div dndHandle *ngIf="!disabled && !dragDisabled" class="tb-chip-drag-handle">
<mat-icon class="tb-mat-20">drag_handle</mat-icon> <mat-icon class="tb-mat-18">drag_indicator</mat-icon>
</div>
<div class="tb-color-preview" (click)="showColorPicker(key)" style="margin-right: 5px;">
<div class="tb-color-result" [ngStyle]="{background: key.color}"></div>
</div> </div>
<div class="tb-chip-labels"> <div class="tb-chip-labels">
<div class="tb-chip-label"> <div class="tb-chip-label">
<ng-container *ngIf="datasourceType === datasourceTypes.entity"> <ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-mat-20 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm" <mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}" matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon> matTooltipPosition="above">notifications</mat-icon>
<mat-icon class="tb-mat-20 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.attribute" <mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.attribute"
matTooltip="{{'datakey.attributes' | translate }}" matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"></mat-icon> matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"></mat-icon>
<mat-icon class="tb-mat-20 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.entityField" <mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.entityField"
matTooltip="{{'datakey.entity-field' | translate }}" matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"></mat-icon> matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"></mat-icon>
<mat-icon class="tb-mat-20 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.timeseries" <mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.timeseries"
matTooltip="{{'datakey.timeseries' | translate }}" matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above">timeline</mat-icon> matTooltipPosition="above">timeline</mat-icon>
</ng-container> </ng-container>
{{key.label}} <span *ngIf="!hideDataKeyLabel && key.label !== key.name">{{key.label}}</span>
</div> </div>
<div class="tb-chip-separator">: </div> <div *ngIf="!hideDataKeyLabel && key.label !== key.name" class="tb-chip-separator">: </div>
<div class="tb-chip-label"> <div class="tb-chip-label">
<strong> <strong>
<ng-container *ngTemplateOutlet="keyName; context:{key: key}"></ng-container> <ng-container *ngTemplateOutlet="keyName; context:{key: key}"></ng-container>
</strong> </strong>
</div> </div>
</div> </div>
<div *ngIf="!hideDataKeyColor" style="padding: 3px;">
<div class="tb-color-preview small box" (click)="showColorPicker(key)">
<div class="tb-color-result" [ngStyle]="{background: key.color}"></div>
</div>
</div>
<button *ngIf="!disabled" <button *ngIf="!disabled"
type="button" type="button"
(click)="editDataKey(key, $index)" mat-icon-button class="tb-mat-32"> (click)="editDataKey(key, $index)" mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-20">edit</mat-icon> <mat-icon class="tb-mat-18">edit</mat-icon>
</button>
<button matChipRemove *ngIf="!disabled && !isCountDatasource"
type="button"
mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">close</mat-icon>
</button> </button>
<mat-icon matChipRemove *ngIf="!disabled && !isCountDatasource">close</mat-icon>
</div> </div>
</mat-chip-row> </mat-chip-row>
<input matInput type="text" placeholder="{{ !disabled ? (keys.length ? secondaryPlaceholder : placeholder) : '' }}" <input matInput type="text" placeholder="{{ !disabled && (!maxDataKeysSet || keys.length < maxDataKeys) ? secondaryPlaceholder : '' }}"
[ngClass]="{'tb-dragging': dragIndex > -1}" [ngClass]="{'tb-dragging': dragIndex > -1}"
style="max-width: 200px;" style="max-width: 200px;"
#keyInput #keyInput
formControlName="key" formControlName="key"
matAutocompleteOrigin matAutocompleteOrigin
[readonly]="isCountDatasource" [readonly]="inputDisabled"
#origin="matAutocompleteOrigin" #origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin" [matAutocompleteConnectedTo]="origin"
(focusin)="onFocus()" (focusin)="onFocus()"
(drop)="$event.preventDefault();" (drop)="$event.preventDefault();"
[matAutocomplete]="keyAutocomplete" [matAutocomplete]="keyAutocomplete"
[matAutocompleteDisabled]="isCountDatasource" [matAutocompleteDisabled]="inputDisabled"
[matChipInputFor]="chipList" [matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="add($event)"> (matChipInputTokenEnd)="add($event)">
@ -105,7 +112,7 @@
[displayWith]="displayKeyFn"> [displayWith]="displayKeyFn">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key"> <mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<span style="white-space: nowrap;"> <span style="white-space: nowrap;">
<ng-container *ngIf="datasourceType === datasourceTypes.entity"> <ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm" <mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}" matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon> matTooltipPosition="above">notifications</mat-icon>
@ -132,7 +139,7 @@
{{ translate.get('entity.no-key-matching', {{ translate.get('entity.no-key-matching',
{key: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }} {key: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span> </span>
<span *ngIf="datasourceType !== datasourceTypes.entity; else createEntityKey"> <span *ngIf="!isEntityDatasource; else createEntityKey">
<a translate (click)="createKey(searchText)">entity.create-new-key</a> <a translate (click)="createKey(searchText)">entity.create-new-key</a>
</span> </span>
<ng-template #createEntityKey> <ng-template #createEntityKey>
@ -162,7 +169,7 @@
{{ requiredText }} {{ requiredText }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<div class="mat-caption" style="color: rgba(0,0,0,0.57);" *ngIf="maxDataKeys !== null && maxDataKeys > -1"> <div class="mat-caption" style="color: rgba(0,0,0,0.57);" *ngIf="maxDataKeysSet && maxDataKeys > 1">
{{ maxDataKeysText() }} {{ maxDataKeysText() }}
</div> </div>
<ng-template #keyName let-key="key"> <ng-template #keyName let-key="key">

1
ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts → ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts

@ -21,4 +21,5 @@ import { Observable } from 'rxjs';
export interface DataKeysCallbacks { export interface DataKeysCallbacks {
generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => DataKey; generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => DataKey;
fetchEntityKeys: (entityAliasId: string, types: Array<DataKeyType>) => Observable<Array<DataKey>>; fetchEntityKeys: (entityAliasId: string, types: Array<DataKeyType>) => Observable<Array<DataKey>>;
fetchEntityKeysForDevice: (deviceId: string, types: Array<DataKeyType>) => Observable<Array<DataKey>>;
} }

28
ui-ngx/src/app/modules/home/components/widget/data-keys.component.scss → ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss

@ -28,6 +28,13 @@
line-height: 20px; line-height: 20px;
height: 32px; height: 32px;
&.mdc-evolution-chip--with-trailing-action {
.mdc-evolution-chip__action--primary {
padding-left: 4px;
padding-right: 12px;
}
}
.mat-mdc-chip-action { .mat-mdc-chip-action {
overflow: hidden; overflow: hidden;
.mat-mdc-chip-action-label { .mat-mdc-chip-action-label {
@ -37,15 +44,12 @@
.tb-attribute-chip { .tb-attribute-chip {
max-width: 100%; max-width: 100%;
color: rgb(66, 66, 66); color: rgb(66, 66, 66);
font-weight: normal;
font-size: 16px;
.tb-chip-drag-handle { .tb-chip-drag-handle {
padding: 3px;
height: 24px;
cursor: move; cursor: move;
mat-icon { mat-icon {
pointer-events: none; pointer-events: none;
margin-right: 4px;
margin-left: 4px;
vertical-align: bottom;
} }
} }
.tb-chip-labels { .tb-chip-labels {
@ -53,7 +57,13 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
min-width: 0; min-width: 0;
background: rgba(0, 0, 0, 0.04);
border-radius: 100px;
padding: 2px 10px;
.tb-chip-label { .tb-chip-label {
font-weight: normal;
font-size: 14px;
line-height: 20px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -70,15 +80,9 @@
white-space: pre; white-space: pre;
} }
} }
.mat-mdc-chip-remove.mat-icon { .mat-mdc-chip-remove.mat-mdc-icon-button {
width: 24px;
min-width: 24px;
height: 24px;
font-size: 24px;
margin-right: 4px;
color: inherit; color: inherit;
opacity: inherit; opacity: inherit;
padding-left: 0;
} }
} }

193
ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts → ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts

@ -30,6 +30,7 @@ import {
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { import {
AbstractControl,
ControlValueAccessor, ControlValueAccessor,
FormGroupDirective, FormGroupDirective,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
@ -37,14 +38,14 @@ import {
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
Validators ValidationErrors
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators'; import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MatAutocomplete } from '@angular/material/autocomplete'; import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { MatChipGrid, MatChipInputEvent, MatChipRow } from '@angular/material/chips'; import { MatChipGrid, MatChipInputEvent, MatChipRow } from '@angular/material/chips';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -60,12 +61,14 @@ import { MatDialog } from '@angular/material/dialog';
import { import {
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
DataKeyConfigDialogData DataKeyConfigDialogData
} from '@home/components/widget/data-key-config-dialog.component'; } from '@home/components/widget/config/data-key-config-dialog.component';
import { deepClone, guid, isUndefined } from '@core/utils'; import { deepClone, guid, isDefinedAndNotNull, isUndefined } from '@core/utils';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { AggregationType } from '@shared/models/time/time.models'; import { AggregationType } from '@shared/models/time/time.models';
import { DndDropEvent } from 'ngx-drag-drop/lib/dnd-dropzone.directive'; import { DndDropEvent } from 'ngx-drag-drop/lib/dnd-dropzone.directive';
import { moveItemInArray } from '@angular/cdk/drag-drop'; import { moveItemInArray } from '@angular/cdk/drag-drop';
import { coerceBoolean } from '@shared/decorators/coercion';
import { DatasourceComponent } from '@home/components/widget/config/datasource.component';
@Component({ @Component({
selector: 'tb-data-keys', selector: 'tb-data-keys',
@ -76,16 +79,32 @@ import { moveItemInArray } from '@angular/cdk/drag-drop';
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DataKeysComponent), useExisting: forwardRef(() => DataKeysComponent),
multi: true multi: true
}, } /*,
{ {
provide: ErrorStateMatcher, provide: ErrorStateMatcher,
useExisting: DataKeysComponent useExisting: DataKeysComponent
} } */
], ],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChanges, ErrorStateMatcher { export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChanges, ErrorStateMatcher {
public get hideDataKeyLabel(): boolean {
return this.datasourceComponent.hideDataKeyLabel;
}
public get hideDataKeyColor(): boolean {
return this.datasourceComponent.hideDataKeyColor;
}
public get hideDataKeyUnits(): boolean {
return this.datasourceComponent.hideDataKeyUnits;
}
public get hideDataKeyDecimals(): boolean {
return this.datasourceComponent.hideDataKeyDecimals;
}
datasourceTypes = DatasourceType; datasourceTypes = DatasourceType;
widgetTypes = widgetType; widgetTypes = widgetType;
dataKeyTypes = DataKeyType; dataKeyTypes = DataKeyType;
@ -113,6 +132,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
@Input() @Input()
optDataKeys: boolean; optDataKeys: boolean;
@Input()
@coerceBoolean()
simpleDataKeysLabel = false;
@Input() @Input()
aliasController: IAliasController; aliasController: IAliasController;
@ -134,6 +157,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
@Input() @Input()
entityAliasId: string; entityAliasId: string;
@Input()
deviceId: string;
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
return this.requiredValue || !this.optDataKeys || this.isCountDatasource; return this.requiredValue || !this.optDataKeys || this.isCountDatasource;
@ -148,6 +174,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
@ViewChild('keyInput') keyInput: ElementRef<HTMLInputElement>; @ViewChild('keyInput') keyInput: ElementRef<HTMLInputElement>;
@ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete; @ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete;
@ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger;
@ViewChild('chipList') chipList: MatChipGrid; @ViewChild('chipList') chipList: MatChipGrid;
keys: Array<DataKey> = []; keys: Array<DataKey> = [];
@ -178,6 +205,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private datasourceComponent: DatasourceComponent,
public translate: TranslateService, public translate: TranslateService,
private utils: UtilsService, private utils: UtilsService,
private dialogs: DialogService, private dialogs: DialogService,
@ -189,10 +217,19 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
} }
updateValidators() { updateValidators() {
this.keysListFormGroup.get('keys').setValidators(this.required ? [Validators.required] : []); this.keysListFormGroup.get('keys').setValidators(this.required ? [this.keysRequired] : []);
this.keysListFormGroup.get('keys').updateValueAndValidity(); this.keysListFormGroup.get('keys').updateValueAndValidity();
} }
keysRequired(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (value && Array.isArray(value) && value.length) {
return null;
} else {
return {required: true};
}
}
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
} }
@ -202,7 +239,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
ngOnInit() { ngOnInit() {
this.keysListFormGroup = this.fb.group({ this.keysListFormGroup = this.fb.group({
keys: [null, this.required ? [Validators.required] : []], keys: [null, this.required ? [this.keysRequired] : []],
key: [null] key: [null]
}); });
this.alarmKeys = []; this.alarmKeys = [];
@ -251,37 +288,40 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
} }
private updateParams() { private updateParams() {
if (this.datasourceType === DatasourceType.function) { const singleKey = this.maxDataKeysSet && this.maxDataKeys === 1;
this.dataKeyType = DataKeyType.function; this.secondaryPlaceholder = '+' + this.translate.instant('action.add');
this.requiredText = this.translate.instant('datakey.function-types-required'); if (this.datasourceType === DatasourceType.function) {
if (this.widgetType === widgetType.latest) { this.dataKeyType = DataKeyType.function;
this.placeholder = this.translate.instant('datakey.latest-key-functions'); this.requiredText = this.translate.instant('datakey.function-types-required');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.latest-key-function'); if (this.widgetType === widgetType.latest) {
} else if (this.widgetType === widgetType.alarm) { this.placeholder = this.translate.instant(singleKey ? 'datakey.latest-key-function' : 'datakey.latest-key-functions');
this.placeholder = this.translate.instant('datakey.alarm-key-functions'); } else if (this.widgetType === widgetType.alarm) {
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.alarm-key-function'); this.placeholder = this.translate.instant(singleKey ? 'datakey.alarm-key-function' : 'datakey.alarm-key-functions');
} else { } else {
this.placeholder = this.translate.instant('datakey.timeseries-key-functions'); this.placeholder = this.translate.instant(singleKey ? 'datakey.timeseries-key-function' : 'datakey.timeseries-key-functions');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.timeseries-key-function'); }
} } else {
if (this.widgetType !== widgetType.latest && this.widgetType !== widgetType.alarm) {
this.dataKeyType = DataKeyType.timeseries;
} else {
this.dataKeyType = null;
}
if (this.simpleDataKeysLabel && this.widgetType !== widgetType.alarm) {
this.placeholder = this.translate.instant(singleKey ? 'datakey.data-key' : 'datakey.data-keys');
this.requiredText = this.translate.instant(singleKey ? 'datakey.data-key-required' : 'datakey.data-keys-required');
} else { } else {
if (this.widgetType === widgetType.latest) { if (this.widgetType === widgetType.latest) {
this.dataKeyType = null; this.placeholder = this.translate.instant(singleKey ? 'datakey.latest-key' : 'datakey.latest-keys');
this.placeholder = this.translate.instant('datakey.latest-keys');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.latest-key');
this.requiredText = this.translate.instant('datakey.timeseries-or-attributes-required'); this.requiredText = this.translate.instant('datakey.timeseries-or-attributes-required');
} else if (this.widgetType === widgetType.alarm) { } else if (this.widgetType === widgetType.alarm) {
this.dataKeyType = null; this.placeholder = this.translate.instant(singleKey ? 'datakey.alarm-key' : 'datakey.alarm-keys');
this.placeholder = this.translate.instant('datakey.alarm-keys');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.alarm-key');
this.requiredText = this.translate.instant('datakey.alarm-fields-timeseries-or-attributes-required'); this.requiredText = this.translate.instant('datakey.alarm-fields-timeseries-or-attributes-required');
} else { } else {
this.dataKeyType = DataKeyType.timeseries; this.placeholder = this.translate.instant(singleKey ? 'datakey.timeseries-key' : 'datakey.timeseries-keys');
this.placeholder = this.translate.instant('datakey.timeseries-keys');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.timeseries-key');
this.requiredText = this.translate.instant('datakey.timeseries-required'); this.requiredText = this.translate.instant('datakey.timeseries-required');
} }
} }
}
} }
private reset() { private reset() {
@ -307,16 +347,22 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
for (const propName of Object.keys(changes)) { for (const propName of Object.keys(changes)) {
const change = changes[propName]; const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) { if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'entityAliasId') { if (['deviceId', 'entityAliasId'].includes(propName)) {
this.clearSearchCache(); this.clearSearchCache();
this.dirty = true; this.dirty = true;
} else if (['widgetType', 'datasourceType'].includes(propName)) { } else if (['widgetType', 'datasourceType', 'maxDataKeys', 'simpleDataKeysLabel'].includes(propName)) {
this.clearSearchCache(); if (propName === 'datasourceType' &&
this.updateParams(); [DatasourceType.device, DatasourceType.entity].includes(change.previousValue) &&
setTimeout(() => { [DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) {
this.reset(); this.clearSearchCache();
}, 1); } else {
} else if (['required', 'optDataKeys'].includes('propName')) { this.clearSearchCache();
this.updateParams();
setTimeout(() => {
this.reset();
}, 1);
}
} else if (['required', 'optDataKeys'].includes(propName)) {
this.updateValidators(); this.updateValidators();
} }
} }
@ -325,7 +371,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = this.required && !this.modelValue; const customErrorState = this.required && (!this.modelValue || !this.modelValue.length);
return originalErrorState || customErrorState; return originalErrorState || customErrorState;
} }
@ -365,7 +411,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
} }
addKey(key: DataKey): void { addKey(key: DataKey): void {
if (!this.maxDataKeys || this.maxDataKeys < 0 || if (!this.maxDataKeysSet ||
!this.modelValue || this.modelValue.length < this.maxDataKeys) { !this.modelValue || this.modelValue.length < this.maxDataKeys) {
if (!this.modelValue) { if (!this.modelValue) {
this.modelValue = []; this.modelValue = [];
@ -375,17 +421,17 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
this.keysListFormGroup.get('keys').setValue(this.keys); this.keysListFormGroup.get('keys').setValue(this.keys);
} }
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
this.clear(); const focus = !this.maxDataKeysSet || this.modelValue.length < this.maxDataKeys;
this.clear('', focus);
} }
add(event: MatChipInputEvent): void { add(event: MatChipInputEvent): void {
const value = event.value; const value = event.value;
if ((value || '').trim()) { if ((value || '').trim() && this.dataKeyType) {
if (this.dataKeyType) { this.addFromChipValue({name: value.trim(), type: this.dataKeyType});
this.addFromChipValue({name: value.trim(), type: this.dataKeyType}); } else {
} this.clear();
} }
this.clear();
} }
remove(key: DataKey) { remove(key: DataKey) {
@ -402,8 +448,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
} }
} }
chipDragStart(index: number, chipRow: MatChipRow, placeholderChipRow: MatChipRow) { chipDragStart(index: number, chipRow: MatChipRow, placeholderChipRow: Element) {
this.renderer.setStyle(placeholderChipRow._elementRef.nativeElement, 'width', chipRow._elementRef.nativeElement.offsetWidth + 'px'); this.autocomplete.closePanel();
this.renderer.setStyle(placeholderChipRow,
'width', chipRow._elementRef.nativeElement.offsetWidth + 'px');
this.dragIndex = index; this.dragIndex = index;
} }
@ -447,9 +495,14 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
aliasController: this.aliasController, aliasController: this.aliasController,
widget: this.widget, widget: this.widget,
widgetType: this.widgetType, widgetType: this.widgetType,
deviceId: this.deviceId,
entityAliasId: this.entityAliasId, entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm, showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks callbacks: this.callbacks,
hideDataKeyLabel: this.hideDataKeyLabel,
hideDataKeyColor: this.hideDataKeyColor,
hideDataKeyUnits: this.hideDataKeyUnits,
hideDataKeyDecimals: this.hideDataKeyDecimals
} }
}).afterClosed().subscribe((updatedDataKey) => { }).afterClosed().subscribe((updatedDataKey) => {
if (updatedDataKey) { if (updatedDataKey) {
@ -496,7 +549,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
fetchObservable = of(targetKeysList); fetchObservable = of(targetKeysList);
} else if (this.datasourceType === DatasourceType.entity && this.entityAliasId) { } else if (this.datasourceType === DatasourceType.entity && this.entityAliasId ||
this.datasourceType === DatasourceType.device && this.deviceId) {
const dataKeyTypes = [DataKeyType.timeseries]; const dataKeyTypes = [DataKeyType.timeseries];
if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) { if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.attribute); dataKeyTypes.push(DataKeyType.attribute);
@ -505,7 +559,11 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
dataKeyTypes.push(DataKeyType.alarm); dataKeyTypes.push(DataKeyType.alarm);
} }
} }
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes); if (this.datasourceType === DatasourceType.device) {
fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes);
} else {
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes);
}
} else { } else {
fetchObservable = of([]); fetchObservable = of([]);
} }
@ -523,22 +581,41 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
} }
textIsNotEmpty(text: string): boolean { textIsNotEmpty(text: string): boolean {
return (text && text != null && text.length > 0) ? true : false; return text && text.length > 0;
} }
clear(value: string = '') { clear(value: string = '', focus = true) {
this.autocomplete.closePanel();
this.keyInput.nativeElement.value = value; this.keyInput.nativeElement.value = value;
this.keysListFormGroup.get('key').patchValue(value, {emitEvent: true}); this.keysListFormGroup.get('key').patchValue(value, {emitEvent: focus});
setTimeout(() => { if (focus) {
this.keyInput.nativeElement.blur(); setTimeout(() => {
this.keyInput.nativeElement.focus(); this.keyInput.nativeElement.blur();
}, 0); this.keyInput.nativeElement.focus();
}, 0);
}
} }
get isCountDatasource(): boolean { get isCountDatasource(): boolean {
return [DatasourceType.entityCount, DatasourceType.alarmCount].includes(this.datasourceType); return [DatasourceType.entityCount, DatasourceType.alarmCount].includes(this.datasourceType);
} }
get isEntityDatasource(): boolean {
return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType);
}
get inputDisabled(): boolean {
return this.isCountDatasource || (this.maxDataKeysSet && this.keys.length >= this.maxDataKeys);
}
get dragDisabled(): boolean {
return this.keys.length < 2;
}
get maxDataKeysSet(): boolean {
return isDefinedAndNotNull(this.maxDataKeysValue) && this.maxDataKeysValue > -1;
}
private clearSearchCache() { private clearSearchCache() {
this.searchText = ''; this.searchText = '';
this.fetchObservable$ = null; this.fetchObservable$ = null;

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

@ -0,0 +1,103 @@
<!--
Copyright © 2016-2023 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.
-->
<section class="tb-datasource-section" [formGroup]="datasourceFormGroup">
<mat-form-field *ngIf="!basicMode" class="tb-datasource-type" hideRequiredMarker>
<mat-label translate>widget-config.datasource-type</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
{{ datasourceTypesTranslations.get(datasourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<section fxLayout="column" [ngSwitch]="datasourceFormGroup.get('type').value">
<ng-template [ngSwitchCase]="datasourceType.function">
<mat-form-field fxFlex>
<mat-label translate>datasource.label</mat-label>
<input matInput
formControlName="name">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="datasourceFormGroup.get('type').value === datasourceType.device ||
datasourceFormGroup.get('type').value === datasourceType.entity ||
datasourceFormGroup.get('type').value === datasourceType.entityCount ||
datasourceFormGroup.get('type').value === datasourceType.alarmCount ? datasourceFormGroup.get('type').value : ''">
<tb-alarm-filter-config *ngIf="datasourceFormGroup.get('type').value === datasourceType.alarmCount"
propagatedFilter="false"
style="height: 56px; margin-bottom: 22px;"
formControlName="alarmFilterConfig"></tb-alarm-filter-config>
<tb-entity-autocomplete *ngIf="datasourceFormGroup.get('type').value === datasourceType.device"
required
[entityType]="entityType.DEVICE"
formControlName="deviceId">
</tb-entity-autocomplete>
<tb-entity-alias-select
*ngIf="datasourceFormGroup.get('type').value !== datasourceType.device && datasourceFormGroup.get('type').value !== datasourceType.alarmCount"
[showLabel]="true"
[tbRequired]="true"
[aliasController]="aliasController"
formControlName="entityAliasId"
[callbacks]="entityAliasSelectCallbacks">
</tb-entity-alias-select>
<mat-form-field *ngIf="[datasourceType.entityCount, datasourceType.alarmCount].includes(datasourceFormGroup.get('type').value)"
fxFlex>
<input matInput
placeholder="{{ 'datasource.label' | translate }}"
formControlName="name">
</mat-form-field>
</ng-template>
</section>
<section fxLayout="column" fxLayoutAlign="stretch" fxFlex>
<tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType"
[datasourceType]="datasourceFormGroup.get('type').value"
[maxDataKeys]="maxDataKeys"
[optDataKeys]="isDataKeysOptional(datasourceFormGroup.get('type').value)"
[simpleDataKeysLabel]="!hasAdditionalLatestDataKeys"
[aliasController]="aliasController"
[datakeySettingsSchema]="dataKeySettingsSchema"
[dataKeySettingsDirective]="dataKeySettingsDirective"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="dataKeysCallbacks"
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value"
[deviceId]="datasourceFormGroup.get('deviceId').value"
formControlName="dataKeys">
</tb-data-keys>
<tb-data-keys *ngIf="hasAdditionalLatestDataKeys" class="tb-data-keys" fxFlex
[widgetType]="widgetTypes.latest"
[datasourceType]="datasourceFormGroup.get('type').value"
[optDataKeys]="true"
[aliasController]="aliasController"
[datakeySettingsSchema]="latestDataKeySettingsSchema"
[dataKeySettingsDirective]="latestDataKeySettingsDirective"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="dataKeysCallbacks"
[entityAliasId]="datasourceFormGroup.get('entityAliasId').value"
[deviceId]="datasourceFormGroup.get('deviceId').value"
formControlName="latestDataKeys">
</tb-data-keys>
</section>
<tb-filter-select
*ngIf="!basicMode && ![datasourceType.function, datasourceType.alarmCount].includes(datasourceFormGroup.get('type').value)"
[showLabel]="true"
[aliasController]="aliasController"
formControlName="filterId"
[callbacks]="filterSelectCallbacks">
</tb-filter-select>
</section>

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

@ -14,9 +14,8 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models'; import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models';
import { DataKeysCallbacks } from './data-keys.component.models';
import { WidgetActionCallbacks } from './action/manage-widget-actions.component.models';
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models'; import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & FilterSelectCallbacks & DataKeysCallbacks & WidgetActionCallbacks; export type DatasourceCallbacks = EntityAliasSelectCallbacks & FilterSelectCallbacks & DataKeysCallbacks;

33
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.scss

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2023 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-datasource-section {
display: flex;
flex-direction: column;
align-items: stretch;
flex: 1;
}
:host ::ng-deep {
.tb-datasource-section {
tb-alarm-filter-config {
.mdc-button {
width: 100%;
height: 100%;
justify-content: flex-start;
}
}
}
}

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

@ -0,0 +1,275 @@
///
/// Copyright © 2016-2023 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, forwardRef, Input, OnInit, Optional } from '@angular/core';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator,
Validators
} from '@angular/forms';
import {
Datasource,
DatasourceType,
datasourceTypeTranslationMap,
JsonSettingsSchema,
Widget, WidgetConfigMode,
widgetType
} from '@shared/models/widget.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { IAliasController } from '@core/api/widget-api.models';
import { EntityAliasSelectCallbacks } from '@home/components/alias/entity-alias-select.component.models';
import { FilterSelectCallbacks } from '@home/components/filter/filter-select.component.models';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DatasourcesComponent } from '@home/components/widget/config/datasources.component';
@Component({
selector: 'tb-datasource',
templateUrl: './datasource.component.html',
styleUrls: ['./datasource.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatasourceComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DatasourceComponent),
multi: true,
}
]
})
export class DatasourceComponent implements ControlValueAccessor, OnInit, Validator {
public get basicMode(): boolean {
return !this.widgetConfigComponent.widgetEditMode && this.widgetConfigComponent.widgetConfigMode === WidgetConfigMode.basic;
}
public get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType;
}
public get aliasController(): IAliasController {
return this.widgetConfigComponent.aliasController;
}
public get entityAliasSelectCallbacks(): EntityAliasSelectCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}
public get filterSelectCallbacks(): FilterSelectCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}
public get dataKeysCallbacks(): DataKeysCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}
public get hasAdditionalLatestDataKeys(): boolean {
return this.widgetConfigComponent.widgetType === widgetType.timeseries &&
this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys;
}
public get dataKeysOptional(): boolean {
return this.widgetConfigComponent.modelValue?.typeParameters?.dataKeysOptional;
}
public get maxDataKeys(): number {
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys;
}
public get dataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
}
public get dataKeySettingsDirective(): string {
return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective;
}
public get latestDataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsSchema;
}
public get latestDataKeySettingsDirective(): string {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsDirective;
}
public get dashboard(): Dashboard {
return this.widgetConfigComponent.dashboard;
}
public get widget(): Widget {
return this.widgetConfigComponent.widget;
}
public get hideDataKeyLabel(): boolean {
return this.datasourcesComponent?.hideDataKeyLabel;
}
public get hideDataKeyColor(): boolean {
return this.datasourcesComponent?.hideDataKeyColor;
}
public get hideDataKeyUnits(): boolean {
return this.datasourcesComponent?.hideDataKeyUnits;
}
public get hideDataKeyDecimals(): boolean {
return this.datasourcesComponent?.hideDataKeyDecimals;
}
@Input()
disabled: boolean;
widgetTypes = widgetType;
entityType = EntityType;
datasourceType = DatasourceType;
datasourceTypes: Array<DatasourceType> = [];
datasourceTypesTranslations = datasourceTypeTranslationMap;
datasourceFormGroup: UntypedFormGroup;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
@Optional()
private datasourcesComponent: DatasourcesComponent,
private widgetConfigComponent: WidgetConfigComponent) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (!this.datasourceFormGroup.valid) {
setTimeout(() => {
this.datasourceUpdated(this.datasourceFormGroup.value);
}, 0);
}
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.datasourceFormGroup.disable({emitEvent: false});
} else {
this.datasourceFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
ngOnInit() {
if (this.widgetConfigComponent.functionsOnly) {
this.datasourceTypes = [DatasourceType.function];
} else {
this.datasourceTypes = [DatasourceType.function, DatasourceType.device, DatasourceType.entity];
if (this.widgetConfigComponent.widgetType === widgetType.latest) {
this.datasourceTypes.push(DatasourceType.entityCount);
this.datasourceTypes.push(DatasourceType.alarmCount);
}
}
this.datasourceFormGroup = this.fb.group(
{
type: [null, [Validators.required]],
name: [null, []],
deviceId: [null, []],
entityAliasId: [null, []],
filterId: [null, []],
dataKeys: [null, []],
alarmFilterConfig: [null, []]
}
);
if (this.hasAdditionalLatestDataKeys) {
this.datasourceFormGroup.addControl('latestDataKeys', this.fb.control(null));
}
this.datasourceFormGroup.get('type').valueChanges.subscribe(() => {
this.updateValidators();
});
this.datasourceFormGroup.valueChanges.subscribe(
() => {
this.datasourceUpdated(this.datasourceFormGroup.value);
}
);
}
writeValue(datasource?: Datasource): void {
this.datasourceFormGroup.patchValue({
type: datasource?.type,
name: datasource?.name,
deviceId: datasource?.deviceId,
entityAliasId: datasource?.entityAliasId,
filterId: datasource?.filterId,
dataKeys: datasource?.dataKeys,
alarmFilterConfig: datasource?.alarmFilterConfig ?
datasource?.alarmFilterConfig : { statusList: [AlarmSearchStatus.ACTIVE] }
}, {emitEvent: false});
if (this.hasAdditionalLatestDataKeys) {
this.datasourceFormGroup.patchValue({
latestDataKeys: datasource?.latestDataKeys
}, {emitEvent: false});
}
this.updateValidators();
}
validate(c: UntypedFormControl) {
return (this.datasourceFormGroup.valid) ? null : {
datasource: {
valid: false,
},
};
}
public isDataKeysOptional(type?: DatasourceType): boolean {
if (this.hasAdditionalLatestDataKeys) {
return true;
} else {
return this.dataKeysOptional
&& type !== DatasourceType.entityCount && type !== DatasourceType.alarmCount;
}
}
private datasourceUpdated(datasource: Datasource) {
this.propagateChange(datasource);
}
private updateValidators() {
const type: DatasourceType = this.datasourceFormGroup.get('type').value;
this.datasourceFormGroup.get('deviceId').setValidators(
type === DatasourceType.device ? [Validators.required] : []
);
this.datasourceFormGroup.get('entityAliasId').setValidators(
(type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : []
);
const newDataKeysRequired = !this.isDataKeysOptional(type);
this.datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []);
this.datasourceFormGroup.get('deviceId').updateValueAndValidity({emitEvent: false});
this.datasourceFormGroup.get('entityAliasId').updateValueAndValidity({emitEvent: false});
this.datasourceFormGroup.get('dataKeys').updateValueAndValidity({emitEvent: false});
}
}

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

@ -0,0 +1,101 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="datasourcesFormGroup" class="tb-widget-config-panel tb-datasources" [ngClass]="{'no-padding-bottom': !showAddDatasource}">
<div fxLayout="column">
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-widget-config-panel-title">{{ (singleDatasource ? 'widget-config.datasource' : 'widget-config.datasources') | translate }}</div>
<div fxFlex fxLayout="row" fxLayoutAlign="center center" *ngIf="timeseriesKeyError">
<mat-error >{{ 'widget-config.timeseries-key-error' | translate }}</mat-error>
</div>
<tb-toggle-header *ngIf="basicMode" (valueChange)="datasourcesModeChange($event)" ignoreMdLgSize="true"
[options]="[
{
name: translate.instant('device.device'),
value: datasourceType.device
},
{
name: translate.instant('entity.entity-alias'),
value: datasourceType.entity
}
]" [value]="datasourcesMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
</div>
<div *ngIf="maxDatasources > 1"
class="tb-widget-config-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}</div>
</div>
<div *ngIf="datasourcesFormArray.length === 0; else datasourcesTemplate">
<span translate fxLayoutAlign="center center"
class="tb-prompt">datasource.add-datasource-prompt</span>
</div>
<ng-template #datasourcesTemplate>
<div style="overflow: auto;">
<mat-list class="tb-drop-list"
cdkDropList
cdkDropListOrientation="vertical"
(cdkDropListDropped)="onDatasourceDrop($event)"
[cdkDropListDisabled]="dragDisabled"
formArrayName="datasources">
<mat-list-item *ngFor="let datasourceControl of datasourcesControls; trackBy: trackByDatasource; let $index = index;"
class="tb-datasource-list-item tb-draggable" [ngClass]="{'bordered': !singleDatasource}" cdkDrag
[cdkDragDisabled]="dragDisabled">
<div fxFlex fxLayout="row" fxLayoutAlign="start start">
<div *ngIf="!singleDatasource" fxLayout="row" fxLayoutAlign="center center" style="width: 42px; height: 56px;">
<div class="tb-datasource-index">{{$index + 1}}</div>
</div>
<div class="tb-datasource-params" fxFlex>
<div fxFlex
fxLayout="row"
fxLayoutAlign="start">
<tb-datasource fxFlex
[formControl]="datasourceControl">
</tb-datasource>
<div *ngIf="!singleDatasource && !disabled" fxLayout="row" fxLayoutAlign="center center" style="height: 56px;">
<button type="button"
mat-icon-button color="primary"
(click)="removeDatasource($index)"
matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button *ngIf="!dragDisabled"
mat-icon-button color="primary"
type="button"
cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
<tb-error class="tb-datasource-error" [error]="datasourceError[$index] ? datasourceError[$index] : ''"></tb-error>
</div>
</div>
</mat-list-item>
</mat-list>
</div>
</ng-template>
<div *ngIf="showAddDatasource">
<button type="button"
mat-stroked-button color="primary"
(click)="addDatasource()"
matTooltip="{{ 'widget-config.add-datasource' | translate }}"
matTooltipPosition="above">
<span translate>widget-config.add-datasource</span>
</button>
</div>
</div>

83
ui-ngx/src/app/modules/home/components/widget/config/datasources.component.scss

@ -0,0 +1,83 @@
/**
* Copyright © 2016-2023 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 "../../../../../../theme";
.tb-datasource-list-item {
&.mat-mdc-list-item {
height: auto;
display: block;
padding: 0;
&.bordered {
padding-top: 16px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
}
}
&.tb-draggable {
&.cdk-drag-preview {
background: #fff;
}
}
}
.tb-datasource-list-item + .tb-datasource-list-item {
margin-top: 16px;
}
.tb-datasource-index {
width: 24px;
height: 24px;
position: relative;
font-weight: 400;
font-size: 16px;
text-align: center;
color: $tb-primary-color;
&:before {
content: "";
display: block;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background-color: $tb-primary-color;
opacity: 0.06;
border-radius: 100%;
}
}
.tb-datasource-params {
position: relative;
tb-error.tb-datasource-error {
position: absolute;
bottom: 4px;
left: 8px;
}
}
:host {
.tb-datasources {
.handle {
cursor: move;
}
.mat-mdc-list {
min-height: 68px;
padding-left: 0;
}
}
}

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

@ -0,0 +1,329 @@
///
/// Copyright © 2016-2023 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, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator
} from '@angular/forms';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import {
Datasource,
DatasourceType,
JsonSettingsSchema,
WidgetConfigMode,
widgetType
} from '@shared/models/widget.models';
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 } from '@home/components/widget/config/data-keys.component.models';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-datasources',
templateUrl: './datasources.component.html',
styleUrls: ['./datasources.component.scss', './widget-config.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DatasourcesComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DatasourcesComponent),
multi: true,
}
]
})
export class DatasourcesComponent implements ControlValueAccessor, OnInit, Validator, OnChanges {
datasourceType = DatasourceType;
public get basicMode(): boolean {
return !this.widgetConfigComponent.widgetEditMode && this.configMode === WidgetConfigMode.basic;
}
public get maxDatasources(): number {
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDatasources;
}
public get singleDatasource(): boolean {
return this.maxDatasources === 1;
}
public get showAddDatasource(): boolean {
return this.widgetConfigComponent.modelValue?.typeParameters &&
(this.maxDatasources === -1 || this.datasourcesFormArray.length < this.maxDatasources);
}
public get dragDisabled(): boolean {
return this.disabled || this.singleDatasource || this.datasourcesFormArray.length < 2;
}
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
hideDataKeyLabel = false;
@Input()
@coerceBoolean()
hideDataKeyColor = false;
@Input()
@coerceBoolean()
hideDataKeyUnits = false;
@Input()
@coerceBoolean()
hideDataKeyDecimals = false;
@Input()
configMode: WidgetConfigMode;
datasourcesFormGroup: UntypedFormGroup;
timeseriesKeyError = false;
datasourceError: string[] = [];
datasourcesMode: DatasourceType;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private utils: UtilsService,
public translate: TranslateService,
private widgetConfigComponent: WidgetConfigComponent) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (this.validate(null)) {
setTimeout(() => {
this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value);
}, 0);
}
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.datasourcesFormGroup.disable({emitEvent: false});
} else {
this.datasourcesFormGroup.enable({emitEvent: false});
}
}
ngOnInit() {
this.datasourcesFormGroup = this.fb.group({
datasources: this.fb.array([])
});
this.datasourcesFormGroup.valueChanges.subscribe(
() => {
this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value);
}
);
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'configMode') {
this.configModeChanged();
}
}
}
}
writeValue(datasources?: Datasource[]): void {
this.datasourcesFormArray.clear({emitEvent: false});
this.datasourcesMode = this.detectDatasourcesMode(datasources);
let changed = false;
if (datasources) {
datasources.forEach((datasource) => {
if (this.basicMode && datasource.type !== this.datasourcesMode) {
datasource.type = this.datasourcesMode;
changed = true;
}
this.datasourcesFormArray.push(this.fb.control(datasource, []), {emitEvent: false});
});
}
if (this.singleDatasource && !this.datasourcesFormArray.length) {
this.addDatasource(false);
}
if (changed) {
setTimeout(() => {
this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value);
}, 0);
}
}
validate(c: UntypedFormControl) {
this.timeseriesKeyError = false;
this.datasourceError = [];
if (!this.datasourcesFormGroup.valid) {
return {
datasources: {
valid: false,
}
};
}
const datasources: Datasource[] = this.datasourcesFormGroup.get('datasources').value;
if (!this.datasourcesOptional && (!datasources || !datasources.length)) {
return {
datasources: {
valid: false
}
};
}
if (this.hasAdditionalLatestDataKeys) {
let valid = datasources.filter(datasource => datasource?.dataKeys?.length).length > 0;
if (!valid) {
this.timeseriesKeyError = true;
return {
timeseriesDataKeys: {
valid: false
}
};
} else {
const emptyDatasources = datasources.filter(datasource => !datasource?.dataKeys?.length &&
!datasource?.latestDataKeys?.length);
valid = emptyDatasources.length === 0;
if (!valid) {
for (const emptyDatasource of emptyDatasources) {
const i = datasources.indexOf(emptyDatasource);
this.datasourceError[i] = 'At least one data key should be specified';
}
return {
dataKeys: {
valid: false
}
};
}
}
}
return null;
}
datasourcesModeChange(datasourcesMode: DatasourceType) {
this.datasourcesMode = datasourcesMode;
if (this.basicMode) {
for (const datasourceControl of this.datasourcesControls) {
const datasource: Datasource = datasourceControl.value;
if (datasource.type !== datasourcesMode) {
datasource.type = datasourcesMode;
datasourceControl.patchValue(datasource);
}
}
}
}
private configModeChanged() {
if (this.basicMode) {
let datasourcesMode = this.detectDatasourcesMode(this.datasourcesFormGroup.get('datasources').value);
this.datasourcesModeChange(datasourcesMode);
}
}
private detectDatasourcesMode(datasources?: Datasource[]) {
let datasourcesMode = DatasourceType.device;
if (datasources && datasources.length) {
datasourcesMode = datasources[0].type;
}
if (datasourcesMode !== DatasourceType.device && datasourcesMode !== DatasourceType.entity) {
datasourcesMode = DatasourceType.device;
}
return datasourcesMode;
}
get datasourcesFormArray(): UntypedFormArray {
return this.datasourcesFormGroup.get('datasources') as UntypedFormArray;
}
get datasourcesControls(): FormControl[] {
return this.datasourcesFormArray.controls as FormControl[];
}
public trackByDatasource(index: number, datasourceControl: AbstractControl): any {
return datasourceControl;
}
private datasourcesUpdated(datasources: Datasource[]) {
this.propagateChange(datasources);
}
public onDatasourceDrop(event: CdkDragDrop<string[]>) {
const datasourceForm = this.datasourcesFormArray.at(event.previousIndex);
this.datasourcesFormArray.removeAt(event.previousIndex);
this.datasourcesFormArray.insert(event.currentIndex, datasourceForm);
}
public removeDatasource(index: number) {
this.datasourcesFormArray.removeAt(index);
}
public addDatasource(emitEvent = true) {
let newDatasource: Datasource;
if (this.widgetConfigComponent.functionsOnly) {
newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema));
newDatasource.dataKeys = [this.dataKeysCallbacks.generateDataKey('Sin', DataKeyType.function, this.dataKeySettingsSchema)];
} else {
const type = this.basicMode ? this.datasourcesMode : DatasourceType.entity;
newDatasource = { type,
dataKeys: []
};
}
if (this.hasAdditionalLatestDataKeys) {
newDatasource.latestDataKeys = [];
}
this.datasourcesFormArray.push(this.fb.control(newDatasource, []), {emitEvent});
}
private get dataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
}
private get dataKeysCallbacks(): DataKeysCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}
private get hasAdditionalLatestDataKeys(): boolean {
return this.widgetConfigComponent.widgetType === widgetType.timeseries &&
this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys;
}
private get datasourcesOptional(): boolean {
return this.widgetConfigComponent.modelValue?.typeParameters?.datasourcesOptional;
}
}

42
ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.html

@ -0,0 +1,42 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="timewindowConfig" class="tb-widget-config-panel">
<div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-widget-config-panel-title" translate>timewindow.timewindow</div>
<tb-toggle-header (valueChange)="timewindowConfig.get('useDashboardTimewindow').patchValue($event)" ignoreMdLgSize="true"
[options]="[
{ name: translate.instant('widget-config.use-dashboard-timewindow'), value: true},
{ name: translate.instant('widget-config.use-widget-timewindow'), value: false}
]"
[value]="timewindowConfig.get('useDashboardTimewindow').value" name="useDashboardTimewindow" useSelectOnMdLg="false">
</tb-toggle-header>
</div>
<div class="tb-widget-config-row">
<tb-timewindow asButton="true"
strokedButton
isEdit="true"
alwaysDisplayTypePrefix
[historyOnly]="onlyHistoryTimewindow"
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}"
aggregation="{{ widgetType === widgetTypes.timeseries }}"
formControlName="timewindow"></tb-timewindow>
<mat-slide-toggle class="mat-slide" formControlName="displayTimewindow">
{{ 'widget-config.display-timewindow' | translate }}
</mat-slide-toggle>
</div>
</div>

115
ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts

@ -0,0 +1,115 @@
///
/// Copyright © 2016-2023 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, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { widgetType } from '@shared/models/widget.models';
import { Timewindow } from '@shared/models/time/time.models';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
export interface TimewindowConfigData {
useDashboardTimewindow: boolean;
displayTimewindow: boolean;
timewindow: Timewindow;
}
@Component({
selector: 'tb-timewindow-config-panel',
templateUrl: './timewindow-config-panel.component.html',
styleUrls: ['./widget-config.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TimewindowConfigPanelComponent),
multi: true
}
]
})
export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnInit {
widgetTypes = widgetType;
public get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType;
}
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
onlyHistoryTimewindow = false;
timewindowConfig: UntypedFormGroup;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
public translate: TranslateService,
private widgetConfigComponent: WidgetConfigComponent) {
}
ngOnInit() {
this.timewindowConfig = this.fb.group({
useDashboardTimewindow: [null, []],
displayTimewindow: [null, []],
timewindow: [null, []]
});
this.timewindowConfig.valueChanges.subscribe(
(val) => this.propagateChange(val)
);
this.timewindowConfig.get('useDashboardTimewindow').valueChanges.subscribe(() => {
this.updateTimewindowConfigEnabledState();
});
}
writeValue(data?: TimewindowConfigData): void {
this.timewindowConfig.patchValue(data || {}, {emitEvent: false});
this.updateTimewindowConfigEnabledState();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.timewindowConfig.disable({emitEvent: false});
} else {
this.timewindowConfig.enable({emitEvent: false});
this.updateTimewindowConfigEnabledState();
}
}
private updateTimewindowConfigEnabledState() {
const useDashboardTimewindow: boolean = this.timewindowConfig.get('useDashboardTimewindow').value;
if (useDashboardTimewindow) {
this.timewindowConfig.get('displayTimewindow').disable({emitEvent: false});
this.timewindowConfig.get('timewindow').disable({emitEvent: false});
} else {
this.timewindowConfig.get('displayTimewindow').enable({emitEvent: false});
this.timewindowConfig.get('timewindow').enable({emitEvent: false});
}
}
}

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

@ -0,0 +1,70 @@
///
/// Copyright © 2016-2023 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 { NgModule } from '@angular/core';
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 { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-select.component';
import { FilterSelectComponent } from '@home/components/filter/filter-select.component';
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component';
import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.component';
import { WidgetUnitsComponent } from '@home/components/widget/config/widget-units.component';
@NgModule({
declarations:
[
AlarmAssigneeSelectComponent,
AlarmFilterConfigComponent,
DataKeysComponent,
DataKeyConfigDialogComponent,
DataKeyConfigComponent,
DatasourceComponent,
DatasourcesComponent,
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowConfigPanelComponent,
WidgetUnitsComponent,
WidgetSettingsComponent
],
imports: [
CommonModule,
SharedModule,
WidgetSettingsModule
],
exports: [
AlarmAssigneeSelectComponent,
AlarmFilterConfigComponent,
DataKeysComponent,
DataKeyConfigDialogComponent,
DataKeyConfigComponent,
DatasourceComponent,
DatasourcesComponent,
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowConfigPanelComponent,
WidgetUnitsComponent,
WidgetSettingsComponent
]
})
export class WidgetConfigComponentsModule { }

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

@ -0,0 +1,115 @@
///
/// Copyright © 2016-2023 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 { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models';
import { DatasourceCallbacks } from '@home/components/widget/config/datasource.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { Observable } from 'rxjs';
import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { WidgetConfigMode } from '@shared/models/widget.models';
export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks;
export interface IBasicWidgetConfigComponent {
widgetConfig: WidgetConfigComponentData;
widgetConfigChanged: Observable<WidgetConfigComponentData>;
validateConfig(): boolean;
}
@Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export abstract class BasicWidgetConfigComponent extends PageComponent implements
IBasicWidgetConfigComponent, OnInit, AfterViewInit {
basicMode = WidgetConfigMode.basic;
widgetConfigValue: WidgetConfigComponentData;
set widgetConfig(value: WidgetConfigComponentData) {
this.widgetConfigValue = value;
this.setupConfig(this.widgetConfigValue);
}
get widgetConfig(): WidgetConfigComponentData {
return this.widgetConfigValue;
}
widgetConfigChangedEmitter = new EventEmitter<WidgetConfigComponentData>();
widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable();
protected constructor(@Inject(Store) protected store: Store<AppState>) {
super(store);
}
ngOnInit() {}
ngAfterViewInit(): void {
setTimeout(() => {
if (!this.validateConfig()) {
this.onConfigChanged(this.prepareOutputConfig(this.configForm().value));
}
}, 0);
}
protected setupConfig(widgetConfig: WidgetConfigComponentData) {
this.onConfigSet(widgetConfig);
this.updateValidators(false);
for (const trigger of this.validatorTriggers()) {
const path = trigger.split('.');
let control: AbstractControl = this.configForm();
for (const part of path) {
control = control.get(part);
}
control.valueChanges.subscribe(() => {
this.updateValidators(true, trigger);
});
}
this.configForm().valueChanges.subscribe((updated: any) => {
this.onConfigChanged(this.prepareOutputConfig(updated));
});
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
}
protected validatorTriggers(): string[] {
return [];
}
protected onConfigChanged(widgetConfig: WidgetConfigComponentData) {
this.widgetConfigValue = widgetConfig;
this.widgetConfigChangedEmitter.emit(this.widgetConfigValue);
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
return config;
}
public validateConfig(): boolean {
return this.configForm().valid;
}
protected abstract configForm(): UntypedFormGroup;
protected abstract onConfigSet(configData: WidgetConfigComponentData);
}

169
ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss

@ -0,0 +1,169 @@
/**
* Copyright © 2016-2023 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-widget-config-panel {
box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04);
border-radius: 4px;
padding: 16px;
gap: 16px;
display: flex;
flex-direction: column;
color: rgba(0, 0, 0, 0.87);
letter-spacing: 0.15px;
position: relative;
&.no-padding-bottom {
padding-bottom: 0;
}
&.stroked {
box-shadow: none;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
}
}
.tb-widget-config-panel-title {
font-weight: 500;
font-size: 16px;
}
.tb-widget-config-panel-hint {
font-size: 12px;
color: #808080;
}
.tb-widget-config-row {
height: 56px;
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
padding-left: 16px;
padding-right: 12px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
&.same-padding {
padding-right: 16px;
}
&.space-between {
justify-content: space-between;
}
.mat-divider-vertical {
height: 56px;
}
}
:host ::ng-deep {
.mat-slide {
margin: 8px 0;
.mdc-form-field>label {
font-weight: 400;
font-size: 16px;
line-height: 24px;
margin-left: 12px;
}
}
.slide-block {
display: block;
&:not(:last-child) {
margin-bottom: 8px;
}
}
.mat-mdc-form-field {
&.center {
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
.mat-mdc-form-field-infix {
.mdc-text-field__input {
text-align: center;
}
}
}
}
&.number {
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
padding-right: 4px;
.mat-mdc-form-field-infix {
width: 80px;
input.mdc-text-field__input[type=number]::-webkit-inner-spin-button,
input.mdc-text-field__input[type=number]::-webkit-outer-spin-button {
opacity: 1;
}
}
}
}
}
.tb-widget-config-row {
.mat-mdc-form-field {
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
padding-right: 12px;
padding-left: 12px;
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
border-color: rgba(0, 0, 0, 0.12);
}
}
.mat-mdc-form-field-infix {
padding-top: 7px;
padding-bottom: 7px;
min-height: 38px;
width: 72px;
}
}
}
}
.tb-widget-config-panel {
.mat-expansion-panel {
&.tb-settings {
box-shadow: none;
.mat-content {
overflow: visible;
}
.mat-expansion-panel-header {
font-weight: 500;
font-size: 16px;
line-height: 24px;
letter-spacing: 0.25px;
padding: 0;
.mat-content {
flex: 0;
white-space: nowrap;
}
&:hover {
background: none;
}
.mat-expansion-indicator {
height: 32px;
padding: 2px;
}
}
.mat-expansion-panel-header-description {
align-items: center;
}
> .mat-expansion-panel-content {
> .mat-expansion-panel-body {
padding: 0;
}
}
.tb-json-object-panel, .tb-css-content-panel {
margin: 0 0 8px;
}
}
.mat-expansion-panel-content {
font: inherit;
}
}
}
}

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

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

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

@ -16,28 +16,27 @@
import { import {
AfterViewInit, AfterViewInit,
Component, ComponentFactoryResolver, Component,
ComponentFactoryResolver,
ComponentRef, ComponentRef,
forwardRef, forwardRef,
Input, OnChanges, Input,
OnChanges,
OnDestroy, OnDestroy,
OnInit, SimpleChanges, OnInit,
SimpleChanges,
ViewChild, ViewChild,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { import {
IRuleNodeConfigurationComponent, ControlValueAccessor,
RuleNodeConfiguration, NG_VALUE_ACCESSOR,
RuleNodeDefinition UntypedFormBuilder,
} from '@shared/models/rule-node.models'; UntypedFormGroup,
Validators
} from '@angular/forms';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { RuleChainService } from '@core/http/rule-chain.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils';
import { RuleChainType } from '@shared/models/rule-chain.models';
import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; import { JsonFormComponent } from '@shared/components/json-form/json-form.component';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
import { IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models/widget.models'; import { IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models/widget.models';

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

@ -0,0 +1,20 @@
<!--
Copyright © 2016-2023 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.
-->
<mat-form-field appearance="outline" class="center" subscriptSizing="dynamic">
<input matInput [formControl]="unitsFormControl" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>

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

@ -0,0 +1,67 @@
///
/// Copyright © 2016-2023 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, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms';
@Component({
selector: 'tb-widget-units',
templateUrl: './widget-units.component.html',
styleUrls: ['./widget-config.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetUnitsComponent),
multi: true
}
]
})
export class WidgetUnitsComponent implements ControlValueAccessor, OnInit {
@Input()
disabled: boolean;
unitsFormControl: FormControl;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder) {
}
ngOnInit() {
this.unitsFormControl = this.fb.control('', []);
}
writeValue(units?: string): void {
this.unitsFormControl.patchValue(units, {emitEvent: false});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.unitsFormControl.disable({emitEvent: false});
} else {
this.unitsFormControl.enable({emitEvent: false});
}
}
}

32
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.html

@ -0,0 +1,32 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-absolute-fill" [fxLayout]="legendContainerLayoutType">
<tb-legend *ngIf="displayLegend && isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
<div fxFlex #flotElement></div>
<tb-legend *ngIf="displayLegend && !isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
</div>

151
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.component.ts

@ -0,0 +1,151 @@
///
/// Copyright © 2016-2023 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, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { ChartType, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models';
import { TbFlot } from '@home/components/widget/lib/flot-widget';
import {
defaultLegendConfig,
LegendConfig,
LegendData,
LegendPosition,
widgetType
} from '@shared/models/widget.models';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-flot-widget',
templateUrl: './flot-widget.component.html',
styleUrls: []
})
export class FlotWidgetComponent implements OnInit {
@ViewChild('flotElement', {static: true}) flotElement: ElementRef;
@Input()
ctx: WidgetContext;
@Input()
chartType: ChartType;
displayLegend: boolean;
legendConfig: LegendConfig;
legendData: LegendData;
isLegendFirst: boolean;
legendContainerLayoutType: string;
legendStyle: {[klass: string]: any};
public settings: TbFlotSettings;
private flot: TbFlot;
constructor() {
}
ngOnInit(): void {
this.ctx.$scope.flotWidget = this;
this.settings = this.ctx.settings;
this.chartType = this.chartType || 'line';
this.configureLegend();
this.flot = new TbFlot(this.ctx, this.chartType, $(this.flotElement.nativeElement));
}
private configureLegend(): void {
this.displayLegend = isDefinedAndNotNull(this.settings.showLegend) ? this.settings.showLegend
: false;
this.legendContainerLayoutType = 'column';
if (this.displayLegend) {
this.legendConfig = this.settings.legendConfig || defaultLegendConfig(widgetType.timeseries);
if (this.ctx.defaultSubscription) {
this.legendData = this.ctx.defaultSubscription.legendData;
} else {
this.legendData = {
keys: [],
data: []
};
}
if (this.legendConfig.position === LegendPosition.top ||
this.legendConfig.position === LegendPosition.bottom) {
this.legendContainerLayoutType = 'column';
this.isLegendFirst = this.legendConfig.position === LegendPosition.top;
} else {
this.legendContainerLayoutType = 'row';
this.isLegendFirst = this.legendConfig.position === LegendPosition.left;
}
switch (this.legendConfig.position) {
case LegendPosition.top:
this.legendStyle = {
paddingBottom: '8px',
maxHeight: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.bottom:
this.legendStyle = {
paddingTop: '8px',
maxHeight: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.left:
this.legendStyle = {
paddingRight: '0px',
maxWidth: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.right:
this.legendStyle = {
paddingLeft: '0px',
maxWidth: '50%',
overflowY: 'auto'
};
break;
}
}
}
public onLegendKeyHiddenChange(index: number) {
for (const id of Object.keys(this.ctx.subscriptions)) {
const subscription = this.ctx.subscriptions[id];
subscription.updateDataVisibility(index);
}
}
public onDataUpdated() {
this.flot.update();
}
public onLatestDataUpdated() {
this.flot.latestDataUpdate();
}
public onResize() {
this.flot.resize();
}
public onEditModeChanged() {
this.flot.checkMouseEvents();
}
public onDestroy() {
this.flot.destroy();
}
}

17
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts

@ -17,13 +17,21 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference // eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../../../../src/typings/jquery.flot.typings.d.ts" /> /// <reference path="../../../../../../../src/typings/jquery.flot.typings.d.ts" />
import { DataKey, Datasource, DatasourceData, FormattedData, JsonSettingsSchema } from '@shared/models/widget.models'; import {
DataKey,
Datasource,
DatasourceData,
FormattedData,
JsonSettingsSchema,
LegendConfig
} from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { ComparisonDuration } from '@shared/models/time/time.models'; import { ComparisonDuration } from '@shared/models/time/time.models';
export declare type ChartType = 'line' | 'pie' | 'bar' | 'state' | 'graph'; export declare type ChartType = 'line' | 'pie' | 'bar' | 'state' | 'graph';
export declare type TbFlotSettings = TbFlotBaseSettings & TbFlotGraphSettings & TbFlotBarSettings & TbFlotPieSettings; export declare type TbFlotSettings = TbFlotBaseSettings & TbFlotLegendSettings &
TbFlotGraphSettings & TbFlotBarSettings & TbFlotPieSettings;
export declare type TooltipValueFormatFunction = (value: any, latestData: FormattedData) => string; export declare type TooltipValueFormatFunction = (value: any, latestData: FormattedData) => string;
@ -140,6 +148,11 @@ export interface TbFlotBaseSettings {
yaxis: TbFlotYAxisSettings; yaxis: TbFlotYAxisSettings;
} }
export interface TbFlotLegendSettings {
showLegend?: boolean;
legendConfig?: LegendConfig;
}
export interface TbFlotComparisonSettings { export interface TbFlotComparisonSettings {
comparisonEnabled: boolean; comparisonEnabled: boolean;
timeForComparison: ComparisonDuration; timeForComparison: ComparisonDuration;

4
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts

@ -127,7 +127,7 @@ export class TbFlot {
private pieAnimationLastTime: number; private pieAnimationLastTime: number;
private pieAnimationCaf: CancelAnimationFrame; private pieAnimationCaf: CancelAnimationFrame;
constructor(private ctx: WidgetContext, private readonly chartType: ChartType) { constructor(private ctx: WidgetContext, private readonly chartType: ChartType, private $flotElement?: JQuery<any>) {
this.chartType = this.chartType || 'line'; this.chartType = this.chartType || 'line';
this.settings = ctx.settings as TbFlotSettings; this.settings = ctx.settings as TbFlotSettings;
this.utils = this.ctx.$injector.get(UtilsService); this.utils = this.ctx.$injector.get(UtilsService);
@ -334,7 +334,7 @@ export class TbFlot {
} }
if (this.ctx.defaultSubscription) { if (this.ctx.defaultSubscription) {
this.init(this.ctx.$container, this.ctx.defaultSubscription); this.init(this.$flotElement || this.ctx.$container, this.ctx.defaultSubscription);
} }
} }

3
ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widgets.module.ts

@ -28,7 +28,6 @@ import { GettingStartedWidgetComponent } from '@home/components/widget/lib/home-
import { import {
GettingStartedCompletedDialogComponent GettingStartedCompletedDialogComponent
} from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component'; } from '@home/components/widget/lib/home-page/getting-started-completed-dialog.component';
import { ToggleHeaderComponent } from '@home/components/widget/lib/home-page/toggle-header.component';
import { UsageInfoWidgetComponent } from '@home/components/widget/lib/home-page/usage-info-widget.component'; import { UsageInfoWidgetComponent } from '@home/components/widget/lib/home-page/usage-info-widget.component';
import { QuickLinksWidgetComponent } from '@home/components/widget/lib/home-page/quick-links-widget.component'; import { QuickLinksWidgetComponent } from '@home/components/widget/lib/home-page/quick-links-widget.component';
import { QuickLinkComponent } from '@home/components/widget/lib/home-page/quick-link.component'; import { QuickLinkComponent } from '@home/components/widget/lib/home-page/quick-link.component';
@ -49,7 +48,6 @@ import {
EditLinksDialogComponent, EditLinksDialogComponent,
GettingStartedWidgetComponent, GettingStartedWidgetComponent,
GettingStartedCompletedDialogComponent, GettingStartedCompletedDialogComponent,
ToggleHeaderComponent,
UsageInfoWidgetComponent, UsageInfoWidgetComponent,
QuickLinksWidgetComponent, QuickLinksWidgetComponent,
QuickLinkComponent, QuickLinkComponent,
@ -70,7 +68,6 @@ import {
EditLinksDialogComponent, EditLinksDialogComponent,
GettingStartedWidgetComponent, GettingStartedWidgetComponent,
GettingStartedCompletedDialogComponent, GettingStartedCompletedDialogComponent,
ToggleHeaderComponent,
UsageInfoWidgetComponent, UsageInfoWidgetComponent,
QuickLinksWidgetComponent, QuickLinksWidgetComponent,
QuickLinkComponent, QuickLinkComponent,

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

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

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

29
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html

@ -15,16 +15,19 @@
limitations under the License. limitations under the License.
--> -->
<section [formGroup]="simpleCardWidgetSettingsForm" fxLayout="column"> <div [formGroup]="simpleCardWidgetSettingsForm" class="tb-widget-config-panel">
<mat-form-field fxFlex class="mat-block"> <div class="tb-widget-config-panel-title" translate>widgets.simple-card.label</div>
<mat-label translate>widgets.simple-card.label-position</mat-label> <div class="tb-widget-config-row space-between">
<mat-select formControlName="labelPosition"> <div translate>widgets.simple-card.label-position</div>
<mat-option [value]="'left'"> <mat-form-field appearance="outline" subscriptSizing="dynamic">
{{ 'widgets.simple-card.label-position-left' | translate }} <mat-select formControlName="labelPosition">
</mat-option> <mat-option [value]="'left'">
<mat-option [value]="'top'"> {{ 'widgets.simple-card.label-position-left' | translate }}
{{ 'widgets.simple-card.label-position-top' | translate }} </mat-option>
</mat-option> <mat-option [value]="'top'">
</mat-select> {{ 'widgets.simple-card.label-position-top' | translate }}
</mat-form-field> </mat-option>
</section> </mat-select>
</mat-form-field>
</div>
</div>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts

@ -23,7 +23,7 @@ import { AppState } from '@core/core.state';
@Component({ @Component({
selector: 'tb-simple-card-widget-settings', selector: 'tb-simple-card-widget-settings',
templateUrl: './simple-card-widget-settings.component.html', templateUrl: './simple-card-widget-settings.component.html',
styleUrls: [] styleUrls: ['../../../config/widget-config.scss']
}) })
export class SimpleCardWidgetSettingsComponent extends WidgetSettingsComponent { export class SimpleCardWidgetSettingsComponent extends WidgetSettingsComponent {

19
ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/flot-widget-settings.component.html

@ -68,6 +68,25 @@
<input matInput type="number" min="0" formControlName="thresholdsLineWidth"> <input matInput type="number" min="0" formControlName="thresholdsLineWidth">
</mat-form-field> </mat-form-field>
</fieldset> </fieldset>
<fieldset *ngIf="chartType === 'graph' || chartType === 'bar'" class="fields-group fields-group-slider">
<legend class="group-title" translate>widget-config.legend</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('showLegend').value">
<mat-expansion-panel-header fxLayout="row" style="min-height: 48px;">
<mat-panel-title fxFlex="60">
<mat-slide-toggle formControlName="showLegend" (click)="$event.stopPropagation()"
fxLayoutAlign="center" style="height: 100%;">
{{ 'widget-config.display-legend' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxFlex="40" style="white-space: nowrap;" fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-legend-config formControlName="legendConfig"></tb-legend-config>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group fields-group-slider"> <fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.chart.tooltip-settings</legend> <legend class="group-title" translate>widgets.chart.tooltip-settings</legend>
<mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('showTooltip').value"> <mat-expansion-panel class="tb-settings" [expanded]="flotSettingsFormGroup.get('showTooltip').value">

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

@ -40,8 +40,9 @@ import {
labelDataKeyValidator labelDataKeyValidator
} from '@home/components/widget/lib/settings/chart/label-data-key.component'; } from '@home/components/widget/lib/settings/chart/label-data-key.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { defaultLegendConfig, widgetType } from '@shared/models/widget.models';
export function flotDefaultSettings(chartType: ChartType): Partial<TbFlotSettings> { export const flotDefaultSettings = (chartType: ChartType): Partial<TbFlotSettings> => {
const settings: Partial<TbFlotSettings> = { const settings: Partial<TbFlotSettings> = {
stack: false, stack: false,
fontColor: '#545454', fontColor: '#545454',
@ -95,9 +96,11 @@ export function flotDefaultSettings(chartType: ChartType): Partial<TbFlotSetting
}; };
settings.customLegendEnabled = false; settings.customLegendEnabled = false;
settings.dataKeysListForLabels = []; settings.dataKeysListForLabels = [];
settings.showLegend = true;
settings.legendConfig = defaultLegendConfig(widgetType.timeseries);
} }
return settings; return settings;
} };
@Component({ @Component({
selector: 'tb-flot-widget-settings', selector: 'tb-flot-widget-settings',
@ -221,6 +224,11 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
showLabels: [true, []] showLabels: [true, []]
})); }));
// Legend settings
this.flotSettingsFormGroup.addControl('showLegend', this.fb.control(false, []));
this.flotSettingsFormGroup.addControl('legendConfig', this.fb.control(null, []));
// Custom legend settings // Custom legend settings
this.flotSettingsFormGroup.addControl('customLegendEnabled', this.fb.control(false, [])); this.flotSettingsFormGroup.addControl('customLegendEnabled', this.fb.control(false, []));
@ -240,6 +248,9 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
}); });
if (this.chartType === 'graph' || this.chartType === 'bar') { if (this.chartType === 'graph' || this.chartType === 'bar') {
this.flotSettingsFormGroup.get('showLegend').valueChanges.subscribe(() => {
this.updateValidators(true);
});
this.flotSettingsFormGroup.get('comparisonEnabled').valueChanges.subscribe(() => { this.flotSettingsFormGroup.get('comparisonEnabled').valueChanges.subscribe(() => {
this.updateValidators(true); this.updateValidators(true);
}); });
@ -350,9 +361,15 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
this.flotSettingsFormGroup.get('yaxis.ticksFormatter').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('yaxis.ticksFormatter').updateValueAndValidity({emitEvent: false});
if (this.chartType === 'graph' || this.chartType === 'bar') { if (this.chartType === 'graph' || this.chartType === 'bar') {
const showLegend: boolean = this.flotSettingsFormGroup.get('showLegend').value;
const comparisonEnabled: boolean = this.flotSettingsFormGroup.get('comparisonEnabled').value; const comparisonEnabled: boolean = this.flotSettingsFormGroup.get('comparisonEnabled').value;
const timeForComparison: ComparisonDuration = this.flotSettingsFormGroup.get('timeForComparison').value; const timeForComparison: ComparisonDuration = this.flotSettingsFormGroup.get('timeForComparison').value;
const customLegendEnabled: boolean = this.flotSettingsFormGroup.get('customLegendEnabled').value; const customLegendEnabled: boolean = this.flotSettingsFormGroup.get('customLegendEnabled').value;
if (showLegend) {
this.flotSettingsFormGroup.get('legendConfig').enable({emitEvent});
} else {
this.flotSettingsFormGroup.get('legendConfig').disable({emitEvent});
}
if (comparisonEnabled) { if (comparisonEnabled) {
this.flotSettingsFormGroup.get('timeForComparison').enable({emitEvent: false}); this.flotSettingsFormGroup.get('timeForComparison').enable({emitEvent: false});
if (timeForComparison === 'customInterval') { if (timeForComparison === 'customInterval') {
@ -370,6 +387,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
this.flotSettingsFormGroup.get('dataKeysListForLabels').disable({emitEvent}); this.flotSettingsFormGroup.get('dataKeysListForLabels').disable({emitEvent});
} }
this.flotSettingsFormGroup.get('legendConfig').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('dataKeysListForLabels').updateValueAndValidity({emitEvent: false}); this.flotSettingsFormGroup.get('dataKeysListForLabels').updateValueAndValidity({emitEvent: false});

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

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

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

@ -265,6 +265,7 @@ import {
import { import {
QuickLinksWidgetSettingsComponent QuickLinksWidgetSettingsComponent
} from '@home/components/widget/lib/settings/home-page/quick-links-widget-settings.component'; } from '@home/components/widget/lib/settings/home-page/quick-links-widget-settings.component';
import { LegendConfigComponent } from '@home/components/widget/lib/settings/common/legend-config.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -290,6 +291,7 @@ import {
AnalogueCompassWidgetSettingsComponent, AnalogueCompassWidgetSettingsComponent,
DigitalGaugeWidgetSettingsComponent, DigitalGaugeWidgetSettingsComponent,
ValueSourceComponent, ValueSourceComponent,
LegendConfigComponent,
FixedColorLevelComponent, FixedColorLevelComponent,
TickValueComponent, TickValueComponent,
FlotWidgetSettingsComponent, FlotWidgetSettingsComponent,
@ -395,6 +397,7 @@ import {
AnalogueCompassWidgetSettingsComponent, AnalogueCompassWidgetSettingsComponent,
DigitalGaugeWidgetSettingsComponent, DigitalGaugeWidgetSettingsComponent,
ValueSourceComponent, ValueSourceComponent,
LegendConfigComponent,
FixedColorLevelComponent, FixedColorLevelComponent,
TickValueComponent, TickValueComponent,
FlotWidgetSettingsComponent, FlotWidgetSettingsComponent,

0
ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html → ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.html

0
ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.scss

0
ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/trip-animation/trip-animation.component.ts

7
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -99,6 +99,8 @@ export class WidgetComponentService {
settingsDirective: this.utils.editWidgetInfo.settingsDirective, settingsDirective: this.utils.editWidgetInfo.settingsDirective,
dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective, dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: this.utils.editWidgetInfo.latestDataKeySettingsDirective, latestDataKeySettingsDirective: this.utils.editWidgetInfo.latestDataKeySettingsDirective,
hasBasicMode: this.utils.editWidgetInfo.hasBasicMode,
basicModeDirective: this.utils.editWidgetInfo.basicModeDirective,
defaultConfig: this.utils.editWidgetInfo.defaultConfig defaultConfig: this.utils.editWidgetInfo.defaultConfig
}, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined
); );
@ -162,7 +164,7 @@ export class WidgetComponentService {
(window as any).TbMapWidgetV2 = mod.TbMapWidgetV2; (window as any).TbMapWidgetV2 = mod.TbMapWidgetV2;
})) }))
); );
widgetModulesTasks.push(from(import('@home/components/widget/trip-animation/trip-animation.component')).pipe( widgetModulesTasks.push(from(import('@home/components/widget/lib/trip-animation/trip-animation.component')).pipe(
tap((mod) => { tap((mod) => {
(window as any).TbTripAnimationWidget = mod.TbTripAnimationWidget; (window as any).TbTripAnimationWidget = mod.TbTripAnimationWidget;
})) }))
@ -404,6 +406,9 @@ export class WidgetComponentService {
if (widgetInfo.latestDataKeySettingsDirective && widgetInfo.latestDataKeySettingsDirective.length) { if (widgetInfo.latestDataKeySettingsDirective && widgetInfo.latestDataKeySettingsDirective.length) {
directives.push(widgetInfo.latestDataKeySettingsDirective); directives.push(widgetInfo.latestDataKeySettingsDirective);
} }
if (widgetInfo.basicModeDirective && widgetInfo.basicModeDirective.length) {
directives.push(widgetInfo.basicModeDirective);
}
if (directives.length) { if (directives.length) {
factories.filter((factory) => directives.includes(factory.selector)) factories.filter((factory) => directives.includes(factory.selector))
.forEach((foundFactory) => { .forEach((foundFactory) => {

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

@ -29,7 +29,7 @@ import {
DateRangeNavigatorWidgetComponent DateRangeNavigatorWidgetComponent
} from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component'; } from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component';
import { MultipleInputWidgetComponent } from '@home/components/widget/lib/multiple-input-widget.component'; import { MultipleInputWidgetComponent } from '@home/components/widget/lib/multiple-input-widget.component';
import { TripAnimationComponent } from '@home/components/widget/trip-animation/trip-animation.component'; import { TripAnimationComponent } from '@home/components/widget/lib/trip-animation/trip-animation.component';
import { PhotoCameraInputWidgetComponent } from '@home/components/widget/lib/photo-camera-input.component'; import { PhotoCameraInputWidgetComponent } from '@home/components/widget/lib/photo-camera-input.component';
import { GatewayFormComponent } from '@home/components/widget/lib/gateway/gateway-form.component'; import { GatewayFormComponent } from '@home/components/widget/lib/gateway/gateway-form.component';
import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
@ -41,6 +41,8 @@ import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-wi
import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component'; import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component';
import { HomePageWidgetsModule } from '@home/components/widget/lib/home-page/home-page-widgets.module'; import { HomePageWidgetsModule } from '@home/components/widget/lib/home-page/home-page-widgets.module';
import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { FlotWidgetComponent } from '@home/components/widget/lib/flot-widget.component';
import { LegendComponent } from '@home/components/widget/lib/legend.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -62,7 +64,9 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
NavigationCardWidgetComponent, NavigationCardWidgetComponent,
QrCodeWidgetComponent, QrCodeWidgetComponent,
MarkdownWidgetComponent, MarkdownWidgetComponent,
SelectEntityDialogComponent SelectEntityDialogComponent,
LegendComponent,
FlotWidgetComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -88,7 +92,9 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
NavigationCardsWidgetComponent, NavigationCardsWidgetComponent,
NavigationCardWidgetComponent, NavigationCardWidgetComponent,
QrCodeWidgetComponent, QrCodeWidgetComponent,
MarkdownWidgetComponent MarkdownWidgetComponent,
LegendComponent,
FlotWidgetComponent
], ],
providers: [ providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

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

@ -15,480 +15,268 @@
limitations under the License. limitations under the License.
--> -->
<mat-tab-group mat-stretch-tabs="false" class="tb-widget-config tb-absolute-fill" [(selectedIndex)]="selectedTab"> <div class="tb-widget-config tb-absolute-fill">
<mat-tab label="{{ 'widget-config.data' | translate }}" *ngIf="widgetType !== widgetTypes.static"> <div class="tb-widget-config-header" *ngIf="!hideHeader">
<div [formGroup]="dataSettings" class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px"> <tb-toggle-header *ngIf="!hideToggleHeader && widgetConfigMode === widgetConfigModes.advanced" (valueChange)="selectedOption = $event" ignoreMdLgSize="true"
<div *ngIf="displayTimewindowConfig()" fxFlex="100" appearance="fill" [options]="headerOptions" [value]="selectedOption" name="widgetConfigHeader" useSelectOnMdLg="false">
fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"> </tb-toggle-header>
<div fxLayout="column" fxFlex.gt-xs> <ng-content select=".tb-widget-config-header-prefix"></ng-content>
<mat-checkbox formControlName="useDashboardTimewindow"> <ng-content select=".tb-widget-config-header-suffix"></ng-content>
{{ 'widget-config.use-dashboard-timewindow' | translate }} </div>
</mat-checkbox> <div *ngIf="widgetConfigMode === widgetConfigModes.advanced; else basicMode" class="tb-widget-config-content">
<mat-checkbox formControlName="displayTimewindow"> <div *ngIf="widgetType !== widgetTypes.static" [fxShow]="selectedOption === 'data'" class="mat-content">
{{ 'widget-config.display-timewindow' | translate }} <ng-container *ngTemplateOutlet="data"></ng-container>
</mat-checkbox> </div>
<div [fxShow]="selectedOption === 'appearance'" class="mat-content">
<ng-container *ngTemplateOutlet="appearance"></ng-container>
</div>
<div [fxShow]="selectedOption === 'card'" [formGroup]="widgetSettings" class="mat-content">
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widget-config.card-title</div>
<mat-slide-toggle class="mat-slide" formControlName="showTitle">
{{ 'widget-config.display-title' | translate }}
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex>
<mat-label translate>widget-config.title</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.title-tooltip</mat-label>
<input matInput formControlName="titleTooltip">
</mat-form-field>
</div> </div>
<section fxLayout="row" fxLayoutAlign="start center" fxLayout.lt-lg="column" fxLayoutAlign.lt-lg="center start" <div class="tb-widget-config-row space-between same-padding">
fxFlex.gt-xs> <mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
<span [ngClass]="{'tb-disabled-label': dataSettings.get('useDashboardTimewindow').value}" translate {{ 'widget-config.display-icon' | translate }}
style="padding-right: 8px;">widget-config.timewindow</span> </mat-slide-toggle>
<tb-timewindow asButton="true" <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
isEdit="true" <tb-material-icon-select asBoxInput
alwaysDisplayTypePrefix [color]="widgetSettings.get('iconColor').value"
[historyOnly]="onlyHistoryTimewindow()" formControlName="titleIcon">
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}" </tb-material-icon-select>
aggregation="{{ widgetType === widgetTypes.timeseries }}" <mat-form-field appearance="outline" subscriptSizing="dynamic" style="width: 60px;">
fxFlex formControlName="timewindow"></tb-timewindow> <input matInput formControlName="iconSize">
</section> </mat-form-field>
</div> <mat-divider vertical></mat-divider>
<div *ngIf="widgetType === widgetTypes.alarm" fxLayout="column" fxLayoutAlign="center"> <tb-color-input asBoxInput
<tb-alarm-filter-config buttonMode="false" formControlName="alarmFilterConfig"></tb-alarm-filter-config> formControlName="iconColor">
</div> </tb-color-input>
<mat-accordion multi> </div>
<mat-expansion-panel (opened)="extensionPanelIsOpen(true)" (closed)="extensionPanelIsOpen(false)" class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc && </div>
widgetType !== widgetTypes.alarm && <mat-expansion-panel class="tb-settings">
modelValue?.isDataEnabled" [expanded]="true"> <mat-expansion-panel-header expandedHeight="48px">
<mat-expansion-panel-header> <mat-panel-description fxLayoutAlign="start" translate>
<mat-panel-title fxLayout="row"> widget-config.advanced-title-style
<div fxLayout="column">
<div class="tb-panel-title" translate>widget-config.datasources</div>
<div *ngIf="modelValue?.typeParameters && modelValue?.typeParameters.maxDatasources > -1"
class="tb-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}</div>
</div>
<mat-icon *ngIf = '(datasourceError.length > 0 || !datasourcesFormArray().valid) && !openExtensionPanel'
style = "color: red; font-size: 18px; margin-left: 5px;">
report_gmailerrorred
</mat-icon>
</mat-panel-title>
<mat-panel-description *ngIf = "timeseriesKeyError">
<mat-error >{{ 'widget-config.timeseries-key-error' | translate }}</mat-error>
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div *ngIf="datasourcesFormArray().length === 0; else datasourcesTemplate"> <ng-template matExpansionPanelContent>
<span translate fxLayoutAlign="center center" <tb-json-object-edit
class="tb-prompt">datasource.add-datasource-prompt</span> [editorStyle]="{minHeight: '100px'}"
</div> required
<ng-template #datasourcesTemplate> label="{{ 'widget-config.title-style' | translate }}"
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> formControlName="titleStyle"
<span style="width: 68px;"></span> ></tb-json-object-edit>
<div fxFlex fxLayout="row" fxLayoutAlign="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span translate style="min-width: 160px;">widget-config.datasource-type</span>
<span fxHide fxShow.gt-sm translate fxFlex
style="padding-left: 10px;">widget-config.datasource-parameters</span>
<span style="min-width: 40px;"></span>
</div>
</div>
<div style="overflow: auto; padding-bottom: 15px;">
<mat-list class="tb-drop-list"
cdkDropList
cdkDropListOrientation="vertical"
(cdkDropListDropped)="onDatasourceDrop($event)"
[cdkDropListDisabled]="disabled"
formArrayName="datasources">
<mat-list-item *ngFor="let datasourceControl of datasourcesFormArray().controls; let $index = index;"
class="tb-datasource-list-item tb-draggable" cdkDrag
[cdkDragDisabled]="disabled">
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="row" fxLayoutAlign="start center" style="width: 68px;">
<button *ngIf="!disabled" mat-icon-button color="primary"
class="handle"
style="min-width: 40px; margin: 0"
cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_handle</mat-icon>
</button>
<span>{{$index + 1}}.</span>
</div>
<div class="mat-elevation-z4 tb-datasource-params" fxFlex>
<div fxFlex
fxLayout="row"
fxLayoutAlign="start center">
<section class="tb-datasource-section">
<mat-form-field class="tb-datasource-type">
<mat-select [formControl]="datasourceControl.get('type')">
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
{{ datasourceTypesTranslations.get(datasourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<section fxLayout="column" class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value">
<ng-template [ngSwitchCase]="datasourceType.function">
<mat-form-field fxFlex>
<mat-label translate>datasource.label</mat-label>
<input matInput
[formControl]="datasourceControl.get('name')">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="datasourceControl.get('type').value === datasourceType.entity ||
datasourceControl.get('type').value === datasourceType.entityCount ||
datasourceControl.get('type').value === datasourceType.alarmCount ? datasourceControl.get('type').value : ''">
<tb-alarm-filter-config *ngIf="datasourceControl.get('type').value === datasourceType.alarmCount"
propagatedFilter="false"
style="height: 56px; margin-bottom: 8px;"
[formControl]="datasourceControl.get('alarmFilterConfig')"></tb-alarm-filter-config>
<tb-entity-alias-select
*ngIf="datasourceControl.get('type').value !== datasourceType.alarmCount"
[showLabel]="true"
[tbRequired]="true"
[aliasController]="aliasController"
[formControl]="datasourceControl.get('entityAliasId')"
[callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select>
<tb-filter-select
*ngIf="datasourceControl.get('type').value !== datasourceType.alarmCount"
[showLabel]="true"
[aliasController]="aliasController"
[formControl]="datasourceControl.get('filterId')"
[callbacks]="widgetConfigCallbacks">
</tb-filter-select>
<mat-form-field *ngIf="[datasourceType.entityCount, datasourceType.alarmCount].includes(datasourceControl.get('type').value)"
fxFlex>
<input matInput
placeholder="{{ 'datasource.label' | translate }}"
[formControl]="datasourceControl.get('name')">
</mat-form-field>
</ng-template>
</section>
<section fxLayout="column" fxLayoutAlign="stretch" fxFlex>
<tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType"
[datasourceType]="datasourceControl.get('type').value"
[maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
[optDataKeys]="dataKeysOptional(datasourceControl.get('type').value)"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="widgetConfigCallbacks"
[entityAliasId]="datasourceControl.get('entityAliasId').value"
[formControl]="datasourceControl.get('dataKeys')">
</tb-data-keys>
<tb-data-keys *ngIf="widgetType === widgetTypes.timeseries &&
modelValue?.typeParameters?.hasAdditionalLatestDataKeys" class="tb-data-keys" fxFlex
[widgetType]="widgetTypes.latest"
[datasourceType]="datasourceControl.get('type').value"
[optDataKeys]="true"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.latestDataKeySettingsSchema"
[dataKeySettingsDirective]="modelValue?.latestDataKeySettingsDirective"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="widgetConfigCallbacks"
[entityAliasId]="datasourceControl.get('entityAliasId').value"
[formControl]="datasourceControl.get('latestDataKeys')">
</tb-data-keys>
</section>
</section>
<button [disabled]="isLoading$ | async"
type="button"
mat-icon-button color="primary"
style="min-width: 40px;"
(click)="removeDatasource($index)"
matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<tb-error class="tb-datasource-error" [error]="datasourceError[$index] ? datasourceError[$index] : ''"></tb-error>
</div>
</div>
</mat-list-item>
</mat-list>
</div>
</ng-template> </ng-template>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<button [disabled]="isLoading$ | async"
type="button"
mat-raised-button color="primary"
[fxShow]="modelValue?.typeParameters &&
(modelValue?.typeParameters.maxDatasources === -1 || datasourcesFormArray().controls.length < modelValue?.typeParameters.maxDatasources)"
(click)="addDatasource()"
matTooltip="{{ 'widget-config.add-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
<span translate>action.add</span>
</button>
</div>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc && </div>
modelValue?.isDataEnabled" [expanded]="true"> <div class="tb-widget-config-panel">
<mat-expansion-panel-header> <div class="tb-widget-config-panel-title" translate>widget-config.card-style</div>
<mat-panel-title> <div class="tb-widget-config-row space-between same-padding">
{{ 'widget-config.target-device' | translate }} <div>{{ 'widget-config.text' | translate }}</div>
</mat-panel-title> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
</mat-expansion-panel-header> <mat-divider vertical></mat-divider>
<div [formGroup]="targetDeviceSettings" style="padding: 0 5px;"> <tb-color-input asBoxInput
<tb-entity-alias-select fxFlex formControlName="color">
[tbRequired]="!widgetEditMode" </tb-color-input>
[aliasController]="aliasController"
[allowedEntityTypes]="[entityTypes.DEVICE]"
[callbacks]="widgetConfigCallbacks"
formControlName="targetDeviceAliasId">
</tb-entity-alias-select>
</div> </div>
</mat-expansion-panel> </div>
<mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.alarm && <div class="tb-widget-config-row space-between same-padding">
modelValue?.isDataEnabled" [expanded]="true"> <div>{{ 'widget-config.background' | translate }}</div>
<mat-expansion-panel-header> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-panel-title> <mat-divider vertical></mat-divider>
{{ 'widget-config.alarm-source' | translate }} <tb-color-input asBoxInput
</mat-panel-title> formControlName="backgroundColor">
</mat-expansion-panel-header> </tb-color-input>
<div class="tb-datasource-params" [formGroup]="alarmSourceSettings">
<section class="tb-datasource-section">
<mat-form-field class="tb-datasource-type">
<mat-select formControlName="type">
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
{{ datasourceTypesTranslations.get(datasourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<section class="tb-datasource" [ngSwitch]="alarmSourceSettings.get('type').value">
<ng-template [ngSwitchCase]="datasourceType.entity">
<tb-entity-alias-select
[showLabel]="true"
[tbRequired]="alarmSourceSettings.get('type').value === datasourceType.entity"
[aliasController]="aliasController"
formControlName="entityAliasId"
[callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select>
<tb-filter-select
[showLabel]="true"
[aliasController]="aliasController"
formControlName="filterId"
[callbacks]="widgetConfigCallbacks">
</tb-filter-select>
</ng-template>
</section>
<tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType"
[datasourceType]="alarmSourceSettings.get('type').value"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="widgetConfigCallbacks"
[entityAliasId]="alarmSourceSettings.get('entityAliasId').value"
formControlName="dataKeys">
</tb-data-keys>
</section>
</div> </div>
</mat-expansion-panel> </div>
<mat-expansion-panel [formGroup]="widgetSettings"> <div class="tb-widget-config-row space-between">
<div>{{ 'widget-config.padding' | translate }}</div>
<mat-form-field appearance="outline" class="center" subscriptSizing="dynamic">
<input matInput formControlName="padding" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div>{{ 'widget-config.margin' | translate }}</div>
<mat-form-field appearance="outline" class="center" subscriptSizing="dynamic">
<input matInput formControlName="margin" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<mat-slide-toggle class="mat-slide slide-block" formControlName="dropShadow">
{{ 'widget-config.drop-shadow' | translate }}
</mat-slide-toggle>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title translate>widget-config.data-settings</mat-panel-title> <mat-panel-description fxLayoutAlign="start" translate>
widget-config.advanced-widget-style
</mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div fxLayout="row" *ngIf="widgetType !== widgetTypes.rpc && <tb-json-object-edit
widgetType !== widgetTypes.alarm && [editorStyle]="{minHeight: '100px'}"
modelValue?.isDataEnabled && !modelValue?.typeParameters?.singleEntity"> required
<mat-form-field fxFlex> label="{{ 'widget-config.widget-style' | translate }}"
<mat-label translate>widget-config.data-page-size</mat-label> formControlName="widgetStyle"
<input matInput formControlName="pageSize" type="number" min="1" step="1"> ></tb-json-object-edit>
</mat-form-field> <tb-css
</div> label="{{ 'widget-config.widget-css' | translate }}"
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px"> formControlName="widgetCss"
<mat-form-field fxFlex> ></tb-css>
<mat-label translate>widget-config.units</mat-label>
<input matInput formControlName="units">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.decimals</mat-label>
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1">
</mat-form-field>
</div>
<div fxLayout="row">
<mat-form-field fxFlex>
<mat-label translate>widget-config.no-data-display-message</mat-label>
<input matInput formControlName="noDataDisplayMessage">
</mat-form-field>
</div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </div>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widget-config.card-buttons</div>
<mat-slide-toggle class="mat-slide slide-block" formControlName="enableFullscreen">
{{ 'widget-config.enable-fullscreen' | translate }}
</mat-slide-toggle>
</div>
</div> </div>
</mat-tab> <div [fxShow]="selectedOption === 'actions'" [formGroup]="actionsSettings" class="mat-content" style="height: 100%;">
<mat-tab label="{{ 'widget-config.settings' | translate }}"> <div class="tb-widget-config-panel" style="height: 100%;">
<div class="mat-content mat-padding" fxLayout="column"> <tb-manage-widget-actions
<div [formGroup]="widgetSettings" fxLayout="column"> [callbacks]="widgetConfigCallbacks"
<fieldset class="fields-group"> [widgetType] = "modelValue.widgetType"
<legend class="group-title" translate>widget-config.title</legend> [actionSources]="modelValue.actionSources"
<mat-slide-toggle class="mat-slide" formControlName="showTitle"> formControlName="actions">
{{ 'widget-config.display-title' | translate }} </tb-manage-widget-actions>
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex>
<mat-label translate>widget-config.title</mat-label>
<input matInput formControlName="title">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.title-tooltip</mat-label>
<input matInput formControlName="titleTooltip">
</mat-form-field>
</div>
<fieldset class="fields-group" fxLayoutGap.gt-xs="8px" style="margin: 0">
<legend class="group-title" translate>widget-config.title-icon</legend>
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.display-icon' | translate }}
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px">
<tb-material-icon-select fxFlex
formControlName="titleIcon">
</tb-material-icon-select>
<tb-color-input fxFlex
label="{{'widget-config.icon-color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="iconColor">
</tb-color-input>
<mat-form-field fxFlex>
<mat-label translate>widget-config.icon-size</mat-label>
<input matInput formControlName="iconSize">
</mat-form-field>
</div>
</fieldset>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description fxLayoutAlign="end" translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
required
label="{{ 'widget-config.title-style' | translate }}"
formControlName="titleStyle"
></tb-json-object-edit>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widget-config.widget-style</legend>
<div fxLayout="column" fxLayout.gt-md="row wrap" fxFlex fxLayoutGap.gt-xs="8px" class="tb-widget-style">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<tb-color-input fxFlex
label="{{'widget-config.background-color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="backgroundColor">
</tb-color-input>
<tb-color-input fxFlex
label="{{'widget-config.text-color' | translate}}"
icon="format_color_fill"
openOnInput
formControlName="color">
</tb-color-input>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>widget-config.padding</mat-label>
<input matInput formControlName="padding">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.margin</mat-label>
<input matInput formControlName="margin">
</mat-form-field>
</div>
</div>
<mat-slide-toggle class="slide-block" formControlName="dropShadow">
{{ 'widget-config.drop-shadow' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="slide-block" formControlName="enableFullscreen">
{{ 'widget-config.enable-fullscreen' | translate }}
</mat-slide-toggle>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description fxLayoutAlign="end" translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
required
label="{{ 'widget-config.widget-style' | translate }}"
formControlName="widgetStyle"
></tb-json-object-edit>
<tb-css
label="{{ 'widget-config.widget-css' | translate }}"
formControlName="widgetCss"
></tb-css>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset class="fields-group fields-group-slider" *ngIf="showLegendFieldset">
<legend class="group-title" translate>widget-config.legend</legend>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="showLegend" (click)="$event.stopPropagation()" fxLayoutAlign="center">
{{ 'widget-config.display-legend' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-legend-config formControlName="legendConfig"></tb-legend-config>
</ng-template>
</mat-expansion-panel>
</fieldset>
<fieldset [formGroup]="layoutSettings" class="fields-group fields-group-slider">
<legend class="group-title" translate>widget-config.mobile-mode-settings</legend>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header style="height: fit-content;">
<mat-panel-title fxLayout.xs="column" fxLayoutAlign.xs="center start" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex="70">
<mat-slide-toggle formControlName="mobileHide" (click)="$event.stopPropagation()" fxLayoutAlign="center">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
<mat-slide-toggle formControlName="desktopHide" (click)="$event.stopPropagation()" fxLayoutAlign="center">
{{ 'widget-config.desktop-hide' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate fxFlex="30">
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap.gt-xs="8px" fxFlex>
<mat-form-field fxFlex>
<mat-label translate>widget-config.order</mat-label>
<input matInput formControlName="mobileOrder" type="number" step="1">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.height</mat-label>
<input matInput formControlName="mobileHeight" type="number" min="1" step="1">
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>
</div> </div>
</div> </div>
</mat-tab> <div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content">
<mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}"> <div class="tb-widget-config-panel">
<div [formGroup]="advancedSettings" class="mat-content mat-padding tb-advanced-widget-config" <mat-slide-toggle class="mat-slide" formControlName="mobileHide">
fxLayout="column"> {{ 'widget-config.mobile-hide' | translate }}
<tb-widget-settings </mat-slide-toggle>
[aliasController]="aliasController" </div>
[dashboard]="dashboard" <div class="tb-widget-config-panel">
[widget]="widget" <mat-slide-toggle class="mat-slide" formControlName="desktopHide">
formControlName="settings"> {{ 'widget-config.desktop-hide' | translate }}
</tb-widget-settings> </mat-slide-toggle>
</div>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-row space-between">
<div translate>widget-config.order</div>
<mat-form-field appearance="outline" class="number center" subscriptSizing="dynamic">
<input matInput formControlName="mobileOrder" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between">
<div translate>widget-config.height</div>
<mat-form-field appearance="outline" class="number center" subscriptSizing="dynamic">
<input matInput formControlName="mobileHeight" type="number" min="1" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
</div>
</div>
<ng-template #basicMode>
<div class="tb-widget-config-content">
<div class="mat-content" *ngIf="hasBasicModeDirective; else basicModeContent">
<ng-container #basicModeContainer></ng-container>
<div class="tb-basic-mode-directive-error" *ngIf="basicModeDirectiveError">{{basicModeDirectiveError}}</div>
</div>
<ng-template #basicModeContent>
<div class="mat-content">
<ng-container *ngTemplateOutlet="data"></ng-container>
<ng-container *ngTemplateOutlet="appearance"></ng-container>
<ng-container [formGroup]="actionsSettings">
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>
</div>
</ng-template>
</div> </div>
</mat-tab> </ng-template>
<mat-tab label="{{ 'widget-config.actions' | translate }}" [formGroup]="actionsSettings"> <ng-template #data>
<tb-manage-widget-actions <ng-container *ngIf="displayTimewindowConfig" [formGroup]="dataSettings">
[callbacks]="widgetConfigCallbacks" <tb-timewindow-config-panel
[widgetType] = "modelValue.widgetType" [onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="actionsData"> formControlName="timewindowConfig">
</tb-manage-widget-actions> </tb-timewindow-config-panel>
</mat-tab> </ng-container>
</mat-tab-group> <div *ngIf="widgetType === widgetTypes.alarm" [formGroup]="dataSettings" class="tb-widget-config-panel" fxLayout="column" fxLayoutAlign="center">
<tb-alarm-filter-config buttonMode="false" formControlName="alarmFilterConfig"></tb-alarm-filter-config>
</div>
<div *ngIf="widgetType !== widgetTypes.rpc &&
widgetType !== widgetTypes.alarm &&
modelValue?.isDataEnabled"
[formGroup]="dataSettings">
<tb-datasources
[configMode]="widgetConfigMode"
formControlName="datasources">
</tb-datasources>
</div>
<div class="tb-widget-config-panel" *ngIf="widgetType === widgetTypes.rpc &&
modelValue?.isDataEnabled">
<div class="tb-widget-config-panel-title" translate>widget-config.target-device</div>
<div [formGroup]="targetDeviceSettings">
<tb-entity-alias-select fxFlex
[tbRequired]="!widgetEditMode"
[aliasController]="aliasController"
[allowedEntityTypes]="[entityTypes.DEVICE]"
[callbacks]="widgetConfigCallbacks"
formControlName="targetDeviceAliasId">
</tb-entity-alias-select>
</div>
</div>
<div *ngIf="widgetType === widgetTypes.alarm && modelValue?.isDataEnabled"
[formGroup]="dataSettings" class="tb-widget-config-panel" >
<div class="tb-widget-config-panel-title" translate>widget-config.alarm-source</div>
<tb-datasource
formControlName="alarmSource">
</tb-datasource>
</div>
<div *ngIf="displayLimits" class="tb-widget-config-panel" [formGroup]="widgetSettings">
<div class="tb-widget-config-panel-title" translate>widget-config.limits</div>
<div class="tb-widget-config-row space-between">
<div translate>widget-config.data-page-size</div>
<mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput formControlName="pageSize" type="number" min="1" step="1">
</mat-form-field>
</div>
</div>
</ng-template>
<ng-template #appearance>
<div *ngIf="displayAppearanceDataSettings" class="tb-widget-config-panel" [formGroup]="widgetSettings">
<div class="tb-widget-config-panel-title" translate>widget-config.data-settings</div>
<div *ngIf="displayUnitsConfig" class="tb-widget-config-row space-between">
<div translate>widget-config.units</div>
<tb-widget-units
formControlName="units">
</tb-widget-units>
</div>
<div *ngIf="displayUnitsConfig" class="tb-widget-config-row space-between">
<div translate>widget-config.decimals</div>
<mat-form-field appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div *ngIf="displayNoDataDisplayMessageConfig" class="tb-widget-config-row">
<div translate>widget-config.no-data-display-message</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="noDataDisplayMessage" placeholder="{{ 'widget-config.set-message' | translate }}">
</mat-form-field>
</div>
</div>
<div *ngIf="displayAdvancedAppearance" style="height: 100%;" [formGroup]="advancedSettings">
<tb-widget-settings
[aliasController]="aliasController"
[dashboard]="dashboard"
[widget]="widget"
formControlName="settings">
</tb-widget-settings>
</div>
</ng-template>
</div>

174
ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss

@ -13,118 +13,46 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import "../../../../../theme";
@import '../../../../../scss/constants'; @import '../../../../../scss/constants';
.tb-widget-config { .tb-widget-config {
.tb-panel-hint { display: flex;
font-size: 12px; flex-direction: column;
color: #808080; gap: 16px;
} .tb-widget-config-header {
} padding: 24px 24px 8px;
height: 56px;
.tb-datasource-list-item {
&.mat-mdc-list-item {
height: auto;
min-height: 68px;
display: block;
padding: 0;
}
&.tb-draggable {
&.cdk-drag-preview {
background: #fff;
}
}
}
.tb-datasource-params {
position: relative;
padding: 0 0 0 10px;
margin: 5px;
tb-error.tb-datasource-error {
position: absolute;
bottom: 4px;
left: 8px;
}
.tb-datasource-section {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
align-items: stretch; align-items: center;
justify-content: space-between;
}
.tb-widget-config-content {
flex: 1; flex: 1;
padding-top: 20px; overflow: auto;
@media #{$mat-gt-sm} { & > div {
flex-direction: row; padding: 16px;
align-items: center;
justify-content: flex-start;
} }
} .mat-content {
.tb-datasource-type { display: flex;
min-width: 160px; flex-direction: column;
@media #{$mat-gt-sm} { gap: 16px;
max-width: 160px;
} }
} }
.tb-datasource { .tb-basic-mode-directive-error {
@media #{$mat-gt-sm} { font-size: 13px;
padding-left: 8px; font-weight: 400;
width: 208px; color: rgb(221, 44, 0);
max-width: 208px;
}
}
.tb-data-keys {
@media #{$mat-gt-sm} {
padding-left: 8px;
}
}
}
:host {
.tb-widget-config {
.tb-advanced-widget-config {
height: 100%;
}
.tb-datasources {
.handle {
cursor: move;
}
.mat-mdc-list {
min-height: 68px;
padding-left: 0;
}
}
.fields-group {
padding: 0 16px 8px;
margin-bottom: 10px;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
legend {
color: rgba(0, 0, 0, .7);
width: fit-content;
}
}
.fields-group-slider {
padding: 0;
legend {
margin-left: 16px;
}
.tb-settings {
padding: 0 16px 8px;
}
}
.tb-widget-style {
margin-top: 16px;
}
} }
} }
:host ::ng-deep { :host ::ng-deep {
.tb-widget-config { .tb-widget-config {
tb-alarm-filter-config { .tb-widget-config-header {
.mdc-button { .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
width: 100%; padding: 0 20px;
height: 100%;
justify-content: flex-start;
} }
} }
.mat-mdc-tab-body-wrapper { .mat-mdc-tab-body-wrapper {
@ -134,53 +62,5 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
} }
.mat-expansion-panel {
&.tb-settings {
box-shadow: none;
.mat-content {
overflow: visible;
}
.mat-expansion-panel-header {
padding: 0;
&:hover {
background: none;
}
.mat-expansion-indicator {
padding: 2px;
}
}
.mat-expansion-panel-header-description {
align-items: center;
}
> .mat-expansion-panel-content {
> .mat-expansion-panel-body {
padding: 0;
}
}
.tb-json-object-panel, .tb-css-content-panel {
margin: 0 0 8px;
}
}
&.tb-datasources {
&.mat-expanded {
overflow: visible;
}
.mat-expansion-panel-body{
padding: 0 12px 16px;
}
}
.mat-expansion-panel-content {
font: inherit;
}
}
.mat-slide {
margin: 8px 0;
}
.slide-block {
display: block;
&:not(:last-child) {
margin-bottom: 8px;
}
}
} }
} }

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

File diff suppressed because it is too large

31
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html

@ -0,0 +1,31 @@
<!--
Copyright © 2016-2023 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-dashboard class="tb-preview-dashboard"
[aliasController]="aliasController"
[stateController]="stateController"
[widgets]="widgets"
[autofillHeight]="true"
[columns]="24"
[isEdit]="false"
[isMobileDisabled]="true"
[isEditActionEnabled]="false"
[isRemoveActionEnabled]="false">
</tb-dashboard>
<div class="tb-preview-panel">
<ng-content select=".tb-preview-panel-content"></ng-content>
</div>

39
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2023 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.
*/
:host {
z-index: 10;
background: #F3F6FA;
.tb-preview-dashboard {
position: absolute;
top: 15%;
bottom: 15%;
left: 0;
right: 0;
}
.tb-preview-panel {
position: absolute;
top: 16px;
left: 24px;
}
}
:host ::ng-deep {
tb-dashboard.tb-preview-dashboard {
.tb-dashboard-content {
background-color: transparent !important;
}
}
}

79
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts

@ -0,0 +1,79 @@
///
/// Copyright © 2016-2023 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, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { Widget, WidgetConfig } from '@shared/models/widget.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-widget-preview',
templateUrl: './widget-preview.component.html',
styleUrls: ['./widget-preview.component.scss']
})
export class WidgetPreviewComponent extends PageComponent implements OnInit, OnChanges {
@Input()
aliasController: IAliasController;
@Input()
stateController: IStateController;
@Input()
widget: Widget;
@Input()
widgetConfig: WidgetConfig;
widgets: Widget[];
constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.loadPreviewWidget();
}
ngOnChanges(changes: SimpleChanges): void {
let reloadPreviewWidget = false;
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['widget', 'widgetConfig'].includes(propName)) {
reloadPreviewWidget = true;
}
}
}
if (reloadPreviewWidget) {
this.loadPreviewWidget();
}
}
private loadPreviewWidget() {
const widget = deepClone(this.widget);
widget.sizeX = 24;
widget.sizeY = this.widget.sizeY * 2;
widget.row = 0;
widget.col = 0;
widget.config = this.widgetConfig;
this.widgets = [widget];
}
}

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

@ -15,23 +15,11 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-absolute-fill" [fxLayout]="legendContainerLayoutType" tb-toast <div class="tb-absolute-fill" fxLayout="column" tb-toast
toastTarget="{{ toastTargetId }}"> toastTarget="{{ toastTargetId }}">
<tb-legend *ngIf="displayLegend && isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
<div fxFlex id="widget-container"> <div fxFlex id="widget-container">
<ng-container #widgetContent></ng-container> <ng-container #widgetContent></ng-container>
</div> </div>
<tb-legend *ngIf="displayLegend && !isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
</div> </div>
<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData"> <div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">
<span [innerHtml]="('Widget Error:<br/><br/>' + widgetErrorData.message) | safe:'html'"></span> <span [innerHtml]="('Widget Error:<br/><br/>' + widgetErrorData.message) | safe:'html'"></span>
@ -42,5 +30,5 @@
class="tb-absolute-fill">{{ noDataDisplayMessageText }}</span> class="tb-absolute-fill">{{ noDataDisplayMessageText }}</span>
</div> </div>
<div class="tb-absolute-fill tb-widget-loading" [fxShow]="loadingData" fxLayout="column" fxLayoutAlign="center center"> <div class="tb-absolute-fill tb-widget-loading" [fxShow]="loadingData" fxLayout="column" fxLayoutAlign="center center">
<mat-spinner color="accent" md-mode="indeterminate" diameter="40"></mat-spinner> <mat-spinner color="accent" mode="indeterminate" diameter="40"></mat-spinner>
</div> </div>

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

@ -39,10 +39,6 @@ import {
} from '@angular/core'; } from '@angular/core';
import { DashboardWidget } from '@home/models/dashboard-component.models'; import { DashboardWidget } from '@home/models/dashboard-component.models';
import { import {
defaultLegendConfig,
LegendConfig,
LegendData,
LegendPosition,
Widget, Widget,
WidgetActionDescriptor, WidgetActionDescriptor,
widgetActionSources, widgetActionSources,
@ -152,13 +148,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
displayNoData = false; displayNoData = false;
noDataDisplayMessageText: string; noDataDisplayMessageText: string;
displayLegend: boolean;
legendConfig: LegendConfig;
legendData: LegendData;
isLegendFirst: boolean;
legendContainerLayoutType: string;
legendStyle: {[klass: string]: any};
dynamicWidgetComponentRef: ComponentRef<IDynamicWidgetComponent>; dynamicWidgetComponentRef: ComponentRef<IDynamicWidgetComponent>;
dynamicWidgetComponent: IDynamicWidgetComponent; dynamicWidgetComponent: IDynamicWidgetComponent;
@ -220,57 +209,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widget = this.dashboardWidget.widget; this.widget = this.dashboardWidget.widget;
this.displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend
: this.widget.type === widgetType.timeseries;
this.legendContainerLayoutType = 'column';
if (this.displayLegend) {
this.legendConfig = this.widget.config.legendConfig || defaultLegendConfig(this.widget.type);
this.legendData = {
keys: [],
data: []
};
if (this.legendConfig.position === LegendPosition.top ||
this.legendConfig.position === LegendPosition.bottom) {
this.legendContainerLayoutType = 'column';
this.isLegendFirst = this.legendConfig.position === LegendPosition.top;
} else {
this.legendContainerLayoutType = 'row';
this.isLegendFirst = this.legendConfig.position === LegendPosition.left;
}
switch (this.legendConfig.position) {
case LegendPosition.top:
this.legendStyle = {
paddingBottom: '8px',
maxHeight: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.bottom:
this.legendStyle = {
paddingTop: '8px',
maxHeight: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.left:
this.legendStyle = {
paddingRight: '0px',
maxWidth: '50%',
overflowY: 'auto'
};
break;
case LegendPosition.right:
this.legendStyle = {
paddingLeft: '0px',
maxWidth: '50%',
overflowY: 'auto'
};
break;
}
}
const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {}; const actionDescriptorsBySourceId: {[actionSourceId: string]: Array<WidgetActionDescriptor>} = {};
if (this.widget.config.actions) { if (this.widget.config.actions) {
for (const actionSourceId of Object.keys(this.widget.config.actions)) { for (const actionSourceId of Object.keys(this.widget.config.actions)) {
@ -463,13 +401,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
} }
public onLegendKeyHiddenChange(index: number) {
for (const id of Object.keys(this.widgetContext.subscriptions)) {
const subscription = this.widgetContext.subscriptions[id];
subscription.updateDataVisibility(index);
}
}
private loadFromWidgetInfo() { private loadFromWidgetInfo() {
this.widgetContext.widgetNamespace = this.widgetContext.widgetNamespace =
`widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`; `widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`;
@ -884,9 +815,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
this.createSubscription(options, subscribe).subscribe( this.createSubscription(options, subscribe).subscribe(
(subscription) => { (subscription) => {
if (useDefaultComponents) {
this.defaultSubscriptionOptions(subscription, options);
}
createSubscriptionSubject.next(subscription); createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete(); createSubscriptionSubject.complete();
}, },
@ -904,8 +832,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
? this.widget.config.displayTimewindow : !options.useDashboardTimewindow; ? this.widget.config.displayTimewindow : !options.useDashboardTimewindow;
options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow; options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow;
options.legendConfig = null; options.legendConfig = null;
if (this.displayLegend) { if (this.widget.config.settings.showLegend === true) {
options.legendConfig = this.legendConfig; options.legendConfig = this.widget.config.settings.legendConfig;
} }
options.decimals = this.widgetContext.decimals; options.decimals = this.widgetContext.decimals;
options.units = this.widgetContext.units; options.units = this.widgetContext.units;
@ -974,13 +902,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}); });
} }
}; };
}
private defaultSubscriptionOptions(subscription: IWidgetSubscription, options: WidgetSubscriptionOptions) {
if (this.displayLegend) {
this.legendData = subscription.legendData;
}
} }
private createDefaultSubscription(): Observable<any> { private createDefaultSubscription(): Observable<any> {
@ -1011,7 +932,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.createSubscription(options).subscribe( this.createSubscription(options).subscribe(
(subscription) => { (subscription) => {
this.defaultSubscriptionOptions(subscription, options);
// backward compatibility // backward compatibility
this.widgetContext.datasources = subscription.datasources; this.widgetContext.datasources = subscription.datasources;
@ -1491,7 +1411,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
} }
private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>, actionDescriptor: WidgetActionDescriptor): Observable<any> { private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array<WidgetResource>,
actionDescriptor: WidgetActionDescriptor): Observable<any> {
const resourceTasks: Observable<string>[] = []; const resourceTasks: Observable<string>[] = [];
const modulesTasks: Observable<ModulesWithFactories | string>[] = []; const modulesTasks: Observable<ModulesWithFactories | string>[] = [];

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

@ -447,6 +447,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
} }
export interface WidgetConfigComponentData { export interface WidgetConfigComponentData {
widgetName: string;
config: WidgetConfig; config: WidgetConfig;
layout: WidgetLayout; layout: WidgetLayout;
widgetType: widgetType; widgetType: widgetType;
@ -459,6 +460,7 @@ export interface WidgetConfigComponentData {
settingsDirective: string; settingsDirective: string;
dataKeySettingsDirective: string; dataKeySettingsDirective: string;
latestDataKeySettingsDirective: string; latestDataKeySettingsDirective: string;
basicModeDirective: string;
} }
export const MissingWidgetType: WidgetInfo = { export const MissingWidgetType: WidgetInfo = {
@ -551,6 +553,8 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
settingsDirective: widgetTypeEntity.descriptor.settingsDirective, settingsDirective: widgetTypeEntity.descriptor.settingsDirective,
dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective, dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective, latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective,
hasBasicMode: widgetTypeEntity.descriptor.hasBasicMode,
basicModeDirective: widgetTypeEntity.descriptor.basicModeDirective,
defaultConfig: widgetTypeEntity.descriptor.defaultConfig defaultConfig: widgetTypeEntity.descriptor.defaultConfig
}; };
} }
@ -581,6 +585,8 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
settingsDirective: widgetInfo.settingsDirective, settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective, latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
hasBasicMode: widgetInfo.hasBasicMode,
basicModeDirective: widgetInfo.basicModeDirective,
defaultConfig: widgetInfo.defaultConfig defaultConfig: widgetInfo.defaultConfig
}; };
return { return {

11
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -270,6 +270,17 @@
[(ngModel)]="widget.latestDataKeySettingsDirective" [(ngModel)]="widget.latestDataKeySettingsDirective"
(ngModelChange)="isDirty = true"/> (ngModelChange)="isDirty = true"/>
</mat-form-field> </mat-form-field>
<mat-slide-toggle class="mat-block" style="padding-bottom: 16px;"
[(ngModel)]="widget.hasBasicMode"
(ngModelChange)="isDirty = true">
{{ 'widget.has-basic-mode' | translate }}
</mat-slide-toggle>
<mat-form-field *ngIf="widget.hasBasicMode" class="mat-block">
<mat-label translate>widget.basic-mode-form-selector</mat-label>
<input matInput
[(ngModel)]="widget.basicModeDirective"
(ngModelChange)="isDirty = true"/>
</mat-form-field>
</div> </div>
</div> </div>
</mat-tab> </mat-tab>

7
ui-ngx/src/app/shared/components/color-input.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field [formGroup]="colorFormGroup"> <mat-form-field *ngIf="!asBoxInput; else boxInput" [formGroup]="colorFormGroup">
<mat-label *ngIf="icon || label"> <mat-label *ngIf="icon || label">
<mat-icon *ngIf="icon">{{icon}}</mat-icon> <mat-icon *ngIf="icon">{{icon}}</mat-icon>
<span *ngIf="label">{{label}}</span> <span *ngIf="label">{{label}}</span>
@ -34,3 +34,8 @@
{{ requiredText }} {{ requiredText }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<ng-template #boxInput>
<div class="tb-color-preview no-margin box" [ngClass]="{'disabled': disabled}" (click)="!disabled && showColorPicker()">
<div class="tb-color-result" [ngStyle]="!disabled ? {background: colorFormGroup.get('color').value} : {}"></div>
</div>
</ng-template>

3
ui-ngx/src/app/shared/components/color-input.component.scss

@ -25,5 +25,8 @@
.tb-color-preview { .tb-color-preview {
margin-left: 8px; margin-left: 8px;
margin-right: 5px; margin-right: 5px;
&.no-margin {
margin: 0;
}
} }
} }

12
ui-ngx/src/app/shared/components/color-input.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -22,6 +22,7 @@ import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_AC
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-color-input', selector: 'tb-color-input',
@ -37,6 +38,10 @@ import { DialogService } from '@core/services/dialog.service';
}) })
export class ColorInputComponent extends PageComponent implements OnInit, ControlValueAccessor { export class ColorInputComponent extends PageComponent implements OnInit, ControlValueAccessor {
@Input()
@coerceBoolean()
asBoxInput = false;
@Input() @Input()
icon: string; icon: string;
@ -95,7 +100,8 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private dialogs: DialogService, private dialogs: DialogService,
private translate: TranslateService, private translate: TranslateService,
private fb: UntypedFormBuilder) { private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) {
super(store); super(store);
} }
@ -154,6 +160,7 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
this.colorFormGroup.patchValue( this.colorFormGroup.patchValue(
{color}, {emitEvent: true} {color}, {emitEvent: true}
); );
this.cd.markForCheck();
} }
} }
); );
@ -161,5 +168,6 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
clear() { clear() {
this.colorFormGroup.get('color').patchValue(null, {emitEvent: true}); this.colorFormGroup.get('color').patchValue(null, {emitEvent: true});
this.cd.markForCheck();
} }
} }

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

@ -38,7 +38,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { isEqual } from '@core/utils'; import { isDefinedAndNotNull, isEqual } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
@ -291,7 +291,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
async writeValue(value: string | EntityId | null): Promise<void> { async writeValue(value: string | EntityId | null): Promise<void> {
this.searchText = ''; this.searchText = '';
if (value !== null && (typeof value === 'string' || (value.entityType && value.id))) { if (isDefinedAndNotNull(value) && (typeof value === 'string' || (value.entityType && value.id))) {
let targetEntityType: EntityType; let targetEntityType: EntityType;
let id: string; let id: string;
if (typeof value === 'string') { if (typeof value === 'string') {

9
ui-ngx/src/app/shared/components/material-icon-select.component.html

@ -15,8 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<div fxLayout="row" [formGroup]="materialIconFormGroup"> <div *ngIf="!asBoxInput; else boxInput" fxLayout="row" [formGroup]="materialIconFormGroup">
<mat-icon class="icon-value" (click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon> <mat-icon class="icon-value" [ngStyle]="color ? { color: color } : {}" (click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon>
<mat-form-field fxFlex> <mat-form-field fxFlex>
<mat-label>{{ label }}</mat-label> <mat-label>{{ label }}</mat-label>
<input [required]="required" matInput formControlName="icon" (mousedown)="openIconDialog()"> <input [required]="required" matInput formControlName="icon" (mousedown)="openIconDialog()">
@ -28,3 +28,8 @@
</button> </button>
</mat-form-field> </mat-form-field>
</div> </div>
<ng-template #boxInput>
<mat-icon class="icon-box" [ngStyle]="color && !disabled ? { color: color } : {}"
[ngClass]="{'disabled': disabled}"
(click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon>
</ng-template>

29
ui-ngx/src/app/shared/components/material-icon-select.component.scss

@ -14,11 +14,28 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.mat-icon.icon-value { .mat-icon {
padding: 4px; &.icon-value {
margin: 12px 4px 4px; padding: 4px;
cursor: pointer; margin: 12px 4px 4px;
border: solid 1px rgba(0, 0, 0, .27); cursor: pointer;
box-sizing: initial; border: solid 1px rgba(0, 0, 0, .27);
box-sizing: initial;
}
&.icon-box {
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
cursor: pointer;
box-sizing: border-box;
padding: 8px;
height: 40px;
width: 40px;
font-size: 22px;
vertical-align: middle;
&.disabled {
cursor: initial;
color: rgba(0, 0, 0, 0.38);
}
}
} }
} }

15
ui-ngx/src/app/shared/components/material-icon-select.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -22,6 +22,7 @@ import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_AC
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-material-icon-select', selector: 'tb-material-icon-select',
@ -37,9 +38,16 @@ import { TranslateService } from '@ngx-translate/core';
}) })
export class MaterialIconSelectComponent extends PageComponent implements OnInit, ControlValueAccessor { export class MaterialIconSelectComponent extends PageComponent implements OnInit, ControlValueAccessor {
@Input()
@coerceBoolean()
asBoxInput = false;
@Input() @Input()
label = this.translate.instant('icon.icon'); label = this.translate.instant('icon.icon');
@Input()
color: string;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -73,7 +81,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private dialogs: DialogService, private dialogs: DialogService,
private translate: TranslateService, private translate: TranslateService,
private fb: UntypedFormBuilder) { private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) {
super(store); super(store);
} }
@ -126,6 +135,7 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
this.materialIconFormGroup.patchValue( this.materialIconFormGroup.patchValue(
{icon}, {emitEvent: true} {icon}, {emitEvent: true}
); );
this.cd.markForCheck();
} }
} }
); );
@ -134,5 +144,6 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
clear() { clear() {
this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true}); this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true});
this.cd.markForCheck();
} }
} }

1
ui-ngx/src/app/shared/components/public-api.ts

@ -22,3 +22,4 @@ export * from './js-func.component';
export * from './script-lang.component'; export * from './script-lang.component';
export * from './slack-conversation-autocomplete.component'; export * from './slack-conversation-autocomplete.component';
export * from './notification/template-autocomplete.component'; export * from './notification/template-autocomplete.component';
export * from './toggle-header.component';

7
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.html → ui-ngx/src/app/shared/components/toggle-header.component.html

@ -15,9 +15,12 @@
limitations under the License. limitations under the License.
--> -->
<mat-button-toggle-group *ngIf="!isMdLg || !useSelectOnMdLg; else select" class="tb-toggle-header" [name]="name" [(ngModel)]="value" <mat-button-toggle-group *ngIf="!isMdLg || !useSelectOnMdLg; else select" class="tb-toggle-header"
[ngClass]="{'tb-fill': (appearance === 'fill' || appearance === 'fill-invert'),
'tb-invert': appearance === 'fill-invert',
'tb-ignore-md-lg': ignoreMdLgSize}" [name]="name" [(ngModel)]="value"
(ngModelChange)="valueChange.emit(value)"> (ngModelChange)="valueChange.emit(value)">
<mat-button-toggle *ngFor="let option of options" [value]="option.value">{{ option.name }}</mat-button-toggle> <mat-button-toggle *ngFor="let option of options; trackBy: trackByHeaderOption" [value]="option.value">{{ option.name }}</mat-button-toggle>
</mat-button-toggle-group> </mat-button-toggle-group>
<ng-template #select> <ng-template #select>
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic">

30
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.scss → ui-ngx/src/app/shared/components/toggle-header.component.scss

@ -14,7 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
@import "../../../../../../../scss/constants"; @import "../../../theme";
@import "../../../scss/constants";
:host ::ng-deep { :host ::ng-deep {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
@ -47,8 +48,8 @@
&.mat-button-toggle-checked { &.mat-button-toggle-checked {
.mat-button-toggle-button { .mat-button-toggle-button {
background: #F3F6FA; background: #F3F6FA;
color: #305680; color: $tb-primary-color;
border: 1px solid #305680; border: 1px solid $tb-primary-color;
border-radius: 20px; border-radius: 20px;
.mat-button-toggle-label-content { .mat-button-toggle-label-content {
@ -58,9 +59,30 @@
} }
} }
} }
&.tb-fill {
.mat-button-toggle.mat-button-toggle-appearance-standard {
&.mat-button-toggle-checked {
.mat-button-toggle-button {
background: $tb-primary-color;
color: #FFFFFF;
}
}
}
&.tb-invert {
.mat-button-toggle.mat-button-toggle-appearance-standard {
color: rgba(255, 255, 255, 0.8);
&.mat-button-toggle-checked {
.mat-button-toggle-button {
background: #FFFFFF;
color: $tb-primary-color;
}
}
}
}
}
} }
@media #{$mat-md-lg} { @media #{$mat-md-lg} {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header:not(.tb-ignore-md-lg) {
height: 24px; height: 24px;
.mat-button-toggle.mat-button-toggle-appearance-standard { .mat-button-toggle.mat-button-toggle-appearance-standard {
.mat-button-toggle-button { .mat-button-toggle-button {

14
ui-ngx/src/app/modules/home/components/widget/lib/home-page/toggle-header.component.ts → ui-ngx/src/app/shared/components/toggle-header.component.ts

@ -38,12 +38,15 @@ import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { BreadCrumb } from '@shared/components/breadcrumb';
export interface ToggleHeaderOption { export interface ToggleHeaderOption {
name: string; name: string;
value: any; value: any;
} }
export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked';
@Component({ @Component({
selector: 'tb-toggle-header', selector: 'tb-toggle-header',
templateUrl: './toggle-header.component.html', templateUrl: './toggle-header.component.html',
@ -67,6 +70,13 @@ export class ToggleHeaderComponent extends PageComponent implements OnInit {
@coerceBoolean() @coerceBoolean()
useSelectOnMdLg = true; useSelectOnMdLg = true;
@Input()
@coerceBoolean()
ignoreMdLgSize = false;
@Input()
appearance: ToggleHeaderAppearance = 'stroked';
isMdLg: boolean; isMdLg: boolean;
private observeBreakpointSubscription: Subscription; private observeBreakpointSubscription: Subscription;
@ -87,4 +97,8 @@ export class ToggleHeaderComponent extends PageComponent implements OnInit {
} }
); );
} }
trackByHeaderOption(index: number, option: ToggleHeaderOption){
return option.value;
}
} }

8
ui-ngx/src/app/shared/models/query/query.models.ts

@ -749,6 +749,14 @@ export function createDefaultEntityDataPageLink(pageSize: number): EntityDataPag
export const singleEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1); export const singleEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1);
export const singleEntityFilterFromDeviceId = (deviceId: string): EntityFilter => ({
type: AliasFilterType.singleEntity,
singleEntity: {
entityType: EntityType.DEVICE,
id: deviceId
}
});
export interface EntityCountQuery { export interface EntityCountQuery {
entityFilter: EntityFilter; entityFilter: EntityFilter;
keyFilters?: Array<KeyFilter>; keyFilters?: Array<KeyFilter>;

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

@ -162,6 +162,8 @@ export interface WidgetTypeDescriptor {
settingsDirective?: string; settingsDirective?: string;
dataKeySettingsDirective?: string; dataKeySettingsDirective?: string;
latestDataKeySettingsDirective?: string; latestDataKeySettingsDirective?: string;
hasBasicMode?: boolean;
basicModeDirective?: string;
defaultConfig: string; defaultConfig: string;
sizeX: number; sizeX: number;
sizeY: number; sizeY: number;
@ -252,18 +254,16 @@ export interface LegendConfig {
showLatest: boolean; showLatest: boolean;
} }
export function defaultLegendConfig(wType: widgetType): LegendConfig { export const defaultLegendConfig = (wType: widgetType): LegendConfig => ({
return { direction: LegendDirection.column,
direction: LegendDirection.column, position: LegendPosition.bottom,
position: LegendPosition.bottom, sortDataKeys: false,
sortDataKeys: false, showMin: false,
showMin: false, showMax: false,
showMax: false, showAvg: wType === widgetType.timeseries,
showAvg: wType === widgetType.timeseries, showTotal: false,
showTotal: false, showLatest: false
showLatest: false });
};
}
export enum ComparisonResultType { export enum ComparisonResultType {
PREVIOUS_VALUE = 'PREVIOUS_VALUE', PREVIOUS_VALUE = 'PREVIOUS_VALUE',
@ -320,6 +320,7 @@ export interface DataKey extends KeyInfo {
export enum DatasourceType { export enum DatasourceType {
function = 'function', function = 'function',
device = 'device',
entity = 'entity', entity = 'entity',
entityCount = 'entityCount', entityCount = 'entityCount',
alarmCount = 'alarmCount' alarmCount = 'alarmCount'
@ -328,6 +329,7 @@ export enum DatasourceType {
export const datasourceTypeTranslationMap = new Map<DatasourceType, string>( export const datasourceTypeTranslationMap = new Map<DatasourceType, string>(
[ [
[ DatasourceType.function, 'function.function' ], [ DatasourceType.function, 'function.function' ],
[ DatasourceType.device, 'device.device' ],
[ DatasourceType.entity, 'entity.entity' ], [ DatasourceType.entity, 'entity.entity' ],
[ DatasourceType.entityCount, 'entity.entities-count' ], [ DatasourceType.entityCount, 'entity.entities-count' ],
[ DatasourceType.alarmCount, 'entity.alarms-count' ] [ DatasourceType.alarmCount, 'entity.alarms-count' ]
@ -343,6 +345,7 @@ export interface Datasource {
entityType?: EntityType; entityType?: EntityType;
entityId?: string; entityId?: string;
entityName?: string; entityName?: string;
deviceId?: string;
entityAliasId?: string; entityAliasId?: string;
filterId?: string; filterId?: string;
unresolvedStateEntity?: boolean; unresolvedStateEntity?: boolean;
@ -363,7 +366,7 @@ export interface Datasource {
[key: string]: any; [key: string]: any;
} }
export function datasourcesHasAggregation(datasources?: Array<Datasource>): boolean { export const datasourcesHasAggregation = (datasources?: Array<Datasource>): boolean => {
if (datasources) { if (datasources) {
const foundDatasource = datasources.find(datasource => { const foundDatasource = datasources.find(datasource => {
const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries && const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries &&
@ -375,9 +378,9 @@ export function datasourcesHasAggregation(datasources?: Array<Datasource>): bool
} }
} }
return false; return false;
} };
export function datasourcesHasOnlyComparisonAggregation(datasources?: Array<Datasource>): boolean { export const datasourcesHasOnlyComparisonAggregation = (datasources?: Array<Datasource>): boolean => {
if (!datasourcesHasAggregation(datasources)) { if (!datasourcesHasAggregation(datasources)) {
return false; return false;
} }
@ -392,7 +395,7 @@ export function datasourcesHasOnlyComparisonAggregation(datasources?: Array<Data
} }
} }
return true; return true;
} };
export interface FormattedData { export interface FormattedData {
$datasource: Datasource; $datasource: Datasource;
@ -602,7 +605,13 @@ export interface WidgetSettings {
[key: string]: any; [key: string]: any;
} }
export enum WidgetConfigMode {
basic = 'basic',
advanced = 'advanced'
}
export interface WidgetConfig { export interface WidgetConfig {
configMode?: WidgetConfigMode;
title?: string; title?: string;
titleIcon?: string; titleIcon?: string;
showTitle?: boolean; showTitle?: boolean;
@ -614,8 +623,6 @@ export interface WidgetConfig {
enableFullscreen?: boolean; enableFullscreen?: boolean;
useDashboardTimewindow?: boolean; useDashboardTimewindow?: boolean;
displayTimewindow?: boolean; displayTimewindow?: boolean;
showLegend?: boolean;
legendConfig?: LegendConfig;
timewindow?: Timewindow; timewindow?: Timewindow;
desktopHide?: boolean; desktopHide?: boolean;
mobileHide?: boolean; mobileHide?: boolean;
@ -700,7 +707,7 @@ export interface IWidgetSettingsComponent {
[key: string]: any; [key: string]: any;
} }
function removeEmptyWidgetSettings(settings: WidgetSettings): WidgetSettings { const removeEmptyWidgetSettings = (settings: WidgetSettings): WidgetSettings => {
if (settings) { if (settings) {
const keys = Object.keys(settings); const keys = Object.keys(settings);
for (const key of keys) { for (const key of keys) {
@ -711,7 +718,7 @@ function removeEmptyWidgetSettings(settings: WidgetSettings): WidgetSettings {
} }
} }
return settings; return settings;
} };
@Directive() @Directive()
// eslint-disable-next-line @angular-eslint/directive-class-suffix // eslint-disable-next-line @angular-eslint/directive-class-suffix

7
ui-ngx/src/app/shared/shared.module.ts

@ -190,6 +190,7 @@ import {
} from '@shared/layout/layout.directives'; } from '@shared/layout/layout.directives';
import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component'; import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component';
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe'; import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
import { ToggleHeaderComponent } from '@shared/components/toggle-header.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService; return markedOptionsService;
@ -358,7 +359,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutAlignDirective, GtMdLgLayoutAlignDirective,
GtMdLgLayoutGapDirective, GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective, GtMdLgShowHideDirective,
ColorPickerComponent ColorPickerComponent,
ToggleHeaderComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -583,7 +585,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutAlignDirective, GtMdLgLayoutAlignDirective,
GtMdLgLayoutGapDirective, GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective, GtMdLgShowHideDirective,
ColorPickerComponent ColorPickerComponent,
ToggleHeaderComponent
] ]
}) })
export class SharedModule { } export class SharedModule { }

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

@ -35,6 +35,7 @@
"edit-mode": "Edit mode", "edit-mode": "Edit mode",
"enter-edit-mode": "Enter edit mode", "enter-edit-mode": "Enter edit mode",
"decline-changes": "Decline changes", "decline-changes": "Decline changes",
"decline": "Decline",
"close": "Close", "close": "Close",
"back": "Back", "back": "Back",
"run": "Run", "run": "Run",
@ -1114,6 +1115,10 @@
"function-types": "Function types", "function-types": "Function types",
"function-type": "Function type", "function-type": "Function type",
"function-types-required": "Function types are required.", "function-types-required": "Function types are required.",
"data-keys": "Data keys",
"data-key": "Data key",
"data-keys-required": "Data keys are required.",
"data-key-required": "Data key is required.",
"alarm-keys": "Alarm data keys", "alarm-keys": "Alarm data keys",
"alarm-key": "Alarm data key", "alarm-key": "Alarm data key",
"alarm-key-functions": "Alarm key functions", "alarm-key-functions": "Alarm key functions",
@ -1974,6 +1979,7 @@
"no-aliases-found": "No aliases found.", "no-aliases-found": "No aliases found.",
"no-alias-matching": "'{{alias}}' not found.", "no-alias-matching": "'{{alias}}' not found.",
"create-new-alias": "Create a new one!", "create-new-alias": "Create a new one!",
"create-new": "Create new",
"key": "Key", "key": "Key",
"key-name": "Key name", "key-name": "Key name",
"no-keys-found": "No keys found.", "no-keys-found": "No keys found.",
@ -2434,6 +2440,7 @@
"add-filter-prompt": "Please add filter", "add-filter-prompt": "Please add filter",
"no-filter-matching": "'{{filter}}' not found.", "no-filter-matching": "'{{filter}}' not found.",
"create-new-filter": "Create a new one!", "create-new-filter": "Create a new one!",
"create-new": "Create new",
"filter-required": "Filter is required.", "filter-required": "Filter is required.",
"operation": { "operation": {
"operation": "Operation", "operation": "Operation",
@ -3992,6 +3999,10 @@
"settings-form-selector": "Settings form selector", "settings-form-selector": "Settings form selector",
"data-key-settings-form-selector": "Data key settings form selector", "data-key-settings-form-selector": "Data key settings form selector",
"latest-data-key-settings-form-selector": "Latest data key settings form selector", "latest-data-key-settings-form-selector": "Latest data key settings form selector",
"has-basic-mode": "Has basic mode",
"basic-mode-form-selector": "Basic mode form selector",
"basic-mode": "Basic",
"advanced-mode": "Advanced",
"javascript": "Javascript", "javascript": "Javascript",
"js": "JS", "js": "JS",
"remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?",
@ -4106,6 +4117,9 @@
"data": "Data", "data": "Data",
"settings": "Settings", "settings": "Settings",
"advanced": "Advanced", "advanced": "Advanced",
"appearance": "Appearance",
"widget-card": "Widget card",
"mobile": "Mobile",
"title": "Title", "title": "Title",
"title-tooltip": "Title Tooltip", "title-tooltip": "Title Tooltip",
"general-settings": "General settings", "general-settings": "General settings",
@ -4126,12 +4140,16 @@
"desktop-hide": "Hide widget in desktop mode", "desktop-hide": "Hide widget in desktop mode",
"units": "Special symbol to show next to value", "units": "Special symbol to show next to value",
"decimals": "Number of digits after floating point", "decimals": "Number of digits after floating point",
"units-short": "Units",
"decimals-short": "Decimals",
"timewindow": "Timewindow", "timewindow": "Timewindow",
"use-dashboard-timewindow": "Use dashboard timewindow", "use-dashboard-timewindow": "Use dashboard timewindow",
"use-widget-timewindow": "Use widget timewindow",
"display-timewindow": "Display timewindow", "display-timewindow": "Display timewindow",
"legend": "Legend", "legend": "Legend",
"display-legend": "Display legend", "display-legend": "Display legend",
"datasources": "Datasources", "datasources": "Datasources",
"datasource": "Datasource",
"maximum-datasources": "Maximum { count, plural, =1 {1 datasource is allowed.} other {# datasources are allowed} }", "maximum-datasources": "Maximum { count, plural, =1 {1 datasource is allowed.} other {# datasources are allowed} }",
"timeseries-key-error": "At least one timeseries data key should be specified", "timeseries-key-error": "At least one timeseries data key should be specified",
"datasource-type": "Type", "datasource-type": "Type",
@ -4164,9 +4182,20 @@
"icon-size": "Icon size", "icon-size": "Icon size",
"advanced-settings": "Advanced settings", "advanced-settings": "Advanced settings",
"data-settings": "Data settings", "data-settings": "Data settings",
"limits": "Limits",
"no-data-display-message": "\"No data to display\" alternative message", "no-data-display-message": "\"No data to display\" alternative message",
"data-page-size": "Maximum entities per datasource", "data-page-size": "Maximum entities per datasource",
"settings-component-not-found": "Settings form component not found for selector '{{selector}}'" "settings-component-not-found": "Settings form component not found for selector '{{selector}}'",
"preview": "Preview",
"set": "Set",
"set-message": "Set message",
"card-title": "Card title",
"advanced-title-style": "Advanced title style",
"card-style": "Card style",
"text": "Text",
"background": "Background",
"advanced-widget-style": "Advanced widget style",
"card-buttons": "Card buttons"
}, },
"widget-type": { "widget-type": {
"import": "Import widget type", "import": "Import widget type",
@ -5092,6 +5121,7 @@
"markdown-css": "Markdown/HTML CSS" "markdown-css": "Markdown/HTML CSS"
}, },
"simple-card": { "simple-card": {
"label": "Label",
"label-position": "Label position", "label-position": "Label position",
"label-position-left": "Left", "label-position-left": "Left",
"label-position-top": "Top" "label-position-top": "Top"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save