Ability to define component form for advanced widget settings and widget data keys

This commit is contained in:
Igor Kulikov 2022-03-19 10:08:53 +02:00
parent d7efbd6eba
commit c2f4724ca2
21 changed files with 587 additions and 14 deletions

View File

@ -91,7 +91,9 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
actionSources,
isDataEnabled,
settingsSchema,
dataKeySettingsSchema
dataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective
};
this.widgetFormGroup = this.fb.group({

View File

@ -112,7 +112,9 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
actionSources,
isDataEnabled,
settingsSchema,
dataKeySettingsSchema
dataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective
};
this.widgetFormGroup.reset({widgetConfig: this.widgetConfig});
}

View File

@ -148,6 +148,8 @@ import {
} from '@home/components/tokens';
import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
import { WidgetSettingsComponent } from '@home/components/widget/widget-settings.component';
@NgModule({
declarations:
@ -182,6 +184,7 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
WidgetContainerComponent,
WidgetComponent,
LegendComponent,
WidgetSettingsComponent,
WidgetConfigComponent,
EntityFilterViewComponent,
EntityFilterComponent,
@ -273,6 +276,7 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
CommonModule,
SharedModule,
SharedHomeComponentsModule,
WidgetSettingsModule,
Lwm2mProfileComponentsModule,
SnmpDeviceProfileTransportModule,
StatesControllerModule,
@ -301,6 +305,7 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
WidgetContainerComponent,
WidgetComponent,
LegendComponent,
WidgetSettingsComponent,
WidgetConfigComponent,
EntityFilterViewComponent,
EntityFilterComponent,

View File

@ -31,6 +31,7 @@
<div mat-dialog-content>
<tb-data-key-config #dataKeyConfig
[dataKeySettingsSchema]="data.dataKeySettingsSchema"
[dataKeySettingsDirective]="data.dataKeySettingsDirective"
[entityAliasId]="data.entityAliasId"
[showPostProcessing]="data.showPostProcessing"
[callbacks]="data.callbacks"

View File

@ -29,6 +29,7 @@ import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.
export interface DataKeyConfigDialogData {
dataKey: DataKey;
dataKeySettingsSchema: any;
dataKeySettingsDirective: string;
entityAliasId?: string;
showPostProcessing?: boolean;
callbacks?: DataKeysCallbacks;

View File

@ -98,9 +98,10 @@
</mat-tab>
<mat-tab [formGroup]="dataKeySettingsFormGroup" label="{{ 'datakey.advanced' | translate }}" *ngIf="displayAdvanced">
<div class="mat-padding" fxLayout="column">
<tb-json-form
<tb-widget-settings
[settingsDirective]="dataKeySettingsDirective"
formControlName="settings">
</tb-json-form>
</tb-widget-settings>
</div>
</mat-tab>
</mat-tab-group>

View File

@ -72,6 +72,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@Input()
dataKeySettingsSchema: any;
@Input()
dataKeySettingsDirective: string;
@Input()
showPostProcessing = true;
@ -121,11 +124,15 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
type: DataKeyType.alarm
});
}
if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) {
if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema ||
this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) {
this.displayAdvanced = true;
this.dataKeySettingsData = {
schema: this.dataKeySettingsSchema.schema,
form: this.dataKeySettingsSchema.form || ['*']
schema: this.dataKeySettingsSchema?.schema || {
type: 'object',
properties: {}
},
form: this.dataKeySettingsSchema?.form || ['*']
};
this.dataKeySettingsFormGroup = this.fb.group({
settings: [null, []]

View File

@ -113,6 +113,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
@Input()
datakeySettingsSchema: any;
@Input()
dataKeySettingsDirective: string;
@Input()
callbacks: DataKeysCallbacks;
@ -395,6 +398,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
data: {
dataKey: deepClone(key),
dataKeySettingsSchema: this.datakeySettingsSchema,
dataKeySettingsDirective: this.dataKeySettingsDirective,
entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks

View File

@ -0,0 +1,36 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section [formGroup]="qrCodeWidgetSettingsForm" fxLayout="column">
<mat-checkbox formControlName="useQrCodeTextFunction" style="padding-bottom: 16px;">
{{ 'widgets.qr-code.use-qr-code-text-function' | translate }}
</mat-checkbox>
<mat-form-field [fxShow]="qrCodeWidgetSettingsForm.get('useQrCodeTextFunction').value === false">
<mat-label translate>widgets.qr-code.qr-code-text-pattern</mat-label>
<input required matInput formControlName="qrCodeTextPattern">
<mat-error *ngIf="qrCodeWidgetSettingsForm.get('qrCodeTextPattern').hasError('required')">
{{ 'widgets.qr-code.qr-code-text-pattern-required' | translate }}
</mat-error>
</mat-form-field>
<section fxLayout="column" [fxShow]="qrCodeWidgetSettingsForm.get('useQrCodeTextFunction').value === true">
<label translate class="tb-title no-padding">widgets.qr-code.qr-code-text-function</label>
<tb-js-func formControlName="qrCodeTextFunction"
[functionArgs]="['data']"
helpId="widget/lib/qrcode/qrcode_text_fn">
</tb-js-func>
</section>
</section>

View File

@ -0,0 +1,67 @@
///
/// Copyright © 2016-2022 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 { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@Component({
selector: 'tb-qrcode-widget-settings',
templateUrl: './qrcode-widget-settings.component.html',
styleUrls: []
})
export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent {
qrCodeWidgetSettingsForm: FormGroup;
constructor(protected store: Store<AppState>,
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): FormGroup {
return this.qrCodeWidgetSettingsForm;
}
protected onSettingsSet(settings: WidgetSettings) {
this.qrCodeWidgetSettingsForm = this.fb.group({
qrCodeTextPattern: [settings ? settings.qrCodeTextPattern : '${entityName}', []],
useQrCodeTextFunction: [settings ? settings.useQrCodeTextFunction : false, []],
qrCodeTextFunction: [settings ? settings.qrCodeTextFunction : 'return data[0][\'entityName\'];', []]
});
}
protected validatorTriggers(): string[] {
return ['useQrCodeTextFunction'];
}
protected updateValidators(emitEvent: boolean) {
const useQrCodeTextFunction: boolean = this.qrCodeWidgetSettingsForm.get('useQrCodeTextFunction').value;
if (useQrCodeTextFunction) {
this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').setValidators([]);
this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').setValidators([Validators.required]);
} else {
this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').setValidators([Validators.required]);
this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').setValidators([]);
}
this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').updateValueAndValidity({emitEvent});
this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').updateValueAndValidity({emitEvent});
}
}

View File

@ -0,0 +1,42 @@
///
/// Copyright © 2016-2022 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 { QrCodeWidgetSettingsComponent } from '@home/components/widget/lib/settings/qrcode-widget-settings.component';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
import { IWidgetSettingsComponent } from '@shared/models/widget.models';
@NgModule({
declarations: [
QrCodeWidgetSettingsComponent
],
imports: [
CommonModule,
SharedModule,
SharedHomeComponentsModule
],
exports: [
QrCodeWidgetSettingsComponent
]
})
export class WidgetSettingsModule {
}
export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsComponent>} = {
'tb-qrcode-widget-settings': QrCodeWidgetSettingsComponent
};

View File

@ -107,6 +107,8 @@ export class WidgetComponentService {
controllerScript: this.utils.editWidgetInfo.controllerScript,
settingsSchema: this.utils.editWidgetInfo.settingsSchema,
dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema,
settingsDirective: this.utils.editWidgetInfo.settingsDirective,
dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective,
defaultConfig: this.utils.editWidgetInfo.defaultConfig
}, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined
);

