Browse Source

UI: Basic widget config mode directive support

pull/8708/head
Igor Kulikov 3 years ago
parent
commit
bf749ff19b
  1. 3
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 61
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  3. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  4. 9
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  5. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  6. 33
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  7. 3
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html
  8. 5
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts
  9. 4
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html
  10. 6
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss
  11. 31
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts
  12. 36
      ui-ngx/src/app/modules/home/components/widget/basic-config/action/widget-actions-panel.component.html
  13. 139
      ui-ngx/src/app/modules/home/components/widget/basic-config/action/widget-actions-panel.component.ts
  14. 20
      ui-ngx/src/app/modules/home/components/widget/basic-config/basic-config.scss
  15. 49
      ui-ngx/src/app/modules/home/components/widget/basic-config/basic-widget-config.module.ts
  16. 42
      ui-ngx/src/app/modules/home/components/widget/basic-config/cards/simple-card-basic-config.component.html
  17. 58
      ui-ngx/src/app/modules/home/components/widget/basic-config/cards/simple-card-basic-config.component.ts
  18. 18
      ui-ngx/src/app/modules/home/components/widget/datasource.component.html
  19. 6
      ui-ngx/src/app/modules/home/components/widget/datasource.component.ts
  20. 14
      ui-ngx/src/app/modules/home/components/widget/datasources.component.html
  21. 90
      ui-ngx/src/app/modules/home/components/widget/datasources.component.ts
  22. 29
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html
  23. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts
  24. 61
      ui-ngx/src/app/modules/home/components/widget/widget-config-components.module.ts
  25. 40
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  26. 95
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts
  27. 5
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  28. 185
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  29. 1
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  30. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -103,7 +103,8 @@
"dataKeySettingsSchema": "", "dataKeySettingsSchema": "",
"settingsDirective": "tb-simple-card-widget-settings", "settingsDirective": "tb-simple-card-widget-settings",
"hasBasicMode": true, "hasBasicMode": true,
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temp\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\"}" "basicModeDirective": "tb-simple-card-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\"}"
} }
}, },
{ {

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

@ -29,21 +29,25 @@ import {
GridSettings, GridSettings,
WidgetLayout WidgetLayout
} from '@shared/models/dashboard.models'; } from '@shared/models/dashboard.models';
import { isDefined, isDefinedAndNotNull, isString, isUndefined } from '@core/utils'; import { deepClone, isDefined, isDefinedAndNotNull, isString, isUndefined } from '@core/utils';
import { import {
DataKey,
Datasource, Datasource,
datasourcesHasOnlyComparisonAggregation, datasourcesHasOnlyComparisonAggregation,
DatasourceType, DatasourceType,
defaultLegendConfig, defaultLegendConfig,
Widget, Widget,
WidgetConfig, WidgetConfig,
widgetType WidgetConfigMode,
widgetType,
WidgetTypeDescriptor
} from '@app/shared/models/widget.models'; } from '@app/shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models';
import { EntityId } from '@app/shared/models/id/entity-id'; import { EntityId } from '@app/shared/models/id/entity-id';
import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models'; import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -332,6 +336,55 @@ export class DashboardUtilsService {
}; };
} }
public widgetConfigFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor): WidgetConfig {
const config: WidgetConfig = JSON.parse(widgetTypeDescriptor.defaultConfig);
config.datasources = this.convertDatasourcesFromWidgetType(widgetTypeDescriptor, config, config.datasources);
if (isDefinedAndNotNull(config.alarmSource)) {
config.alarmSource = this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, config.alarmSource);
}
return config;
}
private convertDatasourcesFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor,
config: WidgetConfig, datasources?: Datasource[]): Datasource[] {
const newDatasources: Datasource[] = [];
if (datasources) {
datasources.forEach(datasource => {
newDatasources.push(this.convertDatasourceFromWidgetType(widgetTypeDescriptor, config, datasource));
});
}
return newDatasources;
}
private convertDatasourceFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, config: WidgetConfig,
datasource: Datasource): Datasource {
const newDatasource = deepClone(datasource);
if (newDatasource.type === DatasourceType.function) {
newDatasource.type = DatasourceType.entity;
if (widgetTypeDescriptor.hasBasicMode && config.configMode === WidgetConfigMode.basic) {
newDatasource.type = DatasourceType.device;
}
const dataKeys = newDatasource.dataKeys;
newDatasource.dataKeys = [];
dataKeys.forEach(dataKey => {
newDatasource.dataKeys.push(this.convertDataKeyFromWidgetType(widgetTypeDescriptor, config, dataKey));
});
}
return newDatasource;
}
private convertDataKeyFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, config: WidgetConfig, dataKey: DataKey): DataKey {
const newDataKey = deepClone(dataKey);
newDataKey.funcBody = null;
if (widgetTypeDescriptor.type === widgetType.alarm) {
newDataKey.type = DataKeyType.alarm;
} else {
newDataKey.type = DataKeyType.timeseries;
newDataKey.name = newDataKey.label;
}
return newDataKey;
}
private validateAndUpdateState(state: DashboardState) { private validateAndUpdateState(state: DashboardState) {
if (!state.layouts) { if (!state.layouts) {
state.layouts = this.createDefaultLayouts(); state.layouts = this.createDefaultLayouts();
@ -445,7 +498,7 @@ export class DashboardUtilsService {
targetLayout: DashboardLayoutId, targetLayout: DashboardLayoutId,
widget: Widget, widget: Widget,
originalColumns?: number, originalColumns?: number,
originalSize?: {sizeX: number, sizeY: number}, originalSize?: {sizeX: number; sizeY: number},
row?: number, row?: number,
column?: number): void { column?: number): void {
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
@ -518,7 +571,7 @@ export class DashboardUtilsService {
this.removeUnusedWidgets(dashboard); this.removeUnusedWidgets(dashboard);
} }
public isSingleLayoutDashboard(dashboard: Dashboard): {state: string, layout: DashboardLayoutId} { public isSingleLayoutDashboard(dashboard: Dashboard): {state: string; layout: DashboardLayoutId} {
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states; const states = dashboardConfiguration.states;
const stateKeys = Object.keys(states); const stateKeys = Object.keys(states);

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

@ -128,7 +128,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
latestDataKeySettingsSchema, latestDataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective, settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
basicModeDirective: widgetInfo.basicModeDirective
}; };
this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false; this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false;
this.widgetFormGroup = this.fb.group({ this.widgetFormGroup = this.fb.group({

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

@ -1142,15 +1142,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.searchBundle = ''; this.searchBundle = '';
this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe( this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe(
(widgetTypeInfo) => { (widgetTypeInfo) => {
const config: WidgetConfig = JSON.parse(widgetTypeInfo.defaultConfig); const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo);
config.title = 'New ' + widgetTypeInfo.widgetName; config.title = 'New ' + widgetTypeInfo.widgetName;
config.datasources = [];
if (isDefinedAndNotNull(config.alarmSource)) {
config.alarmSource = {
type: DatasourceType.entity,
dataKeys: config.alarmSource.dataKeys || []
};
}
let newWidget: Widget = { let newWidget: Widget = {
isSystemType: widget.isSystemType, isSystemType: widget.isSystemType,
bundleAlias: widget.bundleAlias, bundleAlias: widget.bundleAlias,

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

@ -169,6 +169,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
settingsDirective: widgetInfo.settingsDirective, settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective, latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
basicModeDirective: widgetInfo.basicModeDirective
}; };
this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false; this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false;
this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); this.widgetFormGroup.reset({widgetConfig: this.widgetConfig});

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

@ -44,10 +44,6 @@ import { EntityFilterViewComponent } from '@home/components/entity/entity-filter
import { EntityAliasDialogComponent } from '@home/components/alias/entity-alias-dialog.component'; import { EntityAliasDialogComponent } from '@home/components/alias/entity-alias-dialog.component';
import { EntityFilterComponent } from '@home/components/entity/entity-filter.component'; import { EntityFilterComponent } from '@home/components/entity/entity-filter.component';
import { RelationFiltersComponent } from '@home/components/relation/relation-filters.component'; import { RelationFiltersComponent } from '@home/components/relation/relation-filters.component';
import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-select.component';
import { DataKeysComponent } from '@home/components/widget/data-keys.component';
import { DataKeyConfigDialogComponent } from '@home/components/widget/data-key-config-dialog.component';
import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component';
import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component'; import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component';
import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component'; import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component';
import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component';
@ -76,7 +72,6 @@ import { ComplexFilterPredicateDialogComponent } from '@home/components/filter/c
import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dialog.component'; import { KeyFilterDialogComponent } from '@home/components/filter/key-filter-dialog.component';
import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component'; import { FiltersDialogComponent } from '@home/components/filter/filters-dialog.component';
import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component'; import { FilterDialogComponent } from '@home/components/filter/filter-dialog.component';
import { FilterSelectComponent } from '@home/components/filter/filter-select.component';
import { FiltersEditComponent } from '@home/components/filter/filters-edit.component'; import { FiltersEditComponent } from '@home/components/filter/filters-edit.component';
import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component'; import { FiltersEditPanelComponent } from '@home/components/filter/filters-edit-panel.component';
import { UserFilterDialogComponent } from '@home/components/filter/user-filter-dialog.component'; import { UserFilterDialogComponent } from '@home/components/filter/user-filter-dialog.component';
@ -150,8 +145,6 @@ import { AlarmDynamicValue } from '@home/components/profile/alarm/alarm-dynamic-
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.component'; import { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.component';
import { QueueFormComponent } from '@home/components/queue/queue-form.component'; import { QueueFormComponent } from '@home/components/queue/queue-form.component';
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
import { WidgetSettingsComponent } from '@home/components/widget/widget-settings.component';
import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component'; import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { EntityVersionsTableComponent } from '@home/components/vc/entity-versions-table.component'; import { EntityVersionsTableComponent } from '@home/components/vc/entity-versions-table.component';
@ -176,16 +169,15 @@ import { modulesMap } from '@modules/common/modules-map';
import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component'; import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component';
import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component'; import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component';
import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component';
import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component'; import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component';
import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component'; import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component';
import { DeviceInfoFilterComponent } from '@home/components/device/device-info-filter.component'; import { DeviceInfoFilterComponent } from '@home/components/device/device-info-filter.component';
import { WidgetPreviewComponent } from '@home/components/widget/widget-preview.component'; import { WidgetPreviewComponent } from '@home/components/widget/widget-preview.component';
import { DatasourceComponent } from '@home/components/widget/datasource.component';
import { DatasourcesComponent } from '@home/components/widget/datasources.component';
import { import {
ManageWidgetActionsDialogComponent ManageWidgetActionsDialogComponent
} from '@home/components/widget/action/manage-widget-actions-dialog.component'; } from '@home/components/widget/action/manage-widget-actions-dialog.component';
import { WidgetConfigComponentsModule } from '@home/components/widget/widget-config-components.module';
import { BasicWidgetConfigModule } from '@home/components/widget/basic-config/basic-widget-config.module';
@NgModule({ @NgModule({
declarations: declarations:
@ -212,7 +204,6 @@ import {
AlarmAssigneePanelComponent, AlarmAssigneePanelComponent,
AlarmAssigneeSelectComponent, AlarmAssigneeSelectComponent,
AlarmAssigneeSelectPanelComponent, AlarmAssigneeSelectPanelComponent,
AlarmFilterConfigComponent,
AttributeTableComponent, AttributeTableComponent,
AddAttributeDialogComponent, AddAttributeDialogComponent,
EditAttributeValuePanelComponent, EditAttributeValuePanelComponent,
@ -224,17 +215,10 @@ import {
DashboardComponent, DashboardComponent,
WidgetContainerComponent, WidgetContainerComponent,
WidgetComponent, WidgetComponent,
WidgetSettingsComponent,
WidgetConfigComponent, WidgetConfigComponent,
WidgetPreviewComponent, WidgetPreviewComponent,
EntityFilterViewComponent, EntityFilterViewComponent,
EntityFilterComponent, EntityFilterComponent,
EntityAliasSelectComponent,
DataKeysComponent,
DatasourceComponent,
DatasourcesComponent,
DataKeyConfigComponent,
DataKeyConfigDialogComponent,
ManageWidgetActionsComponent, ManageWidgetActionsComponent,
WidgetActionDialogComponent, WidgetActionDialogComponent,
ManageWidgetActionsDialogComponent, ManageWidgetActionsDialogComponent,
@ -259,7 +243,6 @@ import {
KeyFilterDialogComponent, KeyFilterDialogComponent,
FilterDialogComponent, FilterDialogComponent,
FiltersDialogComponent, FiltersDialogComponent,
FilterSelectComponent,
FilterTextComponent, FilterTextComponent,
FiltersEditComponent, FiltersEditComponent,
FiltersEditPanelComponent, FiltersEditPanelComponent,
@ -344,7 +327,8 @@ import {
CommonModule, CommonModule,
SharedModule, SharedModule,
SharedHomeComponentsModule, SharedHomeComponentsModule,
WidgetSettingsModule, WidgetConfigComponentsModule,
BasicWidgetConfigModule,
Lwm2mProfileComponentsModule, Lwm2mProfileComponentsModule,
SnmpDeviceProfileTransportModule, SnmpDeviceProfileTransportModule,
StatesControllerModule, StatesControllerModule,
@ -368,7 +352,6 @@ import {
AlarmAssigneePanelComponent, AlarmAssigneePanelComponent,
AlarmAssigneeSelectComponent, AlarmAssigneeSelectComponent,
AlarmAssigneeSelectPanelComponent, AlarmAssigneeSelectPanelComponent,
AlarmFilterConfigComponent,
AttributeTableComponent, AttributeTableComponent,
AliasesEntitySelectComponent, AliasesEntitySelectComponent,
AliasesEntityAutocompleteComponent, AliasesEntityAutocompleteComponent,
@ -377,17 +360,10 @@ import {
DashboardComponent, DashboardComponent,
WidgetContainerComponent, WidgetContainerComponent,
WidgetComponent, WidgetComponent,
WidgetSettingsComponent,
WidgetConfigComponent, WidgetConfigComponent,
WidgetPreviewComponent, WidgetPreviewComponent,
EntityFilterViewComponent, EntityFilterViewComponent,
EntityFilterComponent, EntityFilterComponent,
EntityAliasSelectComponent,
DataKeysComponent,
DatasourceComponent,
DatasourcesComponent,
DataKeyConfigComponent,
DataKeyConfigDialogComponent,
ManageWidgetActionsComponent, ManageWidgetActionsComponent,
WidgetActionDialogComponent, WidgetActionDialogComponent,
ManageWidgetActionsDialogComponent, ManageWidgetActionsDialogComponent,
@ -411,7 +387,6 @@ import {
KeyFilterDialogComponent, KeyFilterDialogComponent,
FilterDialogComponent, FilterDialogComponent,
FiltersDialogComponent, FiltersDialogComponent,
FilterSelectComponent,
FilterTextComponent, FilterTextComponent,
FiltersEditComponent, FiltersEditComponent,
UserFilterDialogComponent, UserFilterDialogComponent,

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

@ -32,7 +32,8 @@
<tb-manage-widget-actions <tb-manage-widget-actions
[callbacks]="data.callbacks" [callbacks]="data.callbacks"
[widgetType] = "data.widgetType" [widgetType] = "data.widgetType"
formControlName="actionsData"> [actionSources]="actionSources"
formControlName="actions">
</tb-manage-widget-actions> </tb-manage-widget-actions>
</div> </div>
<div mat-dialog-actions fxLayoutAlign="end center"> <div mat-dialog-actions fxLayoutAlign="end center">

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

@ -43,6 +43,7 @@ export interface ManageWidgetActionsDialogData {
export class ManageWidgetActionsDialogComponent extends DialogComponent<ManageWidgetActionsDialogComponent, export class ManageWidgetActionsDialogComponent extends DialogComponent<ManageWidgetActionsDialogComponent,
WidgetActionsData> implements OnInit { WidgetActionsData> implements OnInit {
actionSources = this.data.actionsData.actionSources;
actionsSettings: UntypedFormGroup; actionsSettings: UntypedFormGroup;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -55,7 +56,7 @@ export class ManageWidgetActionsDialogComponent extends DialogComponent<ManageWi
ngOnInit() { ngOnInit() {
this.actionsSettings = this.fb.group({ this.actionsSettings = this.fb.group({
actionsData: [this.data.actionsData, []] actions: [this.data.actionsData.actionsMap, []]
}); });
} }
@ -64,7 +65,7 @@ export class ManageWidgetActionsDialogComponent extends DialogComponent<ManageWi
} }
save(): void { save(): void {
this.dialogRef.close(this.actionsSettings.get('actionsData').value); this.dialogRef.close(this.actionsSettings.get('actions').value);
} }
} }

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

@ -69,7 +69,7 @@
class="tb-drop-list"> class="tb-drop-list">
<ng-container matColumnDef="actionSourceId"> <ng-container matColumnDef="actionSourceId">
<mat-header-cell *matHeaderCellDef style="width: 36px"></mat-header-cell> <mat-header-cell *matHeaderCellDef style="width: 36px"></mat-header-cell>
<mat-cell *matCellDef="let action"> <mat-cell *matCellDef="let action" class="tb-icon-cell">
<div cdkDragHandle <div cdkDragHandle
class="tb-drag-handle" class="tb-drag-handle"
style="min-width: 36px" style="min-width: 36px"
@ -95,7 +95,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="icon"> <ng-container matColumnDef="icon">
<mat-header-cell *matHeaderCellDef style="width: 40px"> {{ 'widget-config.action-icon' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef style="width: 40px"> {{ 'widget-config.action-icon' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let action" style="width: 40px"> <mat-cell *matCellDef="let action" class="tb-icon-cell" style="width: 40px">
<mat-icon>{{ action.icon }}</mat-icon> <mat-icon>{{ action.icon }}</mat-icon>
</mat-cell> </mat-cell>
</ng-container> </ng-container>

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

@ -43,6 +43,12 @@
opacity: 1 !important; opacity: 1 !important;
} }
.mat-mdc-cell.tb-icon-cell {
.mat-icon {
vertical-align: middle;
}
}
.tb-draggable { .tb-draggable {
&.cdk-drag-placeholder { &.cdk-drag-placeholder {
opacity: 0; opacity: 0;

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

@ -76,6 +76,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
@Input() callbacks: WidgetActionCallbacks; @Input() callbacks: WidgetActionCallbacks;
@Input() actionSources: {[actionSourceId: string]: WidgetActionSource};
innerValue: WidgetActionsData; innerValue: WidgetActionsData;
displayedColumns: string[]; displayedColumns: string[];
@ -338,24 +340,25 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
this.disabled = isDisabled; this.disabled = isDisabled;
} }
writeValue(obj: WidgetActionsData): void { writeValue(actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>}): void {
this.innerValue = obj; this.innerValue = {
if (this.innerValue) { actionsMap: actions || {},
setTimeout(() => { actionSources: this.actionSources || {}
if (!this.destroyed) { };
this.dataSource.setActions(this.innerValue); setTimeout(() => {
if (this.viewsInited) { if (!this.destroyed) {
this.resetSortAndFilter(true); this.dataSource.setActions(this.innerValue);
} else { if (this.viewsInited) {
this.dirtyValue = true; this.resetSortAndFilter(true);
} } else {
this.dirtyValue = true;
} }
}, 0); }
} }, 0);
} }
private onActionsUpdated() { private onActionsUpdated() {
this.updateData(true); this.updateData(true);
this.propagateChange(this.innerValue); this.propagateChange(this.innerValue.actionsMap);
} }
} }

36
ui-ngx/src/app/modules/home/components/widget/basic-config/action/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>

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

@ -0,0 +1,139 @@
///
/// 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_VALIDATORS,
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 { Datasource, 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();
}
}
);
}
}

20
ui-ngx/src/app/modules/home/components/widget/basic-config/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/basic-config/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/widget-config.component.models';
import { WidgetConfigComponentsModule } from '@home/components/widget/widget-config-components.module';
import {
SimpleCardBasicConfigComponent
} from '@home/components/widget/basic-config/cards/simple-card-basic-config.component';
import {
WidgetActionsPanelComponent
} from '@home/components/widget/basic-config/action/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
};

42
ui-ngx/src/app/modules/home/components/widget/basic-config/cards/simple-card-basic-config.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.
-->
<ng-container [formGroup]="simpleCardWidgetConfigForm" >
<tb-datasources
[configMode]="basicMode"
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 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>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

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

@ -0,0 +1,58 @@
///
/// 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/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.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 {
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({
datasources: [configData.config.datasources, []],
labelPosition: [configData.config.settings?.labelPosition, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.datasources = this.simpleCardWidgetConfigForm.value.datasources;
this.widgetConfig.config.actions = this.simpleCardWidgetConfigForm.value.actions;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.labelPosition = this.simpleCardWidgetConfigForm.value.labelPosition;
return this.widgetConfig;
}
}

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

@ -16,7 +16,7 @@
--> -->
<section class="tb-datasource-section" [formGroup]="datasourceFormGroup"> <section class="tb-datasource-section" [formGroup]="datasourceFormGroup">
<mat-form-field class="tb-datasource-type" hideRequiredMarker> <mat-form-field *ngIf="!basicMode" class="tb-datasource-type" hideRequiredMarker>
<mat-label translate>widget-config.datasource-type</mat-label> <mat-label translate>widget-config.datasource-type</mat-label>
<mat-select formControlName="type"> <mat-select formControlName="type">
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType"> <mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
@ -24,7 +24,7 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<section fxLayout="column" class="tb-datasource" [ngSwitch]="datasourceFormGroup.get('type').value"> <section fxLayout="column" [ngSwitch]="datasourceFormGroup.get('type').value">
<ng-template [ngSwitchCase]="datasourceType.function"> <ng-template [ngSwitchCase]="datasourceType.function">
<mat-form-field fxFlex> <mat-form-field fxFlex>
<mat-label translate>datasource.label</mat-label> <mat-label translate>datasource.label</mat-label>
@ -53,13 +53,6 @@
formControlName="entityAliasId" formControlName="entityAliasId"
[callbacks]="entityAliasSelectCallbacks"> [callbacks]="entityAliasSelectCallbacks">
</tb-entity-alias-select> </tb-entity-alias-select>
<tb-filter-select
*ngIf="datasourceFormGroup.get('type').value !== datasourceType.alarmCount"
[showLabel]="true"
[aliasController]="aliasController"
formControlName="filterId"
[callbacks]="filterSelectCallbacks">
</tb-filter-select>
<mat-form-field *ngIf="[datasourceType.entityCount, datasourceType.alarmCount].includes(datasourceFormGroup.get('type').value)" <mat-form-field *ngIf="[datasourceType.entityCount, datasourceType.alarmCount].includes(datasourceFormGroup.get('type').value)"
fxFlex> fxFlex>
<input matInput <input matInput
@ -100,4 +93,11 @@
formControlName="latestDataKeys"> formControlName="latestDataKeys">
</tb-data-keys> </tb-data-keys>
</section> </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> </section>

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

@ -30,7 +30,7 @@ import {
DatasourceType, DatasourceType,
datasourceTypeTranslationMap, datasourceTypeTranslationMap,
JsonSettingsSchema, JsonSettingsSchema,
Widget, Widget, WidgetConfigMode,
widgetType widgetType
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models'; import { AlarmSearchStatus } from '@shared/models/alarm.models';
@ -61,6 +61,10 @@ import { EntityType } from '@shared/models/entity-type.models';
}) })
export class DatasourceComponent implements ControlValueAccessor, OnInit, Validator { export class DatasourceComponent implements ControlValueAccessor, OnInit, Validator {
public get basicMode(): boolean {
return !this.widgetConfigComponent.widgetEditMode && this.widgetConfigComponent.widgetConfigMode === WidgetConfigMode.basic;
}
public get widgetType(): widgetType { public get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType; return this.widgetConfigComponent.widgetType;
} }

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

@ -17,11 +17,23 @@
--> -->
<div [formGroup]="datasourcesFormGroup" class="tb-widget-config-panel tb-datasources" [ngClass]="{'no-padding-bottom': !showAddDatasource}"> <div [formGroup]="datasourcesFormGroup" class="tb-widget-config-panel tb-datasources" [ngClass]="{'no-padding-bottom': !showAddDatasource}">
<div fxLayout="column"> <div fxLayout="column">
<div fxLayout="row"> <div fxLayout="row" fxLayoutAlign="space-between center">
<div class="tb-widget-config-panel-title">{{ (singleDatasource ? 'widget-config.datasource' : 'widget-config.datasources') | translate }}</div> <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"> <div fxFlex fxLayout="row" fxLayoutAlign="center center" *ngIf="timeseriesKeyError">
<mat-error >{{ 'widget-config.timeseries-key-error' | translate }}</mat-error> <mat-error >{{ 'widget-config.timeseries-key-error' | translate }}</mat-error>
</div> </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>
<div *ngIf="maxDatasources > 1" <div *ngIf="maxDatasources > 1"
class="tb-widget-config-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}</div> class="tb-widget-config-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}</div>

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

@ -14,23 +14,33 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { import {
AbstractControl, AbstractControl,
ControlValueAccessor, FormControl, ControlValueAccessor,
FormControl,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, UntypedFormArray, NG_VALUE_ACCESSOR,
UntypedFormBuilder, UntypedFormControl, UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
Validator Validator
} from '@angular/forms'; } from '@angular/forms';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { Datasource, DatasourceType, JsonSettingsSchema, widgetType } from '@shared/models/widget.models'; import {
Datasource,
DatasourceType,
JsonSettingsSchema,
WidgetConfigMode,
widgetType
} from '@shared/models/widget.models';
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'tb-datasources', selector: 'tb-datasources',
@ -49,7 +59,13 @@ import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.m
} }
] ]
}) })
export class DatasourcesComponent implements ControlValueAccessor, OnInit, Validator { 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 { public get maxDatasources(): number {
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDatasources; return this.widgetConfigComponent.modelValue?.typeParameters?.maxDatasources;
@ -71,16 +87,22 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input()
configMode: WidgetConfigMode;
datasourcesFormGroup: UntypedFormGroup; datasourcesFormGroup: UntypedFormGroup;
timeseriesKeyError = false; timeseriesKeyError = false;
datasourceError: string[] = []; datasourceError: string[] = [];
datasourcesMode: DatasourceType;
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,
private utils: UtilsService, private utils: UtilsService,
public translate: TranslateService,
private widgetConfigComponent: WidgetConfigComponent) { private widgetConfigComponent: WidgetConfigComponent) {
} }
@ -116,16 +138,38 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
); );
} }
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 { writeValue(datasources?: Datasource[]): void {
this.datasourcesFormArray.clear({emitEvent: false}); this.datasourcesFormArray.clear({emitEvent: false});
this.datasourcesMode = this.detectDatasourcesMode(datasources);
let changed = false;
if (datasources) { if (datasources) {
datasources.forEach((datasource) => { 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}); this.datasourcesFormArray.push(this.fb.control(datasource, []), {emitEvent: false});
}); });
} }
if (this.singleDatasource && !this.datasourcesFormArray.length) { if (this.singleDatasource && !this.datasourcesFormArray.length) {
this.addDatasource(false); this.addDatasource(false);
} }
if (changed) {
setTimeout(() => {
this.datasourcesUpdated(this.datasourcesFormGroup.get('datasources').value);
}, 0);
}
} }
validate(c: UntypedFormControl) { validate(c: UntypedFormControl) {
@ -175,6 +219,37 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
return null; 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 { get datasourcesFormArray(): UntypedFormArray {
return this.datasourcesFormGroup.get('datasources') as UntypedFormArray; return this.datasourcesFormGroup.get('datasources') as UntypedFormArray;
} }
@ -207,7 +282,8 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema)); newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema));
newDatasource.dataKeys = [this.dataKeysCallbacks.generateDataKey('Sin', DataKeyType.function, this.dataKeySettingsSchema)]; newDatasource.dataKeys = [this.dataKeysCallbacks.generateDataKey('Sin', DataKeyType.function, this.dataKeySettingsSchema)];
} else { } else {
newDatasource = { type: DatasourceType.entity, const type = this.basicMode ? this.datasourcesMode : DatasourceType.entity;
newDatasource = { type,
dataKeys: [] dataKeys: []
}; };
} }

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

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

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

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

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

