From bf749ff19b92b94938014c3798c03e600990496a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 30 May 2023 12:37:47 +0300 Subject: [PATCH] UI: Basic widget config mode directive support --- .../json/system/widget_bundles/cards.json | 3 +- .../core/services/dashboard-utils.service.ts | 61 +++++- .../add-widget-dialog.component.ts | 3 +- .../dashboard-page.component.ts | 9 +- .../dashboard-page/edit-widget.component.ts | 1 + .../home/components/home-components.module.ts | 33 +--- ...anage-widget-actions-dialog.component.html | 3 +- .../manage-widget-actions-dialog.component.ts | 5 +- .../manage-widget-actions.component.html | 4 +- .../manage-widget-actions.component.scss | 6 + .../action/manage-widget-actions.component.ts | 31 +-- .../widget-actions-panel.component.html | 36 ++++ .../action/widget-actions-panel.component.ts | 139 +++++++++++++ .../widget/basic-config/basic-config.scss | 20 ++ .../basic-widget-config.module.ts | 49 +++++ .../simple-card-basic-config.component.html | 42 ++++ .../simple-card-basic-config.component.ts | 58 ++++++ .../widget/datasource.component.html | 18 +- .../components/widget/datasource.component.ts | 6 +- .../widget/datasources.component.html | 14 +- .../widget/datasources.component.ts | 90 ++++++++- ...simple-card-widget-settings.component.html | 29 +-- .../simple-card-widget-settings.component.ts | 2 +- .../widget/widget-config-components.module.ts | 61 ++++++ .../widget/widget-config.component.html | 40 ++-- .../widget/widget-config.component.models.ts | 95 +++++++++ .../widget/widget-config.component.scss | 5 + .../widget/widget-config.component.ts | 185 ++++++++++++------ .../home/models/widget-component.models.ts | 1 + .../assets/locale/locale.constant-en_US.json | 1 + 30 files changed, 869 insertions(+), 181 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/basic-config/action/widget-actions-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/basic-config/action/widget-actions-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/basic-config/basic-config.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/basic-config/basic-widget-config.module.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/basic-config/cards/simple-card-basic-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/basic-config/cards/simple-card-basic-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/widget-config-components.module.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 20b77bf1ab..1cf107b801 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -103,7 +103,8 @@ "dataKeySettingsSchema": "", "settingsDirective": "tb-simple-card-widget-settings", "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\"}" } }, { diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 0d49608a4b..243c5e369b 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -29,21 +29,25 @@ import { GridSettings, WidgetLayout } from '@shared/models/dashboard.models'; -import { isDefined, isDefinedAndNotNull, 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' @@ -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) { if (!state.layouts) { state.layouts = this.createDefaultLayouts(); @@ -445,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; @@ -518,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); diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index ee31ee2900..ada22bba94 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts @@ -128,7 +128,8 @@ export class AddWidgetDialogComponent extends DialogComponent { - 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, diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts index f910f79b01..2528ada4ae 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts +++ b/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, dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective, + basicModeDirective: widgetInfo.basicModeDirective }; this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false; this.widgetFormGroup.reset({widgetConfig: this.widgetConfig}); diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index a8ad2fd4c4..a2d18dfa34 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/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 { 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 { 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'; @@ -76,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'; @@ -150,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'; @@ -176,16 +169,15 @@ 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 { DatasourceComponent } from '@home/components/widget/datasource.component'; -import { DatasourcesComponent } from '@home/components/widget/datasources.component'; import { ManageWidgetActionsDialogComponent } 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({ declarations: @@ -212,7 +204,6 @@ import { AlarmAssigneePanelComponent, AlarmAssigneeSelectComponent, AlarmAssigneeSelectPanelComponent, - AlarmFilterConfigComponent, AttributeTableComponent, AddAttributeDialogComponent, EditAttributeValuePanelComponent, @@ -224,17 +215,10 @@ import { DashboardComponent, WidgetContainerComponent, WidgetComponent, - WidgetSettingsComponent, WidgetConfigComponent, WidgetPreviewComponent, EntityFilterViewComponent, EntityFilterComponent, - EntityAliasSelectComponent, - DataKeysComponent, - DatasourceComponent, - DatasourcesComponent, - DataKeyConfigComponent, - DataKeyConfigDialogComponent, ManageWidgetActionsComponent, WidgetActionDialogComponent, ManageWidgetActionsDialogComponent, @@ -259,7 +243,6 @@ import { KeyFilterDialogComponent, FilterDialogComponent, FiltersDialogComponent, - FilterSelectComponent, FilterTextComponent, FiltersEditComponent, FiltersEditPanelComponent, @@ -344,7 +327,8 @@ import { CommonModule, SharedModule, SharedHomeComponentsModule, - WidgetSettingsModule, + WidgetConfigComponentsModule, + BasicWidgetConfigModule, Lwm2mProfileComponentsModule, SnmpDeviceProfileTransportModule, StatesControllerModule, @@ -368,7 +352,6 @@ import { AlarmAssigneePanelComponent, AlarmAssigneeSelectComponent, AlarmAssigneeSelectPanelComponent, - AlarmFilterConfigComponent, AttributeTableComponent, AliasesEntitySelectComponent, AliasesEntityAutocompleteComponent, @@ -377,17 +360,10 @@ import { DashboardComponent, WidgetContainerComponent, WidgetComponent, - WidgetSettingsComponent, WidgetConfigComponent, WidgetPreviewComponent, EntityFilterViewComponent, EntityFilterComponent, - EntityAliasSelectComponent, - DataKeysComponent, - DatasourceComponent, - DatasourcesComponent, - DataKeyConfigComponent, - DataKeyConfigDialogComponent, ManageWidgetActionsComponent, WidgetActionDialogComponent, ManageWidgetActionsDialogComponent, @@ -411,7 +387,6 @@ import { KeyFilterDialogComponent, FilterDialogComponent, FiltersDialogComponent, - FilterSelectComponent, FilterTextComponent, FiltersEditComponent, UserFilterDialogComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html index 37db477dd0..36f58f27c6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html @@ -32,7 +32,8 @@ + [actionSources]="actionSources" + formControlName="actions">
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts index f458243c10..0133c375f2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts +++ b/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 implements OnInit { + actionSources = this.data.actionsData.actionSources; actionsSettings: UntypedFormGroup; constructor(protected store: Store, @@ -55,7 +56,7 @@ export class ManageWidgetActionsDialogComponent extends DialogComponent - +
{{ 'widget-config.action-icon' | translate }} - + {{ action.icon }} diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss index 2e2e1687fc..108e27ab13 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss @@ -43,6 +43,12 @@ opacity: 1 !important; } + .mat-mdc-cell.tb-icon-cell { + .mat-icon { + vertical-align: middle; + } + } + .tb-draggable { &.cdk-drag-placeholder { opacity: 0; diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index 925435885f..0db42badf2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/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[]; @@ -338,24 +340,25 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni this.disabled = isDisabled; } - writeValue(obj: WidgetActionsData): void { - this.innerValue = obj; - if (this.innerValue) { - setTimeout(() => { - if (!this.destroyed) { - this.dataSource.setActions(this.innerValue); - if (this.viewsInited) { - this.resetSortAndFilter(true); - } else { - this.dirtyValue = true; - } + writeValue(actions?: {[actionSourceId: string]: Array}): 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); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/basic-config/action/widget-actions-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/basic-config/action/widget-actions-panel.component.html new file mode 100644 index 0000000000..95e71efd5e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/basic-config/action/widget-actions-panel.component.html @@ -0,0 +1,36 @@ + +
+
+
widget-config.actions
+ + + + {{ widgetAction.icon }} + {{ widgetAction.name }} + + + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/basic-config/action/widget-actions-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/basic-config/action/widget-actions-panel.component.ts new file mode 100644 index 0000000000..02d9787aa7 --- /dev/null +++ b/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}): 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 { + const actions: {[actionSourceId: string]: Array} = this.actionsFormGroup.get('actions').value; + return actions ? Object.keys(actions) : []; + } + + public widgetActionsByActionSourceId(actionSourceId: string): Array { + const actions: {[actionSourceId: string]: Array} = this.actionsFormGroup.get('actions').value; + return actions[actionSourceId] || []; + } + + public get hasWidgetActions(): boolean { + const actions: {[actionSourceId: string]: Array} = 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} = this.actionsFormGroup.get('actions').value; + const actionsData: WidgetActionsData = { + actionsMap: deepClone(actions), + actionSources: this.widgetConfigComponent.modelValue.actionSources || {} + }; + this.dialog.open}>(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(); + } + } + ); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/basic-config/basic-config.scss b/ui-ngx/src/app/modules/home/components/widget/basic-config/basic-config.scss new file mode 100644 index 0000000000..d29594fce3 --- /dev/null +++ b/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; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/basic-config/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/basic-config/basic-widget-config.module.ts new file mode 100644 index 0000000000..5d36fa43b3 --- /dev/null +++ b/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} = { + 'tb-simple-card-basic-config': SimpleCardBasicConfigComponent +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/basic-config/cards/simple-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/basic-config/cards/simple-card-basic-config.component.html new file mode 100644 index 0000000000..a686add2fd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/basic-config/cards/simple-card-basic-config.component.html @@ -0,0 +1,42 @@ + + + + +
+
widget-config.appearance
+
+
widgets.simple-card.label-position
+ + + + {{ 'widgets.simple-card.label-position-left' | translate }} + + + {{ 'widgets.simple-card.label-position-top' | translate }} + + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/basic-config/cards/simple-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/basic-config/cards/simple-card-basic-config.component.ts new file mode 100644 index 0000000000..f8cdaae59c --- /dev/null +++ b/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, + 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; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/datasource.component.html index 40c63e6764..5cf519637b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/datasource.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/datasource.component.html @@ -16,7 +16,7 @@ -->
- + widget-config.datasource-type @@ -24,7 +24,7 @@ -
+
datasource.label @@ -53,13 +53,6 @@ formControlName="entityAliasId" [callbacks]="entityAliasSelectCallbacks"> - -
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/datasource.component.ts b/ui-ngx/src/app/modules/home/components/widget/datasource.component.ts index 3f521b1d91..79adc363a6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/datasource.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/datasource.component.ts @@ -30,7 +30,7 @@ import { DatasourceType, datasourceTypeTranslationMap, JsonSettingsSchema, - Widget, + Widget, WidgetConfigMode, widgetType } from '@shared/models/widget.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 { + public get basicMode(): boolean { + return !this.widgetConfigComponent.widgetEditMode && this.widgetConfigComponent.widgetConfigMode === WidgetConfigMode.basic; + } + public get widgetType(): widgetType { return this.widgetConfigComponent.widgetType; } diff --git a/ui-ngx/src/app/modules/home/components/widget/datasources.component.html b/ui-ngx/src/app/modules/home/components/widget/datasources.component.html index 98bbe96600..a5a8a89004 100644 --- a/ui-ngx/src/app/modules/home/components/widget/datasources.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/datasources.component.html @@ -17,11 +17,23 @@ -->
-
+
{{ (singleDatasource ? 'widget-config.datasource' : 'widget-config.datasources') | translate }}
{{ 'widget-config.timeseries-key-error' | translate }}
+ +
{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/datasources.component.ts b/ui-ngx/src/app/modules/home/components/widget/datasources.component.ts index 7c1196a0c4..f47adfb2a9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/datasources.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/datasources.component.ts @@ -14,23 +14,33 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { AbstractControl, - ControlValueAccessor, FormControl, + ControlValueAccessor, + FormControl, NG_VALIDATORS, - NG_VALUE_ACCESSOR, UntypedFormArray, - UntypedFormBuilder, UntypedFormControl, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, UntypedFormGroup, Validator } from '@angular/forms'; 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 { 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/data-keys.component.models'; +import { TranslateService } from '@ngx-translate/core'; @Component({ 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 { return this.widgetConfigComponent.modelValue?.typeParameters?.maxDatasources; @@ -71,16 +87,22 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid @Input() disabled: boolean; + @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) { } @@ -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 { 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) { @@ -175,6 +219,37 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid 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; } @@ -207,7 +282,8 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema)); newDatasource.dataKeys = [this.dataKeysCallbacks.generateDataKey('Sin', DataKeyType.function, this.dataKeySettingsSchema)]; } else { - newDatasource = { type: DatasourceType.entity, + const type = this.basicMode ? this.datasourcesMode : DatasourceType.entity; + newDatasource = { type, dataKeys: [] }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html index 8b60b19007..1ee182f923 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.html +++ b/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. --> -
- - widgets.simple-card.label-position - - - {{ 'widgets.simple-card.label-position-left' | translate }} - - - {{ 'widgets.simple-card.label-position-top' | translate }} - - - -
+
+
widgets.simple-card.label
+
+
widgets.simple-card.label-position
+ + + + {{ 'widgets.simple-card.label-position-left' | translate }} + + + {{ 'widgets.simple-card.label-position-top' | translate }} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts index 4ecd21ad1d..f22828d41d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts +++ b/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: ['../../../widget-config.scss'] }) export class SimpleCardWidgetSettingsComponent extends WidgetSettingsComponent { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config-components.module.ts new file mode 100644 index 0000000000..56302e212d --- /dev/null +++ b/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 { } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 872969bd15..cde8205f62 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -147,7 +147,8 @@ + [actionSources]="modelValue.actionSources" + formControlName="actions">
@@ -180,29 +181,21 @@
-
- - -
-
-
widget-config.actions
- - - - {{ widgetAction.icon }} - {{ widgetAction.name }} - - - - - -
-
+
+ +
{{basicModeDirectiveError}}
+ +
+ + + + + + +
+
@@ -239,6 +232,7 @@ modelValue?.isDataEnabled" [formGroup]="dataSettings">
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts index 7a141f6e75..3d9f60f1ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.models.ts +++ b/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 { 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 interface IBasicWidgetConfigComponent { + + widgetConfig: WidgetConfigComponentData; + widgetConfigChanged: Observable; + 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(); + widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable(); + + protected constructor(@Inject(Store) protected store: Store) { + 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); + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index 786456dacf..91b3368ad0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -41,6 +41,11 @@ gap: 16px; } } + .tb-basic-mode-directive-error { + font-size: 13px; + font-weight: 400; + color: rgb(221, 44, 0); + } } :host ::ng-deep { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 8eafe8855e..f7d1337ba3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -14,7 +14,20 @@ /// 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 { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -26,7 +39,6 @@ import { JsonSchema, JsonSettingsSchema, Widget, - WidgetActionDescriptor, WidgetConfigMode, widgetType } from '@shared/models/widget.models'; @@ -50,7 +62,10 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; 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 { EntityAliasDialogComponent, EntityAliasDialogData @@ -59,17 +74,14 @@ import { catchError, mergeMap, tap } from 'rxjs/operators'; import { MatDialog } from '@angular/material/dialog'; import { EntityService } from '@core/http/entity.service'; 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 { entityFields } from '@shared/models/entity.models'; import { Filter, singleEntityFilterFromDeviceId } from '@shared/models/query/query.models'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { coerceBoolean } from '@shared/decorators/coercion'; -import { - ManageWidgetActionsDialogComponent, - ManageWidgetActionsDialogData -} from '@home/components/widget/action/manage-widget-actions-dialog.component'; +import { basicWidgetConfigComponentsMap } from '@home/components/widget/basic-config/basic-widget-config.module'; +import Timeout = NodeJS.Timeout; const emptySettingsSchema: JsonSchema = { 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; @@ -146,6 +160,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe widgetEditMode = this.utils.widgetEditMode; + basicModeDirectiveError: string; + modelValue: WidgetConfigComponentData; private propagateChange = null; @@ -160,6 +176,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe public advancedSettings: UntypedFormGroup; public actionsSettings: UntypedFormGroup; + private createBasicModeComponentTimeout: Timeout; + private basicModeComponentRef: ComponentRef; + private basicModeComponent: IBasicWidgetConfigComponent; + private basicModeComponentChangeSubscription: Subscription; + private dataSettingsChangesSubscription: Subscription; private targetDeviceSettingsSubscription: Subscription; private widgetSettingsSubscription: Subscription; @@ -172,6 +193,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe private entityService: EntityService, private dialog: MatDialog, public translate: TranslateService, + private cfr: ComponentFactoryResolver, private fb: UntypedFormBuilder, private cd: ChangeDetectorRef) { super(store); @@ -218,11 +240,25 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe desktopHide: [false] }); 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 { + this.destroyBasicModeComponent(); this.removeChangeSubscriptions(); } @@ -360,7 +396,61 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe writeValue(value: WidgetConfigComponentData): void { this.modelValue = value; + this.setupConfig(); + } + + private setupConfig() { + this.destroyBasicModeComponent(); 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.widgetType !== this.modelValue.widgetType) { this.widgetType = this.modelValue.widgetType; @@ -399,13 +489,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe {emitEvent: false} ); this.updateWidgetSettingsEnabledState(); - const actionsData: WidgetActionsData = { - actionsMap: config.actions || {}, - actionSources: this.modelValue.actionSources || {} - }; this.actionsSettings.patchValue( { - actionsData + actions: config.actions || {} }, {emitEvent: false} ); @@ -449,9 +535,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } else if (this.widgetType === widgetType.alarm) { this.dataSettings.patchValue( { alarmFilterConfig: isDefined(config.alarmFilterConfig) ? - config.alarmFilterConfig : - { statusList: [AlarmSearchStatus.ACTIVE], searchPropagatedAlarms: true }, - alarmSource: config.alarmSource }, {emitEvent: false} + config.alarmFilterConfig : + { statusList: [AlarmSearchStatus.ACTIVE], searchPropagatedAlarms: true }, + alarmSource: config.alarmSource }, {emitEvent: false} ); } } @@ -590,12 +676,20 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe private updateActionSettings() { if (this.modelValue) { 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); } } + 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 { 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; } - public get widgetActionSourceIds(): Array { - const actionsData: WidgetActionsData = this.actionsSettings.get('actionsData').value; - return actionsData?.actionsMap ? Object.keys(actionsData.actionsMap) : []; - } - - public widgetActionsByActionSourceId(actionSourceId: string): Array { - 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 { if (this.widgetType === widgetType.latest) { 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, { - 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 { if (isObject(chip)) { (chip as DataKey)._hash = Math.random(); @@ -819,7 +869,14 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe } public validate(c: UntypedFormControl) { - if (!this.dataSettings.valid) { + if (this.basicModeComponent && + !this.basicModeComponent.validateConfig()) { + return { + basicWidgetConfig: { + valid: false + } + }; + } else if (!this.dataSettings.valid) { return { dataSettings: { valid: false diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 090d637c99..c00d0a8e82 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -460,6 +460,7 @@ export interface WidgetConfigComponentData { settingsDirective: string; dataKeySettingsDirective: string; latestDataKeySettingsDirective: string; + basicModeDirective: string; } export const MissingWidgetType: WidgetInfo = { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 81a4aa93c8..fb906561b0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5119,6 +5119,7 @@ "markdown-css": "Markdown/HTML CSS" }, "simple-card": { + "label": "Label", "label-position": "Label position", "label-position-left": "Left", "label-position-top": "Top"