View File

@ -190,6 +190,7 @@
[optDataKeys]="modelValue?.typeParameters?.dataKeysOptional"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective"
[callbacks]="widgetConfigCallbacks"
[entityAliasId]="datasourceControl.get('entityAliasId').value"
[formControl]="datasourceControl.get('dataKeys')">
@ -283,6 +284,7 @@
[datasourceType]="alarmSourceSettings.get('type').value"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective"
[callbacks]="widgetConfigCallbacks"
[entityAliasId]="alarmSourceSettings.get('entityAliasId').value"
formControlName="dataKeys">
@ -476,9 +478,10 @@
<mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}">
<div [formGroup]="advancedSettings" class="mat-content mat-padding tb-advanced-widget-config"
fxLayout="column">
<tb-json-form
<tb-widget-settings
[settingsDirective]="modelValue.settingsDirective"
formControlName="settings">
</tb-json-form>
</tb-widget-settings>
</div>
</mat-tab>
<mat-tab label="{{ 'widget-config.actions' | translate }}" [formGroup]="actionsSettings">

View File

@ -605,7 +605,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
widgetSettingsFormData.schema = deepClone(emptySettingsSchema);
widgetSettingsFormData.form = deepClone(defaultSettingsForm);
widgetSettingsFormData.groupInfoes = deepClone(emptySettingsGroupInfoes);
widgetSettingsFormData.model = {};
widgetSettingsFormData.model = settings || {};
}
this.advancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false});
}
@ -713,7 +713,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
public displayAdvanced(): boolean {
return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema;
return !!this.modelValue && (!!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema ||
!!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length);
}
public dndDatasourceMoved(index: number) {
@ -913,7 +914,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
valid: false
}
};
} else if (!this.advancedSettings.valid) {
} else if (!this.advancedSettings.valid || (this.displayAdvanced() && !this.modelValue.config.settings)) {
return {
advancedSettings: {
valid: false

View File

@ -0,0 +1,24 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="widgetSettingsFormGroup">
<ng-container #definedSettingsContent></ng-container>
<div class="tb-settings-directive-error" *ngIf="definedDirectiveError">{{definedDirectiveError}}</div>
<tb-json-form *ngIf="useJsonForm()" #jsonFormComponent
formControlName="settings">
</tb-json-form>
</div>

View File

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2022 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 {
.tb-settings-directive-error {
font-size: 13px;
font-weight: 400;
color: rgb(221, 44, 0);
}
}

View File

@ -0,0 +1,201 @@
///
/// Copyright © 2016-2022 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 {
AfterViewInit,
Component, ComponentFactoryResolver,
ComponentRef,
forwardRef,
Input,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import {
IRuleNodeConfigurationComponent,
RuleNodeConfiguration,
RuleNodeDefinition
} from '@shared/models/rule-node.models';
import { Subscription } from 'rxjs';
import { RuleChainService } from '@core/http/rule-chain.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TranslateService } from '@ngx-translate/core';
import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils';
import { RuleChainType } from '@shared/models/rule-chain.models';
import { JsonFormComponent } from '@shared/components/json-form/json-form.component';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
import { IWidgetSettingsComponent, WidgetSettings } from '@shared/models/widget.models';
import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module';
@Component({
selector: 'tb-widget-settings',
templateUrl: './widget-settings.component.html',
styleUrls: ['./widget-settings.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetSettingsComponent),
multi: true
}]
})
export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, OnDestroy, AfterViewInit {
@ViewChild('definedSettingsContent', {read: ViewContainerRef, static: true}) definedSettingsContainer: ViewContainerRef;
@ViewChild('jsonFormComponent') jsonFormComponent: JsonFormComponent;
@Input()
disabled: boolean;
settingsDirectiveValue: string;
@Input()
set settingsDirective(settingsDirective: string) {
if (this.settingsDirectiveValue !== settingsDirective) {
this.settingsDirectiveValue = settingsDirective;
if (this.settingsDirectiveValue) {
this.validateDefinedDirective();
}
}
}
get settingsDirective(): string {
return this.settingsDirectiveValue;
}
definedDirectiveError: string;
widgetSettingsFormGroup: FormGroup;
changeSubscription: Subscription;
private definedSettingsComponentRef: ComponentRef<IWidgetSettingsComponent>;
private definedSettingsComponent: IWidgetSettingsComponent;
private widgetSettingsFormData: JsonFormComponentData;
private propagateChange = (v: any) => { };
constructor(private translate: TranslateService,
private cfr: ComponentFactoryResolver,
private fb: FormBuilder) {
this.widgetSettingsFormGroup = this.fb.group({
settings: [null, Validators.required]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit(): void {
}
ngOnDestroy(): void {
if (this.definedSettingsComponentRef) {
this.definedSettingsComponentRef.destroy();
}
}
ngAfterViewInit(): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.widgetSettingsFormGroup.disable({emitEvent: false});
} else {
this.widgetSettingsFormGroup.enable({emitEvent: false});
}
}
writeValue(value: JsonFormComponentData): void {
this.widgetSettingsFormData = value;
if (this.changeSubscription) {
this.changeSubscription.unsubscribe();
this.changeSubscription = null;
}
if (this.definedSettingsComponent) {
this.definedSettingsComponent.settings = this.widgetSettingsFormData.model;
this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => {
this.updateModel(settings);
});
} else {
this.widgetSettingsFormGroup.get('settings').patchValue(this.widgetSettingsFormData, {emitEvent: false});
this.changeSubscription = this.widgetSettingsFormGroup.get('settings').valueChanges.subscribe(
(widgetSettingsFormData: JsonFormComponentData) => {
this.updateModel(widgetSettingsFormData.model);
}
);
}
}
useDefinedDirective(): boolean {
return this.settingsDirective &&
this.settingsDirective.length && !this.definedDirectiveError;
}
useJsonForm(): boolean {
return !this.settingsDirective || !this.settingsDirective.length;
}
private updateModel(settings: WidgetSettings) {
this.widgetSettingsFormData.model = settings;
if (this.definedSettingsComponent || this.widgetSettingsFormGroup.valid) {
this.propagateChange(this.widgetSettingsFormData);
} else {
this.propagateChange(null);
}
}
private validateDefinedDirective() {
if (this.definedSettingsComponentRef) {
this.definedSettingsComponentRef.destroy();
this.definedSettingsComponentRef = null;
}
if (this.settingsDirective && this.settingsDirective.length) {
const componentType = widgetSettingsComponentsMap[this.settingsDirective];
if (!componentType) {
this.definedDirectiveError = this.translate.instant('widget-config.settings-component-not-found',
{selector: this.settingsDirective});
} else {
if (this.changeSubscription) {
this.changeSubscription.unsubscribe();
this.changeSubscription = null;
}
this.definedSettingsContainer.clear();
const factory = this.cfr.resolveComponentFactory(componentType);
this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory);
this.definedSettingsComponent = this.definedSettingsComponentRef.instance;
this.definedSettingsComponent.settings = this.widgetSettingsFormData?.model;
this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => {
this.updateModel(settings);
});
}
}
}
validate() {
if (this.useDefinedDirective()) {
this.definedSettingsComponent.validate();
}
}
}

