Browse Source

Merge branch 'develop/3.5.2' of https://github.com/thingsboard/thingsboard into ui_tests_devices_make_public_private

pull/8710/head
Seraphym-Tuhai 3 years ago
parent
commit
1df9c12f6f
  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,
AlarmFilterConfig,
createDefaultEntityDataPageLink,
Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink,
Filter,
FilterInfo,
filterInfoToKeyFilters,
Filters,
KeyFilter,
singleEntityDataPageLink,
singleEntityFilterFromDeviceId,
updateDatasourceFromEntityInfo
} from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core';
@ -246,6 +252,9 @@ export class AliasController implements IAliasController {
private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> {
const newDatasource = deepClone(datasource);
if (newDatasource.type === DatasourceType.device) {
newDatasource.type = DatasourceType.entity;
}
if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount
|| newDatasource.type === DatasourceType.alarmCount) {
if (newDatasource.filterId) {
@ -254,7 +263,22 @@ export class AliasController implements IAliasController {
if (newDatasource.type === DatasourceType.alarmCount) {
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(
mergeMap((aliasInfo) => {
newDatasource.aliasName = aliasInfo.alias;

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

@ -29,20 +29,25 @@ import {
GridSettings,
WidgetLayout
} from '@shared/models/dashboard.models';
import { isDefined, isString, isUndefined } from '@core/utils';
import { deepClone, isDefined, isDefinedAndNotNull, isString, isUndefined } from '@core/utils';
import {
DataKey,
Datasource,
datasourcesHasOnlyComparisonAggregation,
DatasourceType,
defaultLegendConfig,
Widget,
WidgetConfig,
widgetType
WidgetConfigMode,
widgetType,
WidgetTypeDescriptor
} from '@app/shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models';
import { EntityId } from '@app/shared/models/id/entity-id';
import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@Injectable({
providedIn: 'root'
@ -211,9 +216,26 @@ export class DashboardUtilsService {
public validateAndUpdateWidget(widget: Widget): Widget {
widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type);
// Temp workaround
if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') {
if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === '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;
}
@ -225,10 +247,8 @@ export class DashboardUtilsService {
widgetConfig.datasources = [];
}
widgetConfig.datasources.forEach((datasource) => {
if (datasource.type === 'device') {
datasource.type = DatasourceType.entity;
}
if (datasource.deviceAliasId) {
datasource.type = DatasourceType.entity;
datasource.entityAliasId = 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) {
if (!state.layouts) {
state.layouts = this.createDefaultLayouts();
@ -429,7 +498,7 @@ export class DashboardUtilsService {
targetLayout: DashboardLayoutId,
widget: Widget,
originalColumns?: number,
originalSize?: {sizeX: number, sizeY: number},
originalSize?: {sizeX: number; sizeY: number},
row?: number,
column?: number): void {
const dashboardConfiguration = dashboard.configuration;
@ -502,7 +571,7 @@ export class DashboardUtilsService {
this.removeUnusedWidgets(dashboard);
}
public isSingleLayoutDashboard(dashboard: Dashboard): {state: string, layout: DashboardLayoutId} {
public isSingleLayoutDashboard(dashboard: Dashboard): {state: string; layout: DashboardLayoutId} {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.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 DashboardComponent from '@home/components/dashboard/dashboard.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 AliasesEntitySelectComponent from '@home/components/alias/aliases-entity-select.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 RelationFiltersComponent from '@home/components/relation/relation-filters.component';
import * as EntityAliasSelectComponent from '@home/components/alias/entity-alias-select.component';
import * as DataKeysComponent from '@home/components/widget/data-keys.component';
import * as DataKeyConfigDialogComponent from '@home/components/widget/data-key-config-dialog.component';
import * as DataKeyConfigComponent from '@home/components/widget/data-key-config.component';
import * as LegendConfigComponent from '@home/components/widget/legend-config.component';
import * as DataKeysComponent from '@home/components/widget/config/data-keys.component';
import * as DataKeyConfigDialogComponent from '@home/components/widget/config/data-key-config-dialog.component';
import * as DataKeyConfigComponent from '@home/components/widget/config/data-key-config.component';
import * as LegendConfigComponent from '@home/components/widget/lib/settings/common/legend-config.component';
import * as ManageWidgetActionsComponent from '@home/components/widget/action/manage-widget-actions.component';
import * as WidgetActionDialogComponent from '@home/components/widget/action/widget-action-dialog.component';
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/dashboard/dashboard.component': DashboardComponent,
'@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.component': AliasesEntitySelectComponent,
'@home/components/widget/widget-config.component': WidgetConfigComponent,
@ -501,10 +501,10 @@ class ModulesMap implements IModulesMap {
'@home/components/entity/entity-filter.component': EntityFilterComponent,
'@home/components/relation/relation-filters.component': RelationFiltersComponent,
'@home/components/alias/entity-alias-select.component': EntityAliasSelectComponent,
'@home/components/widget/data-keys.component': DataKeysComponent,
'@home/components/widget/data-key-config-dialog.component': DataKeyConfigDialogComponent,
'@home/components/widget/data-key-config.component': DataKeyConfigComponent,
'@home/components/widget/legend-config.component': LegendConfigComponent,
'@home/components/widget/config/data-keys.component': DataKeysComponent,
'@home/components/widget/config/data-key-config-dialog.component': DataKeyConfigDialogComponent,
'@home/components/widget/config/data-key-config.component': DataKeyConfigComponent,
'@home/components/widget/lib/settings/common/legend-config.component': LegendConfigComponent,
'@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent,
'@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent,
'@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()">
<mat-icon class="material-icons">close</mat-icon>
</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"
#entityAliasAutocomplete="matAutocomplete"
[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,
useExisting: forwardRef(() => EntityAliasSelectComponent),
multi: true
},
}/*,
{
provide: ErrorStateMatcher,
useExisting: EntityAliasSelectComponent
}]
}*/]
})
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.stopPropagation();
this.creatingEntityAlias = true;
if (this.callbacks && this.callbacks.createEntityAlias) {
this.callbacks.createEntityAlias(alias, this.allowedEntityTypes).subscribe((newAlias) => {
if (!newAlias) {
setTimeout(() => {
this.entityAliasInput.nativeElement.blur();
this.entityAliasInput.nativeElement.focus();
}, 0);
if (focusOnCancel) {
setTimeout(() => {
this.entityAliasInput.nativeElement.blur();
this.entityAliasInput.nativeElement.focus();
}, 0);
}
} else {
this.entityAliasList.push(newAlias);
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.
-->
<form [formGroup]="widgetFormGroup" (ngSubmit)="add()" style="width: 1200px;">
<form [formGroup]="widgetFormGroup" style="width: 1200px;">
<mat-toolbar color="primary">
<h2 translate>widget.add</h2>
<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
(click)="cancel()"
type="button">
@ -35,11 +49,26 @@
[functionsOnly]="false"
[dashboard]="dashboard"
[widget]="widget"
[widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic"
formControlName="widgetConfig">
</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>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<div mat-dialog-actions fxLayoutAlign="space-between center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
@ -47,10 +76,20 @@
cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid">
{{ 'action.add' | translate }}
</button>
<div fxLayout="row" fxLayoutGap="8px">
<button *ngIf="!previewMode"
mat-button color="primary"
type="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>
</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 { Router } from '@angular/router';
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 { 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 { isDefined, isString } from '@core/utils';
import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
export interface AddWidgetDialogData {
dashboard: Dashboard;
aliasController: IAliasController;
stateController: IStateController;
widget: Widget;
widgetInfo: WidgetInfo;
}
@ -38,22 +40,43 @@ export interface AddWidgetDialogData {
@Component({
selector: 'tb-add-widget-dialog',
templateUrl: './add-widget-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}],
providers: [/*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/],
styleUrls: []
})
export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget>
implements OnInit, ErrorStateMatcher {
widgetConfigModes = WidgetConfigMode;
widgetFormGroup: UntypedFormGroup;
dashboard: Dashboard;
aliasController: IAliasController;
stateController: IStateController;
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;
constructor(protected store: Store<AppState>,
protected router: Router,
public translate: TranslateService,
@Inject(MAT_DIALOG_DATA) public data: AddWidgetDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AddWidgetDialogComponent, Widget>,
@ -62,6 +85,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
this.dashboard = this.data.dashboard;
this.aliasController = this.data.aliasController;
this.stateController = this.data.stateController;
this.widget = this.data.widget;
const widgetInfo = this.data.widgetInfo;
@ -91,7 +115,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
latestDataKeySettingsSchema = isString(rawLatestDataKeySettingsSchema) ?
JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema;
}
const widgetConfig: WidgetConfigComponentData = {
this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config,
layout: {},
widgetType: this.widget.type,
@ -103,16 +128,17 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
latestDataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective,
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({
widgetConfig: [widgetConfig, []]
widgetConfig: [this.widgetConfig, []]
}
);
}
ngOnInit(): void {
ngOnInit() {
}
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
headerTitle="{{editingWidget?.config.title}}"
headerSubtitle="{{ editingWidgetSubtitle }}"
[isReadOnly]="false"
[isAlwaysEdit]="true"
(closeDetails)="onEditWidgetClosed()"
(toggleDetailsEditMode)="onRevertWidgetEdit()"
(applyDetails)="saveWidget()"
[theForm]="tbEditWidget.widgetFormGroup">
<div class="details-buttons">
[isReadOnly]="true"
(closeDetails)="onEditWidgetClosed()">
<div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-header *ngIf="tbEditWidget.hasBasicMode" (valueChange)="tbEditWidget.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]="tbEditWidget.widgetConfigMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<div [tb-help]="helpLinkIdForWidgetType()"></div>
</div>
<tb-edit-widget #tbEditWidget
[dashboard]="dashboard"
[aliasController]="dashboardCtx.aliasController"
[stateController]="dashboardCtx.stateController"
[widgetEditMode]="widgetEditMode"
[widget]="editingWidget"
[widgetLayout]="editingWidgetLayout">
[widgetLayout]="editingWidgetLayout"
(revertWidgetConfig)="onRevertWidgetEdit()"
(applyWidgetConfig)="saveWidget()">
</tb-edit-widget>
</tb-details-panel>
<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 fb: UntypedFormBuilder,
private dialog: MatDialog,
private translate: TranslateService,
public translate: TranslateService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private ngZone: NgZone,
@ -1142,15 +1142,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.searchBundle = '';
this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe(
(widgetTypeInfo) => {
const config: WidgetConfig = JSON.parse(widgetTypeInfo.defaultConfig);
const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo);
config.title = 'New ' + widgetTypeInfo.widgetName;
config.datasources = [];
if (isDefinedAndNotNull(config.alarmSource)) {
config.alarmSource = {
type: DatasourceType.entity,
dataKeys: config.alarmSource.dataKeys || []
};
}
let newWidget: Widget = {
isSystemType: widget.isSystemType,
bundleAlias: widget.bundleAlias,
@ -1176,6 +1169,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
data: {
dashboard: this.dashboard,
aliasController: this.dashboardCtx.aliasController,
stateController: this.dashboardCtx.stateController,
widget: newWidget,
widgetInfo: widgetTypeInfo
}

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

@ -22,7 +22,43 @@
[functionsOnly]="widgetEditMode"
[dashboard]="dashboard"
[widget]="widget"
[hideToggleHeader]="previewMode"
[widgetConfigMode]="widgetConfigMode"
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-preview *ngIf="previewMode" class="widget-preview-section"
[aliasController]="aliasController"
[stateController]="stateController"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
</tb-widget-preview>
</fieldset>
</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.
///
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { MatDialog } from '@angular/material/dialog';
import { Dashboard, WidgetLayout } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
import { Widget } from '@shared/models/widget.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { Widget, WidgetConfigMode } from '@shared/models/widget.models';
import { WidgetComponentService } from '@home/components/widget/widget-component.service';
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';
@Component({
selector: 'tb-edit-widget',
templateUrl: './edit-widget.component.html',
styleUrls: []
styleUrls: ['./edit-widget.component.scss']
})
export class EditWidgetComponent extends PageComponent implements OnInit, OnChanges {
@ -40,6 +40,9 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
@Input()
aliasController: IAliasController;
@Input()
stateController: IStateController;
@Input()
widgetEditMode: boolean;
@ -49,10 +52,33 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
@Input()
widgetLayout: WidgetLayout;
@Output()
applyWidgetConfig = new EventEmitter<void>();
@Output()
revertWidgetConfig = new EventEmitter<void>();
widgetFormGroup: UntypedFormGroup;
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>,
private dialog: MatDialog,
private fb: UntypedFormBuilder,
@ -78,10 +104,27 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
}
}
if (reloadConfig) {
if (this.currentWidgetConfigChanged) {
this.currentWidgetConfigChanged = false;
} else {
this.previewMode = false;
}
this.loadWidgetConfig();
}
}
onApplyWidgetConfig() {
if (this.widgetFormGroup.valid) {
this.currentWidgetConfigChanged = true;
this.applyWidgetConfig.emit();
}
}
onRevertWidgetConfig() {
this.currentWidgetConfigChanged = true;
this.revertWidgetConfig.emit();
}
private loadWidgetConfig() {
if (!this.widget) {
return;
@ -113,6 +156,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema;
}
this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config,
layout: this.widgetLayout,
widgetType: this.widget.type,
@ -124,8 +168,10 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
latestDataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective,
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});
}
}

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

@ -31,6 +31,13 @@
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</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"
#filterAutocomplete="matAutocomplete"
[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.stopPropagation();
this.creatingFilter = true;
if (this.callbacks && this.callbacks.createFilter) {
this.callbacks.createFilter(filter).subscribe((newFilter) => {
if (!newFilter) {
setTimeout(() => {
this.filterInput.nativeElement.blur();
this.filterInput.nativeElement.focus();
}, 0);
if (focusOnCancel) {
setTimeout(() => {
this.filterInput.nativeElement.blur();
this.filterInput.nativeElement.focus();
}, 0);
}
} else {
this.filterList.push(newFilter);
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 { WidgetComponent } from '@home/components/widget/widget.component';
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 { AliasesEntitySelectComponent } from '@home/components/alias/aliases-entity-select.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 { EntityFilterComponent } from '@home/components/entity/entity-filter.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 { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.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 { FiltersDialogComponent } from '@home/components/filter/filters-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 { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.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 { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.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 { VersionControlComponent } from '@home/components/vc/version-control.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 { RouterTabsComponent } from '@home/components/router-tabs.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 { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.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({
declarations:
@ -206,9 +201,7 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
AlarmTableHeaderComponent,
AlarmTableComponent,
AlarmAssigneePanelComponent,
AlarmAssigneeSelectComponent,
AlarmAssigneeSelectPanelComponent,
AlarmFilterConfigComponent,
AttributeTableComponent,
AddAttributeDialogComponent,
EditAttributeValuePanelComponent,
@ -220,18 +213,13 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
DashboardComponent,
WidgetContainerComponent,
WidgetComponent,
LegendComponent,
WidgetSettingsComponent,
WidgetConfigComponent,
WidgetPreviewComponent,
EntityFilterViewComponent,
EntityFilterComponent,
EntityAliasSelectComponent,
DataKeysComponent,
DataKeyConfigComponent,
DataKeyConfigDialogComponent,
LegendConfigComponent,
ManageWidgetActionsComponent,
WidgetActionDialogComponent,
ManageWidgetActionsDialogComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent,
@ -253,7 +241,6 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
KeyFilterDialogComponent,
FilterDialogComponent,
FiltersDialogComponent,
FilterSelectComponent,
FilterTextComponent,
FiltersEditComponent,
FiltersEditPanelComponent,
@ -338,7 +325,8 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
CommonModule,
SharedModule,
SharedHomeComponentsModule,
WidgetSettingsModule,
WidgetConfigComponentsModule,
BasicWidgetConfigModule,
Lwm2mProfileComponentsModule,
SnmpDeviceProfileTransportModule,
StatesControllerModule,
@ -360,9 +348,7 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
RelationFiltersComponent,
AlarmTableComponent,
AlarmAssigneePanelComponent,
AlarmAssigneeSelectComponent,
AlarmAssigneeSelectPanelComponent,
AlarmFilterConfigComponent,
AttributeTableComponent,
AliasesEntitySelectComponent,
AliasesEntityAutocompleteComponent,
@ -371,18 +357,13 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
DashboardComponent,
WidgetContainerComponent,
WidgetComponent,
LegendComponent,
WidgetSettingsComponent,
WidgetConfigComponent,
WidgetPreviewComponent,
EntityFilterViewComponent,
EntityFilterComponent,
EntityAliasSelectComponent,
DataKeysComponent,
DataKeyConfigComponent,
DataKeyConfigDialogComponent,
LegendConfigComponent,
ManageWidgetActionsComponent,
WidgetActionDialogComponent,
ManageWidgetActionsDialogComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent,
@ -403,7 +384,6 @@ import { DeviceInfoFilterComponent } from '@home/components/device/device-info-f
KeyFilterDialogComponent,
FilterDialogComponent,
FiltersDialogComponent,
FilterSelectComponent,
FilterTextComponent,
FiltersEditComponent,
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.
-->
<div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-entity-table-content tb-outlined-border">
<div class="tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-entity-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools">
<span class="tb-entity-table-title" translate>widget-config.actions</span>
@ -69,7 +69,7 @@
class="tb-drop-list">
<ng-container matColumnDef="actionSourceId">
<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
class="tb-drag-handle"
style="min-width: 36px"
@ -95,7 +95,7 @@
</ng-container>
<ng-container matColumnDef="icon">
<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-cell>
</ng-container>

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

@ -24,12 +24,6 @@
background: #fff;
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 {
padding-right: 20px;
white-space: nowrap;
@ -49,6 +43,12 @@
opacity: 1 !important;
}
.mat-mdc-cell.tb-icon-cell {
.mat-icon {
vertical-align: middle;
}
}
.tb-draggable {
&.cdk-drag-placeholder {
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() actionSources: {[actionSourceId: string]: WidgetActionSource};
innerValue: WidgetActionsData;
displayedColumns: string[];
@ -89,6 +91,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
dragDisabled = true;
private widgetResize$: ResizeObserver;
private destroyed = false;
@ViewChild('searchInput') searchInputField: ElementRef;
@ -123,6 +126,7 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
}
ngOnDestroy(): void {
this.destroyed = true;
if (this.widgetResize$) {
this.widgetResize$.disconnect();
}
@ -336,22 +340,25 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
this.disabled = isDisabled;
}
writeValue(obj: WidgetActionsData): void {
this.innerValue = obj;
if (this.innerValue) {
setTimeout(() => {
writeValue(actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>}): void {
this.innerValue = {
actionsMap: actions || {},
actionSources: this.actionSources || {}
};
setTimeout(() => {
if (!this.destroyed) {
this.dataSource.setActions(this.innerValue);
if (this.viewsInited) {
this.resetSortAndFilter(true);
} else {
this.dirtyValue = true;
}
}, 0);
}
}
}, 0);
}
private onActionsUpdated() {
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
[dataKeySettingsSchema]="data.dataKeySettingsSchema"
[dataKeySettingsDirective]="data.dataKeySettingsDirective"
[deviceId]="data.deviceId"
[entityAliasId]="data.entityAliasId"
[dashboard]="data.dashboard"
[aliasController]="data.aliasController"
@ -39,6 +40,10 @@
[widgetType]="data.widgetType"
[showPostProcessing]="data.showPostProcessing"
[callbacks]="data.callbacks"
[hideDataKeyLabel]="data.hideDataKeyLabel"
[hideDataKeyColor]="data.hideDataKeyColor"
[hideDataKeyUnits]="data.hideDataKeyUnits"
[hideDataKeyDecimals]="data.hideDataKeyDecimals"
formControlName="dataKey">
</tb-data-key-config>
</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 { DataKey, Widget, widgetType } from '@shared/models/widget.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 { IAliasController } from '@core/api/widget-api.models';
@ -36,9 +36,14 @@ export interface DataKeyConfigDialogData {
aliasController: IAliasController;
widget: Widget;
widgetType: widgetType;
deviceId?: string;
entityAliasId?: string;
showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks;
hideDataKeyLabel: boolean;
hideDataKeyColor: boolean;
hideDataKeyUnits: boolean;
hideDataKeyDecimals: boolean;
}
@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-form-field>
<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>
<input matInput formControlName="label" required>
</mat-form-field>
<tb-color-input fxFlex
<tb-color-input *ngIf="!hideDataKeyColor" fxFlex
required
label="{{'datakey.color' | translate}}"
icon="format_color_fill"
@ -53,11 +53,11 @@
</tb-color-input>
</div>
<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>
<input matInput formControlName="units">
</mat-form-field>
<mat-form-field fxFlex>
<mat-form-field *ngIf="!hideDataKeyDecimals" fxFlex>
<mat-label translate>datakey.decimals</mat-label>
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1">
</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
* limitations under the License.
*/
@import '../../../../../scss/constants';
@import '../../../../../../scss/constants';
:host {
.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 { MatDialog } from '@angular/material/dialog';
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 { Observable, of } from 'rxjs';
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 { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models';
import { genNextLabel } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-data-key-config',
@ -90,6 +91,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
comparisonResultTypeTranslations = comparisonResultTypeTranslationMap;
@Input()
deviceId: string;
@Input()
entityAliasId: string;
@ -117,6 +121,22 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@Input()
showPostProcessing = true;
@Input()
@coerceBoolean()
hideDataKeyLabel = false;
@Input()
@coerceBoolean()
hideDataKeyColor = false;
@Input()
@coerceBoolean()
hideDataKeyUnits = false;
@Input()
@coerceBoolean()
hideDataKeyDecimals = false;
@ViewChild('keyInput') keyInput: ElementRef;
@ViewChild('funcBodyEdit') funcBodyEdit: JsFuncComponent;
@ -377,9 +397,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
if (this.modelValue.type === DataKeyType.alarm) {
fetchObservable = of(this.alarmKeys);
} else {
if (this.entityAliasId) {
if (this.deviceId || this.entityAliasId) {
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 {
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.
-->
<mat-form-field [formGroup]="keysListFormGroup" class="mat-block tb-chip-list">
<mat-chip-grid #chipList formControlName="keys">
<mat-form-field [formGroup]="keysListFormGroup" class="mat-block">
<mat-label>{{placeholder}}</mat-label>
<mat-chip-grid #chipList formControlName="keys" [required]="required">
<div class="tb-datakeys-container"
[dndDropzone]="[dndId]"
[dndHorizontal]="true"
dndEffectAllowed="move"
dndAllowExternal="false"
[dndDisableIf]="disabled"
[dndDisableIf]="disabled || dragDisabled"
(dndDrop)="onChipDrop($event)">
<mat-chip-row #placeholderChipRow
dndPlaceholderRef
class="tb-datakey-chip tb-datakey-chip-dnd-placeholder">
</mat-chip-row>
<div #placeholderChipRow
dndPlaceholderRef
class="mat-mdc-chip mat-mdc-chip-row mdc-evolution-chip mat-mdc-standard-chip tb-datakey-chip tb-datakey-chip-dnd-placeholder">
</div>
<mat-chip-row #chipRow *ngFor="let key of keys; let $index = index"
class="tb-datakey-chip"
(dndStart)="chipDragStart($index, chipRow, placeholderChipRow)"
(dndEnd)="chipDragEnd()"
[dndDraggable]="key"
[dndDisableIf]="disabled"
[dndDisableIf]="disabled || dragDisabled"
dndType="{{ dndId }}"
dndEffectAllowed="move"
dndDraggingSourceClass="tb-chip-dragging"
[removable]="!disabled && !isCountDatasource"
(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}">
<div class="tb-absolute-fill tb-dragging-chip-image-fill"></div>
<div dndHandle *ngIf="!disabled" class="tb-chip-drag-handle" style="margin-right: 5px;">
<mat-icon class="tb-mat-20">drag_handle</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 dndHandle *ngIf="!disabled && !dragDisabled" class="tb-chip-drag-handle">
<mat-icon class="tb-mat-18">drag_indicator</mat-icon>
</div>
<div class="tb-chip-labels">
<div class="tb-chip-label">
<ng-container *ngIf="datasourceType === datasourceTypes.entity">
<mat-icon class="tb-mat-20 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"
<ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
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 }}"
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 }}"
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 }}"
matTooltipPosition="above">timeline</mat-icon>
</ng-container>
{{key.label}}
<span *ngIf="!hideDataKeyLabel && key.label !== key.name">{{key.label}}</span>
</div>
<div class="tb-chip-separator">: </div>
<div *ngIf="!hideDataKeyLabel && key.label !== key.name" class="tb-chip-separator">: </div>
<div class="tb-chip-label">
<strong>
<ng-container *ngTemplateOutlet="keyName; context:{key: key}"></ng-container>
</strong>
</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"
type="button"
(click)="editDataKey(key, $index)" mat-icon-button class="tb-mat-32">
<mat-icon class="tb-mat-20">edit</mat-icon>
(click)="editDataKey(key, $index)" mat-icon-button class="tb-mat-24">
<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>
<mat-icon matChipRemove *ngIf="!disabled && !isCountDatasource">close</mat-icon>
</div>
</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}"
style="max-width: 200px;"
#keyInput
formControlName="key"
matAutocompleteOrigin
[readonly]="isCountDatasource"
[readonly]="inputDisabled"
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
(focusin)="onFocus()"
(drop)="$event.preventDefault();"
[matAutocomplete]="keyAutocomplete"
[matAutocompleteDisabled]="isCountDatasource"
[matAutocompleteDisabled]="inputDisabled"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="add($event)">
@ -105,7 +112,7 @@
[displayWith]="displayKeyFn">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<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"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon>
@ -132,7 +139,7 @@
{{ translate.get('entity.no-key-matching',
{key: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
<span *ngIf="datasourceType !== datasourceTypes.entity; else createEntityKey">
<span *ngIf="!isEntityDatasource; else createEntityKey">
<a translate (click)="createKey(searchText)">entity.create-new-key</a>
</span>
<ng-template #createEntityKey>
@ -162,7 +169,7 @@
{{ requiredText }}
</mat-error>
</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() }}
</div>
<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 {
generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => 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;
height: 32px;
&.mdc-evolution-chip--with-trailing-action {
.mdc-evolution-chip__action--primary {
padding-left: 4px;
padding-right: 12px;
}
}
.mat-mdc-chip-action {
overflow: hidden;
.mat-mdc-chip-action-label {
@ -37,15 +44,12 @@
.tb-attribute-chip {
max-width: 100%;
color: rgb(66, 66, 66);
font-weight: normal;
font-size: 16px;
.tb-chip-drag-handle {
padding: 3px;
height: 24px;
cursor: move;
mat-icon {
pointer-events: none;
margin-right: 4px;
margin-left: 4px;
vertical-align: bottom;
}
}
.tb-chip-labels {
@ -53,7 +57,13 @@
flex-direction: row;
align-items: center;
min-width: 0;
background: rgba(0, 0, 0, 0.04);
border-radius: 100px;
padding: 2px 10px;
.tb-chip-label {
font-weight: normal;
font-size: 14px;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -70,15 +80,9 @@
white-space: pre;
}
}
.mat-mdc-chip-remove.mat-icon {
width: 24px;
min-width: 24px;
height: 24px;
font-size: 24px;
margin-right: 4px;
.mat-mdc-chip-remove.mat-mdc-icon-button {
color: 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
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormGroupDirective,
NG_VALUE_ACCESSOR,
@ -37,14 +38,14 @@ import {
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validators
ValidationErrors
} from '@angular/forms';
import { Observable, of } from 'rxjs';
import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
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 { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -60,12 +61,14 @@ import { MatDialog } from '@angular/material/dialog';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
} from '@home/components/widget/data-key-config-dialog.component';
import { deepClone, guid, isUndefined } from '@core/utils';
} from '@home/components/widget/config/data-key-config-dialog.component';
import { deepClone, guid, isDefinedAndNotNull, isUndefined } from '@core/utils';
import { Dashboard } from '@shared/models/dashboard.models';
import { AggregationType } from '@shared/models/time/time.models';
import { DndDropEvent } from 'ngx-drag-drop/lib/dnd-dropzone.directive';
import { moveItemInArray } from '@angular/cdk/drag-drop';
import { coerceBoolean } from '@shared/decorators/coercion';
import { DatasourceComponent } from '@home/components/widget/config/datasource.component';
@Component({
selector: 'tb-data-keys',
@ -76,16 +79,32 @@ import { moveItemInArray } from '@angular/cdk/drag-drop';
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DataKeysComponent),
multi: true
},
} /*,
{
provide: ErrorStateMatcher,
useExisting: DataKeysComponent
}
} */
],
encapsulation: ViewEncapsulation.None
})
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;
widgetTypes = widgetType;
dataKeyTypes = DataKeyType;
@ -113,6 +132,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
@Input()
optDataKeys: boolean;
@Input()
@coerceBoolean()
simpleDataKeysLabel = false;
@Input()
aliasController: IAliasController;
@ -134,6 +157,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
@Input()
entityAliasId: string;
@Input()
deviceId: string;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue || !this.optDataKeys || this.isCountDatasource;
@ -148,6 +174,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
@ViewChild('keyInput') keyInput: ElementRef<HTMLInputElement>;
@ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete;
@ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger;
@ViewChild('chipList') chipList: MatChipGrid;
keys: Array<DataKey> = [];
@ -178,6 +205,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
constructor(private store: Store<AppState>,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private datasourceComponent: DatasourceComponent,
public translate: TranslateService,
private utils: UtilsService,
private dialogs: DialogService,
@ -189,10 +217,19 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
}
updateValidators() {
this.keysListFormGroup.get('keys').setValidators(this.required ? [Validators.required] : []);
this.keysListFormGroup.get('keys').setValidators(this.required ? [this.keysRequired] : []);
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 {
this.propagateChange = fn;
}
@ -202,7 +239,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
ngOnInit() {
this.keysListFormGroup = this.fb.group({
keys: [null, this.required ? [Validators.required] : []],
keys: [null, this.required ? [this.keysRequired] : []],
key: [null]
});
this.alarmKeys = [];
@ -251,37 +288,40 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
}
private updateParams() {
if (this.datasourceType === DatasourceType.function) {
this.dataKeyType = DataKeyType.function;
this.requiredText = this.translate.instant('datakey.function-types-required');
if (this.widgetType === widgetType.latest) {
this.placeholder = this.translate.instant('datakey.latest-key-functions');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.latest-key-function');
} else if (this.widgetType === widgetType.alarm) {
this.placeholder = this.translate.instant('datakey.alarm-key-functions');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.alarm-key-function');
} else {
this.placeholder = this.translate.instant('datakey.timeseries-key-functions');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.timeseries-key-function');
}
const singleKey = this.maxDataKeysSet && this.maxDataKeys === 1;
this.secondaryPlaceholder = '+' + this.translate.instant('action.add');
if (this.datasourceType === DatasourceType.function) {
this.dataKeyType = DataKeyType.function;
this.requiredText = this.translate.instant('datakey.function-types-required');
if (this.widgetType === widgetType.latest) {
this.placeholder = this.translate.instant(singleKey ? 'datakey.latest-key-function' : 'datakey.latest-key-functions');
} else if (this.widgetType === widgetType.alarm) {
this.placeholder = this.translate.instant(singleKey ? 'datakey.alarm-key-function' : 'datakey.alarm-key-functions');
} else {
this.placeholder = this.translate.instant(singleKey ? 'datakey.timeseries-key-function' : 'datakey.timeseries-key-functions');
}
} 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 {
if (this.widgetType === widgetType.latest) {
this.dataKeyType = null;
this.placeholder = this.translate.instant('datakey.latest-keys');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.latest-key');
this.placeholder = this.translate.instant(singleKey ? 'datakey.latest-key' : 'datakey.latest-keys');
this.requiredText = this.translate.instant('datakey.timeseries-or-attributes-required');
} else if (this.widgetType === widgetType.alarm) {
this.dataKeyType = null;
this.placeholder = this.translate.instant('datakey.alarm-keys');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.alarm-key');
this.placeholder = this.translate.instant(singleKey ? 'datakey.alarm-key' : 'datakey.alarm-keys');
this.requiredText = this.translate.instant('datakey.alarm-fields-timeseries-or-attributes-required');
} else {
this.dataKeyType = DataKeyType.timeseries;
this.placeholder = this.translate.instant('datakey.timeseries-keys');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.timeseries-key');
this.placeholder = this.translate.instant(singleKey ? 'datakey.timeseries-key' : 'datakey.timeseries-keys');
this.requiredText = this.translate.instant('datakey.timeseries-required');
}
}
}
}
private reset() {
@ -307,16 +347,22 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'entityAliasId') {
if (['deviceId', 'entityAliasId'].includes(propName)) {
this.clearSearchCache();
this.dirty = true;
} else if (['widgetType', 'datasourceType'].includes(propName)) {
this.clearSearchCache();
this.updateParams();
setTimeout(() => {
this.reset();
}, 1);
} else if (['required', 'optDataKeys'].includes('propName')) {
} else if (['widgetType', 'datasourceType', 'maxDataKeys', 'simpleDataKeysLabel'].includes(propName)) {
if (propName === 'datasourceType' &&
[DatasourceType.device, DatasourceType.entity].includes(change.previousValue) &&
[DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) {
this.clearSearchCache();
} else {
this.clearSearchCache();
this.updateParams();
setTimeout(() => {
this.reset();
}, 1);
}
} else if (['required', 'optDataKeys'].includes(propName)) {
this.updateValidators();
}
}
@ -325,7 +371,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
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;
}
@ -365,7 +411,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
}
addKey(key: DataKey): void {
if (!this.maxDataKeys || this.maxDataKeys < 0 ||
if (!this.maxDataKeysSet ||
!this.modelValue || this.modelValue.length < this.maxDataKeys) {
if (!this.modelValue) {
this.modelValue = [];
@ -375,17 +421,17 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
this.keysListFormGroup.get('keys').setValue(this.keys);
}
this.propagateChange(this.modelValue);
this.clear();
const focus = !this.maxDataKeysSet || this.modelValue.length < this.maxDataKeys;
this.clear('', focus);
}
add(event: MatChipInputEvent): void {
const value = event.value;
if ((value || '').trim()) {
if (this.dataKeyType) {
this.addFromChipValue({name: value.trim(), type: this.dataKeyType});
}
if ((value || '').trim() && this.dataKeyType) {
this.addFromChipValue({name: value.trim(), type: this.dataKeyType});
} else {
this.clear();
}
this.clear();
}
remove(key: DataKey) {
@ -402,8 +448,10 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
}
}
chipDragStart(index: number, chipRow: MatChipRow, placeholderChipRow: MatChipRow) {
this.renderer.setStyle(placeholderChipRow._elementRef.nativeElement, 'width', chipRow._elementRef.nativeElement.offsetWidth + 'px');
chipDragStart(index: number, chipRow: MatChipRow, placeholderChipRow: Element) {
this.autocomplete.closePanel();
this.renderer.setStyle(placeholderChipRow,
'width', chipRow._elementRef.nativeElement.offsetWidth + 'px');
this.dragIndex = index;
}
@ -447,9 +495,14 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
aliasController: this.aliasController,
widget: this.widget,
widgetType: this.widgetType,
deviceId: this.deviceId,
entityAliasId: this.entityAliasId,
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) => {
if (updatedDataKey) {
@ -496,7 +549,8 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
if (this.datasourceType === DatasourceType.function) {
const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
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];
if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.attribute);
@ -505,7 +559,11 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
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 {
fetchObservable = of([]);
}
@ -523,22 +581,41 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
}
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.keysListFormGroup.get('key').patchValue(value, {emitEvent: true});
setTimeout(() => {
this.keyInput.nativeElement.blur();
this.keyInput.nativeElement.focus();
}, 0);
this.keysListFormGroup.get('key').patchValue(value, {emitEvent: focus});
if (focus) {
setTimeout(() => {
this.keyInput.nativeElement.blur();
this.keyInput.nativeElement.focus();
}, 0);
}
}
get isCountDatasource(): boolean {
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() {
this.searchText = '';
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.
///
import { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models';
import { DataKeysCallbacks } from './data-keys.component.models';
import { WidgetActionCallbacks } from './action/manage-widget-actions.component.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';
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 {
AfterViewInit,
Component, ComponentFactoryResolver,
Component,
ComponentFactoryResolver,
ComponentRef,
forwardRef,
Input, OnChanges,
Input,
OnChanges,
OnDestroy,
OnInit, SimpleChanges,
OnInit,
SimpleChanges,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
IRuleNodeConfigurationComponent,
RuleNodeConfiguration,
RuleNodeDefinition
} from '@shared/models/rule-node.models';
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
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 { 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 { JsonFormComponentData } from '@shared/components/json-form/json-form-component.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
/// <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 { ComparisonDuration } from '@shared/models/time/time.models';
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;
@ -140,6 +148,11 @@ export interface TbFlotBaseSettings {
yaxis: TbFlotYAxisSettings;
}
export interface TbFlotLegendSettings {
showLegend?: boolean;
legendConfig?: LegendConfig;
}
export interface TbFlotComparisonSettings {
comparisonEnabled: boolean;
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 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.settings = ctx.settings as TbFlotSettings;
this.utils = this.ctx.$injector.get(UtilsService);
@ -334,7 +334,7 @@ export class TbFlot {
}
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 {
GettingStartedCompletedDialogComponent
} 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 { QuickLinksWidgetComponent } from '@home/components/widget/lib/home-page/quick-links-widget.component';
import { QuickLinkComponent } from '@home/components/widget/lib/home-page/quick-link.component';
@ -49,7 +48,6 @@ import {
EditLinksDialogComponent,
GettingStartedWidgetComponent,
GettingStartedCompletedDialogComponent,
ToggleHeaderComponent,
UsageInfoWidgetComponent,
QuickLinksWidgetComponent,
QuickLinkComponent,
@ -70,7 +68,6 @@ import {
EditLinksDialogComponent,
GettingStartedWidgetComponent,
GettingStartedCompletedDialogComponent,
ToggleHeaderComponent,
UsageInfoWidgetComponent,
QuickLinksWidgetComponent,
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.
-->
<section [formGroup]="simpleCardWidgetSettingsForm" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.simple-card.label-position</mat-label>
<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>
</section>
<div [formGroup]="simpleCardWidgetSettingsForm" class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widgets.simple-card.label</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>

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({
selector: 'tb-simple-card-widget-settings',
templateUrl: './simple-card-widget-settings.component.html',
styleUrls: []
styleUrls: ['../../../config/widget-config.scss']
})
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">
</mat-form-field>
</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">
<legend class="group-title" translate>widgets.chart.tooltip-settings</legend>
<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
} from '@home/components/widget/lib/settings/chart/label-data-key.component';
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> = {
stack: false,
fontColor: '#545454',
@ -95,9 +96,11 @@ export function flotDefaultSettings(chartType: ChartType): Partial<TbFlotSetting
};
settings.customLegendEnabled = false;
settings.dataKeysListForLabels = [];
settings.showLegend = true;
settings.legendConfig = defaultLegendConfig(widgetType.timeseries);
}
return settings;
}
};
@Component({
selector: 'tb-flot-widget-settings',
@ -221,6 +224,11 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
showLabels: [true, []]
}));
// Legend settings
this.flotSettingsFormGroup.addControl('showLegend', this.fb.control(false, []));
this.flotSettingsFormGroup.addControl('legendConfig', this.fb.control(null, []));
// Custom legend settings
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') {
this.flotSettingsFormGroup.get('showLegend').valueChanges.subscribe(() => {
this.updateValidators(true);
});
this.flotSettingsFormGroup.get('comparisonEnabled').valueChanges.subscribe(() => {
this.updateValidators(true);
});
@ -350,9 +361,15 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
this.flotSettingsFormGroup.get('yaxis.ticksFormatter').updateValueAndValidity({emitEvent: false});
if (this.chartType === 'graph' || this.chartType === 'bar') {
const showLegend: boolean = this.flotSettingsFormGroup.get('showLegend').value;
const comparisonEnabled: boolean = this.flotSettingsFormGroup.get('comparisonEnabled').value;
const timeForComparison: ComparisonDuration = this.flotSettingsFormGroup.get('timeForComparison').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) {
this.flotSettingsFormGroup.get('timeForComparison').enable({emitEvent: false});
if (timeForComparison === 'customInterval') {
@ -370,6 +387,7 @@ export class FlotWidgetSettingsComponent extends PageComponent implements OnInit
this.flotSettingsFormGroup.get('dataKeysListForLabels').disable({emitEvent});
}
this.flotSettingsFormGroup.get('legendConfig').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false});
this.flotSettingsFormGroup.get('comparisonCustomIntervalValue').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 {
QuickLinksWidgetSettingsComponent
} 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({
declarations: [
@ -290,6 +291,7 @@ import {
AnalogueCompassWidgetSettingsComponent,
DigitalGaugeWidgetSettingsComponent,
ValueSourceComponent,
LegendConfigComponent,
FixedColorLevelComponent,
TickValueComponent,
FlotWidgetSettingsComponent,
@ -395,6 +397,7 @@ import {
AnalogueCompassWidgetSettingsComponent,
DigitalGaugeWidgetSettingsComponent,
ValueSourceComponent,
LegendConfigComponent,
FixedColorLevelComponent,
TickValueComponent,
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,
dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: this.utils.editWidgetInfo.latestDataKeySettingsDirective,
hasBasicMode: this.utils.editWidgetInfo.hasBasicMode,
basicModeDirective: this.utils.editWidgetInfo.basicModeDirective,
defaultConfig: this.utils.editWidgetInfo.defaultConfig
}, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined
);
@ -162,7 +164,7 @@ export class WidgetComponentService {
(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) => {
(window as any).TbTripAnimationWidget = mod.TbTripAnimationWidget;
}))
@ -404,6 +406,9 @@ export class WidgetComponentService {
if (widgetInfo.latestDataKeySettingsDirective && widgetInfo.latestDataKeySettingsDirective.length) {
directives.push(widgetInfo.latestDataKeySettingsDirective);
}
if (widgetInfo.basicModeDirective && widgetInfo.basicModeDirective.length) {
directives.push(widgetInfo.basicModeDirective);
}
if (directives.length) {
factories.filter((factory) => directives.includes(factory.selector))
.forEach((foundFactory) => {

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

@ -29,7 +29,7 @@ import {
DateRangeNavigatorWidgetComponent
} from '@home/components/widget/lib/date-range-navigator/date-range-navigator.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 { GatewayFormComponent } from '@home/components/widget/lib/gateway/gateway-form.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 { HomePageWidgetsModule } from '@home/components/widget/lib/home-page/home-page-widgets.module';
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({
declarations:
@ -62,7 +64,9 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MarkdownWidgetComponent,
SelectEntityDialogComponent
SelectEntityDialogComponent,
LegendComponent,
FlotWidgetComponent
],
imports: [
CommonModule,
@ -88,7 +92,9 @@ import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
NavigationCardsWidgetComponent,
NavigationCardWidgetComponent,
QrCodeWidgetComponent,
MarkdownWidgetComponent
MarkdownWidgetComponent,
LegendComponent,
FlotWidgetComponent
],
providers: [
{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.
-->
<mat-tab-group mat-stretch-tabs="false" class="tb-widget-config tb-absolute-fill" [(selectedIndex)]="selectedTab">
<mat-tab label="{{ 'widget-config.data' | translate }}" *ngIf="widgetType !== widgetTypes.static">
<div [formGroup]="dataSettings" class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px">
<div *ngIf="displayTimewindowConfig()" fxFlex="100"
fxLayout.xs="column" fxLayoutGap.gt-xs="8px" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxFlex.gt-xs>
<mat-checkbox formControlName="useDashboardTimewindow">
{{ 'widget-config.use-dashboard-timewindow' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="displayTimewindow">
{{ 'widget-config.display-timewindow' | translate }}
</mat-checkbox>
<div class="tb-widget-config tb-absolute-fill">
<div class="tb-widget-config-header" *ngIf="!hideHeader">
<tb-toggle-header *ngIf="!hideToggleHeader && widgetConfigMode === widgetConfigModes.advanced" (valueChange)="selectedOption = $event" ignoreMdLgSize="true"
appearance="fill" [options]="headerOptions" [value]="selectedOption" name="widgetConfigHeader" useSelectOnMdLg="false">
</tb-toggle-header>
<ng-content select=".tb-widget-config-header-prefix"></ng-content>
<ng-content select=".tb-widget-config-header-suffix"></ng-content>
</div>
<div *ngIf="widgetConfigMode === widgetConfigModes.advanced; else basicMode" class="tb-widget-config-content">
<div *ngIf="widgetType !== widgetTypes.static" [fxShow]="selectedOption === 'data'" class="mat-content">
<ng-container *ngTemplateOutlet="data"></ng-container>
</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>
<section fxLayout="row" fxLayoutAlign="start center" fxLayout.lt-lg="column" fxLayoutAlign.lt-lg="center start"
fxFlex.gt-xs>
<span [ngClass]="{'tb-disabled-label': dataSettings.get('useDashboardTimewindow').value}" translate
style="padding-right: 8px;">widget-config.timewindow</span>
<tb-timewindow asButton="true"
isEdit="true"
alwaysDisplayTypePrefix
[historyOnly]="onlyHistoryTimewindow()"
quickIntervalOnly="{{ widgetType === widgetTypes.latest }}"
aggregation="{{ widgetType === widgetTypes.timeseries }}"
fxFlex formControlName="timewindow"></tb-timewindow>
</section>
</div>
<div *ngIf="widgetType === widgetTypes.alarm" fxLayout="column" fxLayoutAlign="center">
<tb-alarm-filter-config buttonMode="false" formControlName="alarmFilterConfig"></tb-alarm-filter-config>
</div>
<mat-accordion multi>
<mat-expansion-panel (opened)="extensionPanelIsOpen(true)" (closed)="extensionPanelIsOpen(false)" class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc &&
widgetType !== widgetTypes.alarm &&
modelValue?.isDataEnabled" [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title fxLayout="row">
<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>
<div class="tb-widget-config-row space-between same-padding">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.display-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-material-icon-select asBoxInput
[color]="widgetSettings.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="width: 60px;">
<input matInput formControlName="iconSize">
</mat-form-field>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header expandedHeight="48px">
<mat-panel-description fxLayoutAlign="start" translate>
widget-config.advanced-title-style
</mat-panel-description>
</mat-expansion-panel-header>
<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 fxFlex fxLayout="row" fxLayoutAlign="start center">
<span style="width: 68px;"></span>
<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 matExpansionPanelContent>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
required
label="{{ 'widget-config.title-style' | translate }}"
formControlName="titleStyle"
></tb-json-object-edit>
</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 class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc &&
modelValue?.isDataEnabled" [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'widget-config.target-device' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<div [formGroup]="targetDeviceSettings" style="padding: 0 5px;">
<tb-entity-alias-select fxFlex
[tbRequired]="!widgetEditMode"
[aliasController]="aliasController"
[allowedEntityTypes]="[entityTypes.DEVICE]"
[callbacks]="widgetConfigCallbacks"
formControlName="targetDeviceAliasId">
</tb-entity-alias-select>
</div>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widget-config.card-style</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.text' | 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>
</mat-expansion-panel>
<mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.alarm &&
modelValue?.isDataEnabled" [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
{{ 'widget-config.alarm-source' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<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 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>
</mat-expansion-panel>
<mat-expansion-panel [formGroup]="widgetSettings">
</div>
<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-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>
<ng-template matExpansionPanelContent>
<div fxLayout="row" *ngIf="widgetType !== widgetTypes.rpc &&
widgetType !== widgetTypes.alarm &&
modelValue?.isDataEnabled && !modelValue?.typeParameters?.singleEntity">
<mat-form-field fxFlex>
<mat-label translate>widget-config.data-page-size</mat-label>
<input matInput formControlName="pageSize" type="number" min="1" step="1">
</mat-form-field>
</div>
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px">
<mat-form-field fxFlex>
<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>
<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>
</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>
</mat-tab>
<mat-tab label="{{ 'widget-config.settings' | translate }}">
<div class="mat-content mat-padding" fxLayout="column">
<div [formGroup]="widgetSettings" fxLayout="column">
<fieldset class="fields-group">
<legend class="group-title" translate>widget-config.title</legend>
<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>
<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 [fxShow]="selectedOption === 'actions'" [formGroup]="actionsSettings" class="mat-content" style="height: 100%;">
<div class="tb-widget-config-panel" style="height: 100%;">
<tb-manage-widget-actions
[callbacks]="widgetConfigCallbacks"
[widgetType] = "modelValue.widgetType"
[actionSources]="modelValue.actionSources"
formControlName="actions">
</tb-manage-widget-actions>
</div>
</div>
</mat-tab>
<mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}">
<div [formGroup]="advancedSettings" class="mat-content mat-padding tb-advanced-widget-config"
fxLayout="column">
<tb-widget-settings
[aliasController]="aliasController"
[dashboard]="dashboard"
[widget]="widget"
formControlName="settings">
</tb-widget-settings>
<div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content">
<div class="tb-widget-config-panel">
<mat-slide-toggle class="mat-slide" formControlName="mobileHide">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-widget-config-panel">
<mat-slide-toggle class="mat-slide" formControlName="desktopHide">
{{ 'widget-config.desktop-hide' | translate }}
</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>
</mat-tab>
<mat-tab label="{{ 'widget-config.actions' | translate }}" [formGroup]="actionsSettings">
<tb-manage-widget-actions
[callbacks]="widgetConfigCallbacks"
[widgetType] = "modelValue.widgetType"
formControlName="actionsData">
</tb-manage-widget-actions>
</mat-tab>
</mat-tab-group>
</ng-template>
<ng-template #data>
<ng-container *ngIf="displayTimewindowConfig" [formGroup]="dataSettings">
<tb-timewindow-config-panel
[onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="timewindowConfig">
</tb-timewindow-config-panel>
</ng-container>
<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
* limitations under the License.
*/
@import "../../../../../theme";
@import '../../../../../scss/constants';
.tb-widget-config {
.tb-panel-hint {
font-size: 12px;
color: #808080;
}
}
.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;
flex-direction: column;
gap: 16px;
.tb-widget-config-header {
padding: 24px 24px 8px;
height: 56px;
display: flex;
flex-direction: column;
align-items: stretch;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.tb-widget-config-content {
flex: 1;
padding-top: 20px;
@media #{$mat-gt-sm} {
flex-direction: row;
align-items: center;
justify-content: flex-start;
overflow: auto;
& > div {
padding: 16px;
}
}
.tb-datasource-type {
min-width: 160px;
@media #{$mat-gt-sm} {
max-width: 160px;
.mat-content {
display: flex;
flex-direction: column;
gap: 16px;
}
}
.tb-datasource {
@media #{$mat-gt-sm} {
padding-left: 8px;
width: 208px;
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;
}
.tb-basic-mode-directive-error {
font-size: 13px;
font-weight: 400;
color: rgb(221, 44, 0);
}
}
:host ::ng-deep {
.tb-widget-config {
tb-alarm-filter-config {
.mdc-button {
width: 100%;
height: 100%;
justify-content: flex-start;
.tb-widget-config-header {
.mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
padding: 0 20px;
}
}
.mat-mdc-tab-body-wrapper {
@ -134,53 +62,5 @@
right: 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.
-->
<div class="tb-absolute-fill" [fxLayout]="legendContainerLayoutType" tb-toast
<div class="tb-absolute-fill" fxLayout="column" tb-toast
toastTarget="{{ toastTargetId }}">
<tb-legend *ngIf="displayLegend && isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
<div fxFlex id="widget-container">
<ng-container #widgetContent></ng-container>
</div>
<tb-legend *ngIf="displayLegend && !isLegendFirst"
[ngStyle]="legendStyle"
[legendConfig]="legendConfig"
[legendData]="legendData"
(legendKeyHiddenChange)="onLegendKeyHiddenChange($event)">
</tb-legend>
</div>
<div class="tb-absolute-fill tb-widget-error" *ngIf="widgetErrorData">
<span [innerHtml]="('Widget Error:<br/><br/>' + widgetErrorData.message) | safe:'html'"></span>
@ -42,5 +30,5 @@
class="tb-absolute-fill">{{ noDataDisplayMessageText }}</span>
</div>
<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>

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

@ -39,10 +39,6 @@ import {
} from '@angular/core';
import { DashboardWidget } from '@home/models/dashboard-component.models';
import {
defaultLegendConfig,
LegendConfig,
LegendData,
LegendPosition,
Widget,
WidgetActionDescriptor,
widgetActionSources,
@ -152,13 +148,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
displayNoData = false;
noDataDisplayMessageText: string;
displayLegend: boolean;
legendConfig: LegendConfig;
legendData: LegendData;
isLegendFirst: boolean;
legendContainerLayoutType: string;
legendStyle: {[klass: string]: any};
dynamicWidgetComponentRef: ComponentRef<IDynamicWidgetComponent>;
dynamicWidgetComponent: IDynamicWidgetComponent;
@ -220,57 +209,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
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>} = {};
if (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() {
this.widgetContext.widgetNamespace =
`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(
(subscription) => {
if (useDefaultComponents) {
this.defaultSubscriptionOptions(subscription, options);
}
createSubscriptionSubject.next(subscription);
createSubscriptionSubject.complete();
},
@ -904,8 +832,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
? this.widget.config.displayTimewindow : !options.useDashboardTimewindow;
options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow;
options.legendConfig = null;
if (this.displayLegend) {
options.legendConfig = this.legendConfig;
if (this.widget.config.settings.showLegend === true) {
options.legendConfig = this.widget.config.settings.legendConfig;
}
options.decimals = this.widgetContext.decimals;
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> {
@ -1011,7 +932,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.createSubscription(options).subscribe(
(subscription) => {
this.defaultSubscriptionOptions(subscription, options);
// backward compatibility
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 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 {
widgetName: string;
config: WidgetConfig;
layout: WidgetLayout;
widgetType: widgetType;
@ -459,6 +460,7 @@ export interface WidgetConfigComponentData {
settingsDirective: string;
dataKeySettingsDirective: string;
latestDataKeySettingsDirective: string;
basicModeDirective: string;
}
export const MissingWidgetType: WidgetInfo = {
@ -551,6 +553,8 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
settingsDirective: widgetTypeEntity.descriptor.settingsDirective,
dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective,
hasBasicMode: widgetTypeEntity.descriptor.hasBasicMode,
basicModeDirective: widgetTypeEntity.descriptor.basicModeDirective,
defaultConfig: widgetTypeEntity.descriptor.defaultConfig
};
}
@ -581,6 +585,8 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
hasBasicMode: widgetInfo.hasBasicMode,
basicModeDirective: widgetInfo.basicModeDirective,
defaultConfig: widgetInfo.defaultConfig
};
return {

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

@ -270,6 +270,17 @@
[(ngModel)]="widget.latestDataKeySettingsDirective"
(ngModelChange)="isDirty = true"/>
</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>
</mat-tab>

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

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-form-field [formGroup]="colorFormGroup">
<mat-form-field *ngIf="!asBoxInput; else boxInput" [formGroup]="colorFormGroup">
<mat-label *ngIf="icon || label">
<mat-icon *ngIf="icon">{{icon}}</mat-icon>
<span *ngIf="label">{{label}}</span>
@ -34,3 +34,8 @@
{{ requiredText }}
</mat-error>
</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 {
margin-left: 8px;
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.
///
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -22,6 +22,7 @@ import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_AC
import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DialogService } from '@core/services/dialog.service';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-color-input',
@ -37,6 +38,10 @@ import { DialogService } from '@core/services/dialog.service';
})
export class ColorInputComponent extends PageComponent implements OnInit, ControlValueAccessor {
@Input()
@coerceBoolean()
asBoxInput = false;
@Input()
icon: string;
@ -95,7 +100,8 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
constructor(protected store: Store<AppState>,
private dialogs: DialogService,
private translate: TranslateService,
private fb: UntypedFormBuilder) {
private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) {
super(store);
}
@ -154,6 +160,7 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
this.colorFormGroup.patchValue(
{color}, {emitEvent: true}
);
this.cd.markForCheck();
}
}
);
@ -161,5 +168,6 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
clear() {
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 { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { isEqual } from '@core/utils';
import { isDefinedAndNotNull, isEqual } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
@ -291,7 +291,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
async writeValue(value: string | EntityId | null): Promise<void> {
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 id: 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.
-->
<div fxLayout="row" [formGroup]="materialIconFormGroup">
<mat-icon class="icon-value" (click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon>
<div *ngIf="!asBoxInput; else boxInput" fxLayout="row" [formGroup]="materialIconFormGroup">
<mat-icon class="icon-value" [ngStyle]="color ? { color: color } : {}" (click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon>
<mat-form-field fxFlex>
<mat-label>{{ label }}</mat-label>
<input [required]="required" matInput formControlName="icon" (mousedown)="openIconDialog()">
@ -28,3 +28,8 @@
</button>
</mat-form-field>
</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.
*/
:host {
.mat-icon.icon-value {
padding: 4px;
margin: 12px 4px 4px;
cursor: pointer;
border: solid 1px rgba(0, 0, 0, .27);
box-sizing: initial;
.mat-icon {
&.icon-value {
padding: 4px;
margin: 12px 4px 4px;
cursor: pointer;
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.
///
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 { Store } from '@ngrx/store';
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 { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-material-icon-select',
@ -37,9 +38,16 @@ import { TranslateService } from '@ngx-translate/core';
})
export class MaterialIconSelectComponent extends PageComponent implements OnInit, ControlValueAccessor {
@Input()
@coerceBoolean()
asBoxInput = false;
@Input()
label = this.translate.instant('icon.icon');
@Input()
color: string;
@Input()
disabled: boolean;
@ -73,7 +81,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
constructor(protected store: Store<AppState>,
private dialogs: DialogService,
private translate: TranslateService,
private fb: UntypedFormBuilder) {
private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) {
super(store);
}
@ -126,6 +135,7 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
this.materialIconFormGroup.patchValue(
{icon}, {emitEvent: true}
);
this.cd.markForCheck();
}
}
);
@ -134,5 +144,6 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
clear() {
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 './slack-conversation-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.
-->
<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)">
<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>
<ng-template #select>
<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.
*/
@import "../../../../../../../scss/constants";
@import "../../../theme";
@import "../../../scss/constants";
:host ::ng-deep {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
@ -47,8 +48,8 @@
&.mat-button-toggle-checked {
.mat-button-toggle-button {
background: #F3F6FA;
color: #305680;
border: 1px solid #305680;
color: $tb-primary-color;
border: 1px solid $tb-primary-color;
border-radius: 20px;
.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} {
.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;
.mat-button-toggle.mat-button-toggle-appearance-standard {
.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 { MediaBreakpoints } from '@shared/models/constants';
import { coerceBoolean } from '@shared/decorators/coercion';
import { BreadCrumb } from '@shared/components/breadcrumb';
export interface ToggleHeaderOption {
name: string;
value: any;
}
export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked';
@Component({
selector: 'tb-toggle-header',
templateUrl: './toggle-header.component.html',
@ -67,6 +70,13 @@ export class ToggleHeaderComponent extends PageComponent implements OnInit {
@coerceBoolean()
useSelectOnMdLg = true;
@Input()
@coerceBoolean()
ignoreMdLgSize = false;
@Input()
appearance: ToggleHeaderAppearance = 'stroked';
isMdLg: boolean;
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 singleEntityFilterFromDeviceId = (deviceId: string): EntityFilter => ({
type: AliasFilterType.singleEntity,
singleEntity: {
entityType: EntityType.DEVICE,
id: deviceId
}
});
export interface EntityCountQuery {
entityFilter: EntityFilter;
keyFilters?: Array<KeyFilter>;

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

@ -162,6 +162,8 @@ export interface WidgetTypeDescriptor {
settingsDirective?: string;
dataKeySettingsDirective?: string;
latestDataKeySettingsDirective?: string;
hasBasicMode?: boolean;
basicModeDirective?: string;
defaultConfig: string;
sizeX: number;
sizeY: number;
@ -252,18 +254,16 @@ export interface LegendConfig {
showLatest: boolean;
}
export function defaultLegendConfig(wType: widgetType): LegendConfig {
return {
direction: LegendDirection.column,
position: LegendPosition.bottom,
sortDataKeys: false,
showMin: false,
showMax: false,
showAvg: wType === widgetType.timeseries,
showTotal: false,
showLatest: false
};
}
export const defaultLegendConfig = (wType: widgetType): LegendConfig => ({
direction: LegendDirection.column,
position: LegendPosition.bottom,
sortDataKeys: false,
showMin: false,
showMax: false,
showAvg: wType === widgetType.timeseries,
showTotal: false,
showLatest: false
});
export enum ComparisonResultType {
PREVIOUS_VALUE = 'PREVIOUS_VALUE',
@ -320,6 +320,7 @@ export interface DataKey extends KeyInfo {
export enum DatasourceType {
function = 'function',
device = 'device',
entity = 'entity',
entityCount = 'entityCount',
alarmCount = 'alarmCount'
@ -328,6 +329,7 @@ export enum DatasourceType {
export const datasourceTypeTranslationMap = new Map<DatasourceType, string>(
[
[ DatasourceType.function, 'function.function' ],
[ DatasourceType.device, 'device.device' ],
[ DatasourceType.entity, 'entity.entity' ],
[ DatasourceType.entityCount, 'entity.entities-count' ],
[ DatasourceType.alarmCount, 'entity.alarms-count' ]
@ -343,6 +345,7 @@ export interface Datasource {
entityType?: EntityType;
entityId?: string;
entityName?: string;
deviceId?: string;
entityAliasId?: string;
filterId?: string;
unresolvedStateEntity?: boolean;
@ -363,7 +366,7 @@ export interface Datasource {
[key: string]: any;
}
export function datasourcesHasAggregation(datasources?: Array<Datasource>): boolean {
export const datasourcesHasAggregation = (datasources?: Array<Datasource>): boolean => {
if (datasources) {
const foundDatasource = datasources.find(datasource => {
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;
}
};
export function datasourcesHasOnlyComparisonAggregation(datasources?: Array<Datasource>): boolean {
export const datasourcesHasOnlyComparisonAggregation = (datasources?: Array<Datasource>): boolean => {
if (!datasourcesHasAggregation(datasources)) {
return false;
}
@ -392,7 +395,7 @@ export function datasourcesHasOnlyComparisonAggregation(datasources?: Array<Data
}
}
return true;
}
};
export interface FormattedData {
$datasource: Datasource;
@ -602,7 +605,13 @@ export interface WidgetSettings {
[key: string]: any;
}
export enum WidgetConfigMode {
basic = 'basic',
advanced = 'advanced'
}
export interface WidgetConfig {
configMode?: WidgetConfigMode;
title?: string;
titleIcon?: string;
showTitle?: boolean;
@ -614,8 +623,6 @@ export interface WidgetConfig {
enableFullscreen?: boolean;
useDashboardTimewindow?: boolean;
displayTimewindow?: boolean;
showLegend?: boolean;
legendConfig?: LegendConfig;
timewindow?: Timewindow;
desktopHide?: boolean;
mobileHide?: boolean;
@ -700,7 +707,7 @@ export interface IWidgetSettingsComponent {
[key: string]: any;
}
function removeEmptyWidgetSettings(settings: WidgetSettings): WidgetSettings {
const removeEmptyWidgetSettings = (settings: WidgetSettings): WidgetSettings => {
if (settings) {
const keys = Object.keys(settings);
for (const key of keys) {
@ -711,7 +718,7 @@ function removeEmptyWidgetSettings(settings: WidgetSettings): WidgetSettings {
}
}
return settings;
}
};
@Directive()
// 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';
import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component';
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
import { ToggleHeaderComponent } from '@shared/components/toggle-header.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -358,7 +359,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutAlignDirective,
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent
ColorPickerComponent,
ToggleHeaderComponent
],
imports: [
CommonModule,
@ -583,7 +585,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutAlignDirective,
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent
ColorPickerComponent,
ToggleHeaderComponent
]
})
export class SharedModule { }

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

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

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

Loading…
Cancel
Save