@ -0,0 +1,61 @@
///
/// 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 { 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 { DatasourceComponent } from '@home/components/widget/datasource.component';
import { DatasourcesComponent } from '@home/components/widget/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/widget-settings.component';
@NgModule({
declarations:
[
AlarmFilterConfigComponent,
DataKeysComponent,
DataKeyConfigDialogComponent,
DataKeyConfigComponent,
DatasourceComponent,
DatasourcesComponent,
EntityAliasSelectComponent,
FilterSelectComponent,
WidgetSettingsComponent
],
imports: [
CommonModule,
SharedModule,
WidgetSettingsModule
],
exports: [
AlarmFilterConfigComponent,
DataKeysComponent,
DataKeyConfigDialogComponent,
DataKeyConfigComponent,
DatasourceComponent,
DatasourcesComponent,
EntityAliasSelectComponent,
FilterSelectComponent,
WidgetSettingsComponent
]
})
export class WidgetConfigComponentsModule { }

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

@ -147,7 +147,8 @@
<tb-manage-widget-actions <tb-manage-widget-actions
[callbacks]="widgetConfigCallbacks" [callbacks]="widgetConfigCallbacks"
[widgetType] = "modelValue.widgetType" [widgetType] = "modelValue.widgetType"
formControlName="actionsData"> [actionSources]="modelValue.actionSources"
formControlName="actions">
</tb-manage-widget-actions> </tb-manage-widget-actions>
</div> </div>
</div> </div>
@ -180,29 +181,21 @@
</div> </div>
<ng-template #basicMode> <ng-template #basicMode>
<div class="tb-widget-config-content"> <div class="tb-widget-config-content">
<div class="mat-content"> <div class="mat-content" *ngIf="hasBasicModeDirective; else basicModeContent">
<ng-container *ngTemplateOutlet="data"></ng-container> <ng-container #basicModeContainer></ng-container>
<ng-container *ngTemplateOutlet="appearance"></ng-container> <div class="tb-basic-mode-directive-error" *ngIf="basicModeDirectiveError">{{basicModeDirectiveError}}</div>
<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>
</div> </div>
<ng-template #basicModeContent>
<div class="mat-content">
<ng-container *ngTemplateOutlet="data"></ng-container>
<ng-container *ngTemplateOutlet="appearance"></ng-container>
<ng-container [formGroup]="actionsSettings">
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>
</div>
</ng-template>
</div> </div>
</ng-template> </ng-template>
<ng-template #data> <ng-template #data>
@ -239,6 +232,7 @@
modelValue?.isDataEnabled" modelValue?.isDataEnabled"
[formGroup]="dataSettings"> [formGroup]="dataSettings">
<tb-datasources <tb-datasources
[configMode]="widgetConfigMode"
formControlName="datasources"> formControlName="datasources">
</tb-datasources> </tb-datasources>
</div> </div>

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