View File

@ -424,6 +424,8 @@ export interface WidgetConfigComponentData {
isDataEnabled: boolean;
settingsSchema: JsonSettingsSchema;
dataKeySettingsSchema: JsonSettingsSchema;
settingsDirective: string;
dataKeySettingsDirective: string;
}
export const MissingWidgetType: WidgetInfo = {
@ -510,6 +512,8 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
controllerScript: widgetTypeEntity.descriptor.controllerScript,
settingsSchema: widgetTypeEntity.descriptor.settingsSchema,
dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema,
settingsDirective: widgetTypeEntity.descriptor.settingsDirective,
dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective,
defaultConfig: widgetTypeEntity.descriptor.defaultConfig
};
}
@ -536,6 +540,8 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
controllerScript: widgetInfo.controllerScript,
settingsSchema: widgetInfo.settingsSchema,
dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
defaultConfig: widgetInfo.defaultConfig
};
return {

View File

@ -237,6 +237,18 @@
rows="2" maxlength="255"></textarea>
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/255</mat-hint>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>widget.settings-form-selector</mat-label>
<input matInput
[(ngModel)]="widget.settingsDirective"
(ngModelChange)="isDirty = true"/>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>widget.data-key-settings-form-selector</mat-label>
<input matInput
[(ngModel)]="widget.dataKeySettingsDirective"
(ngModelChange)="isDirty = true"/>
</mat-form-field>
</div>
</div>
</mat-tab>

View File

@ -25,6 +25,14 @@ import { EntityId } from '@shared/models/id/entity-id';
import * as moment_ from 'moment';
import { EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models';
import { PopoverPlacement } from '@shared/components/popover.models';
import { PageComponent } from '@shared/components/page.component';
import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, FormGroup } from '@angular/forms';
import {RuleChainType} from "@shared/models/rule-chain.models";
import {Observable} from "rxjs";
import {RuleNodeConfiguration} from "@shared/models/rule-node.models";
export enum widgetType {
timeseries = 'timeseries',
@ -143,6 +151,8 @@ export interface WidgetTypeDescriptor {
controllerScript: string;
settingsSchema?: string | any;
dataKeySettingsSchema?: string | any;
settingsDirective?: string;
dataKeySettingsDirective?: string;
defaultConfig: string;
sizeX: number;
sizeY: number;
@ -159,6 +169,7 @@ export interface WidgetTypeParameters {
singleEntity?: boolean;
warnOnPageDataOverflow?: boolean;
ignoreDataUpdateOnIntervalTick?: boolean;
}
export interface WidgetControllerDescriptor {
@ -483,6 +494,10 @@ export interface WidgetComparisonSettings {
comparisonCustomIntervalValue?: number;
}
export interface WidgetSettings {
[key: string]: any;
}
export interface WidgetConfig {
title?: string;
titleIcon?: string;
@ -512,7 +527,7 @@ export interface WidgetConfig {
decimals?: number;
noDataDisplayMessage?: string;
actions?: {[actionSourceId: string]: Array<WidgetActionDescriptor>};
settings?: any;
settings?: WidgetSettings;
alarmSource?: Datasource;
alarmStatusList?: AlarmSearchStatus[];
alarmSeverityList?: AlarmSeverity[];
@ -570,3 +585,113 @@ export interface WidgetSize {
sizeX: number;
sizeY: number;
}
export interface IWidgetSettingsComponent {
settings: WidgetSettings;
settingsChanged: Observable<WidgetSettings>;
validate();
[key: string]: any;
}
@Directive()
// tslint:disable-next-line:directive-class-suffix
export abstract class WidgetSettingsComponent extends PageComponent implements
IWidgetSettingsComponent, OnInit, AfterViewInit {
settingsValue: WidgetSettings;
private settingsSet = false;
set settings(value: WidgetSettings) {
this.settingsValue = value;
if (!this.settingsSet) {
this.settingsSet = true;
this.setupSettings(value);
} else {
this.updateSettings(value);
}
}
get settings(): WidgetSettings {
return this.settingsValue;
}
settingsChangedEmitter = new EventEmitter<WidgetSettings>();
settingsChanged = this.settingsChangedEmitter.asObservable();
protected constructor(@Inject(Store) protected store: Store<AppState>) {
super(store);
}
ngOnInit() {}
ngAfterViewInit(): void {
setTimeout(() => {
if (!this.validateSettings()) {
this.settingsChangedEmitter.emit(null);
}
}, 0);
}
validate() {
this.onValidate();
}
protected setupSettings(settings: WidgetSettings) {
this.onSettingsSet(this.prepareInputSettings(settings));
this.updateValidators(false);
for (const trigger of this.validatorTriggers()) {
const path = trigger.split('.');
let control: AbstractControl = this.settingsForm();
for (const part of path) {
control = control.get(part);
}
control.valueChanges.subscribe(() => {
this.updateValidators(true, trigger);
});
}
this.settingsForm().valueChanges.subscribe((updated: WidgetSettings) => {
this.onSettingsChanged(updated);
});
}
protected updateSettings(settings: WidgetSettings) {
this.settingsForm().reset(this.prepareInputSettings(settings), {emitEvent: false});
this.updateValidators(false);
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
}
protected validatorTriggers(): string[] {
return [];
}
protected onSettingsChanged(updated: WidgetSettings) {
this.settingsValue = updated;
if (this.validateSettings()) {
this.settingsChangedEmitter.emit(this.prepareOutputSettings(updated));
} else {
this.settingsChangedEmitter.emit(null);
}
}
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
return settings;
}
protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings {
return settings;
}
protected validateSettings(): boolean {
return this.settingsForm().valid;
}
protected onValidate() {}
protected abstract settingsForm(): FormGroup;
protected abstract onSettingsSet(settings: WidgetSettings);
}

View File

@ -2985,6 +2985,8 @@
"widget-settings": "Widget settings",
"description": "Description",
"image-preview": "Image preview",
"settings-form-selector": "Settings form selector",
"data-key-settings-form-selector": "Data key settings form selector",
"javascript": "Javascript",
"js": "JS",
"remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?",
@ -3151,7 +3153,8 @@
"icon-size": "Icon size",
"advanced-settings": "Advanced settings",
"data-settings": "Data settings",
"no-data-display-message": "\"No data to display\" alternative message"
"no-data-display-message": "\"No data to display\" alternative message",
"settings-component-not-found": "Settings form component not found for selector '{{selector}}'"
},
"widget-type": {
"import": "Import widget type",
@ -3266,6 +3269,12 @@
"value": "Value"
},
"invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type",
"qr-code": {
"use-qr-code-text-function": "Use QR code text function",
"qr-code-text-pattern": "QR code text pattern (for ex. '${entityName} | ${keyName} - some text.')",
"qr-code-text-pattern-required": "QR code text pattern is required.",
"qr-code-text-function": "QR code text function"
},
"persistent-table": {
"rpc-id": "RPC ID",
"message-type": "Message type",