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": "",
"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\"}"
}
},
{

61
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);

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,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
basicModeDirective: widgetInfo.basicModeDirective
};
this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false;
this.widgetFormGroup = this.fb.group({

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.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe(
(widgetTypeInfo) => {
const config: WidgetConfig = JSON.parse(widgetTypeInfo.defaultConfig);
const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo);
config.title = 'New ' + widgetTypeInfo.widgetName;
config.datasources = [];
if (isDefinedAndNotNull(config.alarmSource)) {
config.alarmSource = {
type: DatasourceType.entity,
dataKeys: config.alarmSource.dataKeys || []
};
}
let newWidget: Widget = {
isSystemType: widget.isSystemType,
bundleAlias: widget.bundleAlias,

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,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
basicModeDirective: widgetInfo.basicModeDirective
};
this.hasBasicMode = isDefinedAndNotNull(widgetInfo.hasBasicMode) ? widgetInfo.hasBasicMode : false;
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 { 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,

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

@ -32,7 +32,8 @@
<tb-manage-widget-actions
[callbacks]="data.callbacks"
[widgetType] = "data.widgetType"
formControlName="actionsData">
[actionSources]="actionSources"
formControlName="actions">
</tb-manage-widget-actions>
</div>
<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,
WidgetActionsData> implements OnInit {
actionSources = this.data.actionsData.actionSources;
actionsSettings: UntypedFormGroup;
constructor(protected store: Store<AppState>,
@ -55,7 +56,7 @@ export class ManageWidgetActionsDialogComponent extends DialogComponent<ManageWi
ngOnInit() {
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 {
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">
<ng-container matColumnDef="actionSourceId">
<mat-header-cell *matHeaderCellDef style="width: 36px"></mat-header-cell>
<mat-cell *matCellDef="let action">
<mat-cell *matCellDef="let action" class="tb-icon-cell">
<div cdkDragHandle
class="tb-drag-handle"
style="min-width: 36px"
@ -95,7 +95,7 @@
</ng-container>
<ng-container matColumnDef="icon">
<mat-header-cell *matHeaderCellDef style="width: 40px"> {{ 'widget-config.action-icon' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let action" style="width: 40px">
<mat-cell *matCellDef="let action" class="tb-icon-cell" style="width: 40px">
<mat-icon>{{ action.icon }}</mat-icon>
</mat-cell>
</ng-container>

6
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;

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() 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<WidgetActionDescriptor>}): void {
this.innerValue = {
actionsMap: actions || {},
actionSources: this.actionSources || {}
};
setTimeout(() => {
if (!this.destroyed) {
this.dataSource.setActions(this.innerValue);
if (this.viewsInited) {
this.resetSortAndFilter(true);
} else {
this.dirtyValue = true;
}
}, 0);
}
}
}, 0);
}
private onActionsUpdated() {
this.updateData(true);
this.propagateChange(this.innerValue);
this.propagateChange(this.innerValue.actionsMap);
}
}

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

6
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;
}

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 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 fxFlex fxLayout="row" fxLayoutAlign="center center" *ngIf="timeseriesKeyError">
<mat-error >{{ 'widget-config.timeseries-key-error' | translate }}</mat-error>
</div>
<tb-toggle-header *ngIf="basicMode" (valueChange)="datasourcesModeChange($event)" ignoreMdLgSize="true"
[options]="[
{
name: translate.instant('device.device'),
value: datasourceType.device
},
{
name: translate.instant('entity.entity-alias'),
value: datasourceType.entity
}
]" [value]="datasourcesMode" name="widgetConfigModeHeader" useSelectOnMdLg="false">
</tb-toggle-header>
</div>
<div *ngIf="maxDatasources > 1"
class="tb-widget-config-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: maxDatasources} }}</div>

90
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: []
};
}

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

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

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

@ -23,7 +23,7 @@ import { AppState } from '@core/core.state';
@Component({
selector: 'tb-simple-card-widget-settings',
templateUrl: './simple-card-widget-settings.component.html',
styleUrls: []
styleUrls: ['../../../widget-config.scss']
})
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
[callbacks]="widgetConfigCallbacks"
[widgetType] = "modelValue.widgetType"
formControlName="actionsData">
[actionSources]="modelValue.actionSources"
formControlName="actions">
</tb-manage-widget-actions>
</div>
</div>
@ -180,29 +181,21 @@
</div>
<ng-template #basicMode>
<div class="tb-widget-config-content">
<div class="mat-content">
<ng-container *ngTemplateOutlet="data"></ng-container>
<ng-container *ngTemplateOutlet="appearance"></ng-container>
<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 class="mat-content" *ngIf="hasBasicModeDirective; else basicModeContent">
<ng-container #basicModeContainer></ng-container>
<div class="tb-basic-mode-directive-error" *ngIf="basicModeDirectiveError">{{basicModeDirectiveError}}</div>
</div>
<ng-template #basicModeContent>
<div class="mat-content">
<ng-container *ngTemplateOutlet="data"></ng-container>
<ng-container *ngTemplateOutlet="appearance"></ng-container>
<ng-container [formGroup]="actionsSettings">
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>
</div>
</ng-template>
</div>
</ng-template>
<ng-template #data>
@ -239,6 +232,7 @@
modelValue?.isDataEnabled"
[formGroup]="dataSettings">
<tb-datasources
[configMode]="widgetConfigMode"
formControlName="datasources">
</tb-datasources>
</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 { 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<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;
}
}
.tb-basic-mode-directive-error {
font-size: 13px;
font-weight: 400;
color: rgb(221, 44, 0);
}
}
:host ::ng-deep {

185
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<IBasicWidgetConfigComponent>;
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<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 {
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, 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 {
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

1
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 = {

1
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"

Loading…
Cancel
Save