@ -16,5 +16,100 @@
import { WidgetActionCallbacks } from './action/manage-widget-actions.component.models'; import { WidgetActionCallbacks } from './action/manage-widget-actions.component.models';
import { DatasourceCallbacks } from '@home/components/widget/datasource.component.models'; import { DatasourceCallbacks } from '@home/components/widget/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 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);
}

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

@ -41,6 +41,11 @@
gap: 16px; gap: 16px;
} }
} }
.tb-basic-mode-directive-error {
font-size: 13px;
font-weight: 400;
color: rgb(221, 44, 0);
}
} }
:host ::ng-deep { :host ::ng-deep {

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

@ -14,7 +14,20 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectorRef, Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectorRef,
Component,
ComponentFactoryResolver,
ComponentRef,
forwardRef,
Input,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -26,7 +39,6 @@ import {
JsonSchema, JsonSchema,
JsonSettingsSchema, JsonSettingsSchema,
Widget, Widget,
WidgetActionDescriptor,
WidgetConfigMode, WidgetConfigMode,
widgetType widgetType
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
@ -50,7 +62,10 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { Observable, of, Subscription } from 'rxjs'; import { Observable, of, Subscription } from 'rxjs';
import { WidgetConfigCallbacks } from '@home/components/widget/widget-config.component.models'; import {
IBasicWidgetConfigComponent,
WidgetConfigCallbacks
} from '@home/components/widget/widget-config.component.models';
import { import {
EntityAliasDialogComponent, EntityAliasDialogComponent,
EntityAliasDialogData EntityAliasDialogData
@ -59,17 +74,14 @@ import { catchError, mergeMap, tap } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
import { WidgetActionsData } from './action/manage-widget-actions.component.models';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { entityFields } from '@shared/models/entity.models'; import { entityFields } from '@shared/models/entity.models';
import { Filter, singleEntityFilterFromDeviceId } from '@shared/models/query/query.models'; import { Filter, singleEntityFilterFromDeviceId } from '@shared/models/query/query.models';
import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component';
import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { import { basicWidgetConfigComponentsMap } from '@home/components/widget/basic-config/basic-widget-config.module';
ManageWidgetActionsDialogComponent, import Timeout = NodeJS.Timeout;
ManageWidgetActionsDialogData
} from '@home/components/widget/action/manage-widget-actions-dialog.component';
const emptySettingsSchema: JsonSchema = { const emptySettingsSchema: JsonSchema = {
type: 'object', type: 'object',
@ -97,7 +109,9 @@ const defaultSettingsForm = [
} }
] ]
}) })
export class WidgetConfigComponent extends PageComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { export class WidgetConfigComponent extends PageComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator, OnChanges {
@ViewChild('basicModeContainer', {read: ViewContainerRef, static: false}) basicModeContainer: ViewContainerRef;
widgetTypes = widgetType; widgetTypes = widgetType;
@ -146,6 +160,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
widgetEditMode = this.utils.widgetEditMode; widgetEditMode = this.utils.widgetEditMode;
basicModeDirectiveError: string;
modelValue: WidgetConfigComponentData; modelValue: WidgetConfigComponentData;
private propagateChange = null; private propagateChange = null;
@ -160,6 +176,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
public advancedSettings: UntypedFormGroup; public advancedSettings: UntypedFormGroup;
public actionsSettings: UntypedFormGroup; public actionsSettings: UntypedFormGroup;
private createBasicModeComponentTimeout: Timeout;
private basicModeComponentRef: ComponentRef<IBasicWidgetConfigComponent>;
private basicModeComponent: IBasicWidgetConfigComponent;
private basicModeComponentChangeSubscription: Subscription;
private dataSettingsChangesSubscription: Subscription; private dataSettingsChangesSubscription: Subscription;
private targetDeviceSettingsSubscription: Subscription; private targetDeviceSettingsSubscription: Subscription;
private widgetSettingsSubscription: Subscription; private widgetSettingsSubscription: Subscription;
@ -172,6 +193,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
private entityService: EntityService, private entityService: EntityService,
private dialog: MatDialog, private dialog: MatDialog,
public translate: TranslateService, public translate: TranslateService,
private cfr: ComponentFactoryResolver,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef) {
super(store); super(store);
@ -218,11 +240,25 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
desktopHide: [false] desktopHide: [false]
}); });
this.actionsSettings = this.fb.group({ this.actionsSettings = this.fb.group({
actionsData: [null, []] actions: [null, []]
}); });
} }
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'widgetConfigMode') {
if (this.hasBasicModeDirective) {
this.setupConfig();
}
}
}
}
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroyBasicModeComponent();
this.removeChangeSubscriptions(); this.removeChangeSubscriptions();
} }
@ -360,7 +396,61 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
writeValue(value: WidgetConfigComponentData): void { writeValue(value: WidgetConfigComponentData): void {
this.modelValue = value; this.modelValue = value;
this.setupConfig();
}
private setupConfig() {
this.destroyBasicModeComponent();
this.removeChangeSubscriptions(); this.removeChangeSubscriptions();
if (this.hasBasicModeDirective && this.widgetConfigMode === WidgetConfigMode.basic) {
this.setupBasicModeConfig();
} else {
this.setupDefaultConfig();
}
}
private setupBasicModeConfig() {
const componentType = basicWidgetConfigComponentsMap[this.modelValue.basicModeDirective];
if (!componentType) {
this.basicModeDirectiveError = this.translate.instant('widget-config.settings-component-not-found',
{selector: this.modelValue.basicModeDirective});
} else {
const factory = this.cfr.resolveComponentFactory(componentType);
this.createBasicModeComponentTimeout = setTimeout(() => {
this.createBasicModeComponentTimeout = null;
this.basicModeComponentRef = this.basicModeContainer.createComponent(factory);
this.basicModeComponent = this.basicModeComponentRef.instance;
this.basicModeComponent.widgetConfig = this.modelValue;
this.basicModeComponentChangeSubscription = this.basicModeComponent.widgetConfigChanged.subscribe((data) => {
this.modelValue = data;
this.propagateChange(this.modelValue);
});
this.cd.markForCheck();
}, 0);
}
}
private destroyBasicModeComponent() {
this.basicModeDirectiveError = null;
if (this.basicModeComponentChangeSubscription) {
this.basicModeComponentChangeSubscription.unsubscribe();
this.basicModeComponentChangeSubscription = null;
}
if (this.createBasicModeComponentTimeout) {
clearTimeout(this.createBasicModeComponentTimeout);
this.createBasicModeComponentTimeout = null;
}
if (this.basicModeComponentRef) {
this.basicModeComponentRef.destroy();
this.basicModeComponentRef = null;
this.basicModeComponent = null;
}
if (this.basicModeContainer) {
this.basicModeContainer.clear();
}
}
private setupDefaultConfig() {
if (this.modelValue) { if (this.modelValue) {
if (this.widgetType !== this.modelValue.widgetType) { if (this.widgetType !== this.modelValue.widgetType) {
this.widgetType = this.modelValue.widgetType; this.widgetType = this.modelValue.widgetType;
@ -399,13 +489,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
{emitEvent: false} {emitEvent: false}
); );
this.updateWidgetSettingsEnabledState(); this.updateWidgetSettingsEnabledState();
const actionsData: WidgetActionsData = {
actionsMap: config.actions || {},
actionSources: this.modelValue.actionSources || {}
};
this.actionsSettings.patchValue( this.actionsSettings.patchValue(
{ {
actionsData actions: config.actions || {}
}, },
{emitEvent: false} {emitEvent: false}
); );
@ -449,9 +535,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
} else if (this.widgetType === widgetType.alarm) { } else if (this.widgetType === widgetType.alarm) {
this.dataSettings.patchValue( this.dataSettings.patchValue(
{ alarmFilterConfig: isDefined(config.alarmFilterConfig) ? { alarmFilterConfig: isDefined(config.alarmFilterConfig) ?
config.alarmFilterConfig : config.alarmFilterConfig :
{ statusList: [AlarmSearchStatus.ACTIVE], searchPropagatedAlarms: true }, { statusList: [AlarmSearchStatus.ACTIVE], searchPropagatedAlarms: true },
alarmSource: config.alarmSource }, {emitEvent: false} alarmSource: config.alarmSource }, {emitEvent: false}
); );
} }
} }
@ -590,12 +676,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
private updateActionSettings() { private updateActionSettings() {
if (this.modelValue) { if (this.modelValue) {
if (this.modelValue.config) { if (this.modelValue.config) {
this.modelValue.config.actions = (this.actionsSettings.get('actionsData').value as WidgetActionsData).actionsMap; this.modelValue.config.actions = this.actionsSettings.get('actions').value;
} }
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
} }
} }
public get hasBasicModeDirective(): boolean {
return this.modelValue?.basicModeDirective?.length > 0;
}
public get useDefinedBasicModeDirective(): boolean {
return this.modelValue?.basicModeDirective?.length && !this.basicModeDirectiveError;
}
public get displayAppearance(): boolean { public get displayAppearance(): boolean {
return this.displayAppearanceDataSettings || this.displayAdvancedAppearance; return this.displayAppearanceDataSettings || this.displayAdvancedAppearance;
} }
@ -631,28 +725,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
return this.widgetType !== widgetType.static && !this.modelValue?.typeParameters?.processNoDataByWidget; return this.widgetType !== widgetType.static && !this.modelValue?.typeParameters?.processNoDataByWidget;
} }
public get widgetActionSourceIds(): Array<string> {
const actionsData: WidgetActionsData = this.actionsSettings.get('actionsData').value;
return actionsData?.actionsMap ? Object.keys(actionsData.actionsMap) : [];
}
public widgetActionsByActionSourceId(actionSourceId: string): Array<WidgetActionDescriptor> {
const actionsData: WidgetActionsData = this.actionsSettings.get('actionsData').value;
return actionsData?.actionsMap[actionSourceId] || [];
}
public get hasWidgetActions(): boolean {
const actionsData: WidgetActionsData = this.actionsSettings.get('actionsData').value;
if (actionsData?.actionsMap) {
for (const actionSourceId of Object.keys(actionsData.actionsMap)) {
if (actionsData.actionsMap[actionSourceId] && actionsData.actionsMap[actionSourceId].length) {
return true;
}
}
}
return false;
}
public onlyHistoryTimewindow(): boolean { public onlyHistoryTimewindow(): boolean {
if (this.widgetType === widgetType.latest) { if (this.widgetType === widgetType.latest) {
const datasources = this.dataSettings.get('datasources').value; const datasources = this.dataSettings.get('datasources').value;
@ -662,28 +734,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
} }
} }
public manageWidgetActions() {
const actionsData: WidgetActionsData = this.actionsSettings.get('actionsData').value;
this.dialog.open<ManageWidgetActionsDialogComponent, ManageWidgetActionsDialogData,
WidgetActionsData>(ManageWidgetActionsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
widgetTitle: this.modelValue.widgetName,
callbacks: this.widgetConfigCallbacks,
actionsData: deepClone(actionsData),
widgetType: this.widgetType
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.actionsSettings.get('actionsData').patchValue(res);
this.cd.markForCheck();
}
}
);
}
public generateDataKey(chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema): DataKey { public generateDataKey(chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema): DataKey {
if (isObject(chip)) { if (isObject(chip)) {
(chip as DataKey)._hash = Math.random(); (chip as DataKey)._hash = Math.random();
@ -819,7 +869,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
} }
public validate(c: UntypedFormControl) { public validate(c: UntypedFormControl) {
if (!this.dataSettings.valid) { if (this.basicModeComponent &&
!this.basicModeComponent.validateConfig()) {
return {
basicWidgetConfig: {
valid: false
}
};
} else if (!this.dataSettings.valid) {
return { return {
dataSettings: { dataSettings: {
valid: false valid: false

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

@ -460,6 +460,7 @@ export interface WidgetConfigComponentData {
settingsDirective: string; settingsDirective: string;
dataKeySettingsDirective: string; dataKeySettingsDirective: string;
latestDataKeySettingsDirective: string; latestDataKeySettingsDirective: string;
basicModeDirective: string;
} }
export const MissingWidgetType: WidgetInfo = { export const MissingWidgetType: WidgetInfo = {

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

@ -5119,6 +5119,7 @@
"markdown-css": "Markdown/HTML CSS" "markdown-css": "Markdown/HTML CSS"
}, },
"simple-card": { "simple-card": {
"label": "Label",
"label-position": "Label position", "label-position": "Label position",
"label-position-left": "Left", "label-position-left": "Left",
"label-position-top": "Top" "label-position-top": "Top"

Loading…
Cancel
Save