20 changed files with 748 additions and 31 deletions
@ -0,0 +1,43 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 '@shared/shared.module'; |
|||
import { |
|||
ScadaSymbolMetadataTagComponent |
|||
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag.component'; |
|||
import { ScadaSymbolMetadataComponent } from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component'; |
|||
import { |
|||
ScadaSymbolMetadataTagsComponent |
|||
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tags.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: |
|||
[ |
|||
ScadaSymbolMetadataComponent, |
|||
ScadaSymbolMetadataTagComponent, |
|||
ScadaSymbolMetadataTagsComponent |
|||
], |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule |
|||
], |
|||
exports: [ |
|||
ScadaSymbolMetadataComponent |
|||
] |
|||
}) |
|||
export class ScadaSymbolMetadataComponentsModule { } |
|||
@ -0,0 +1,74 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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]="tagFormGroup" class="tb-form-panel stroked"> |
|||
<mat-expansion-panel #expansionPanel class="tb-settings"> |
|||
<mat-expansion-panel-header fxLayout="row wrap"> |
|||
<div class="tb-scada-symbol-metadata-tag-header"> |
|||
<div class="tb-form-row tb-flex"> |
|||
<div class="fixed-title-width" translate>scada.tag</div> |
|||
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" (click)="$event.stopPropagation()"> |
|||
<mat-select required formControlName="tag" placeholder="{{ 'scada.select-tag' | translate }}"> |
|||
<mat-option *ngFor="let tag of availableTags" [value]="tag"> |
|||
{{ tag }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<button type="button" |
|||
mat-icon-button |
|||
(click)="tagRemoved.emit()" |
|||
matTooltip="{{ 'scada.remove-tag' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<div class="tb-form-panel stroked"> |
|||
<mat-expansion-panel class="tb-settings"> |
|||
<mat-expansion-panel-header fxLayout="row wrap"> |
|||
<mat-panel-title> |
|||
{{ 'scada.state-render-function' | translate }} |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<tb-js-func formControlName="stateRenderFunction" |
|||
[functionArgs]="['element', 'ctx']" |
|||
helpId="widget/lib/timeseries/cell_style_fn"> |
|||
</tb-js-func> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
</div> |
|||
<div class="tb-form-panel stroked"> |
|||
<mat-expansion-panel class="tb-settings"> |
|||
<mat-expansion-panel-header fxLayout="row wrap"> |
|||
<mat-panel-title> |
|||
{{ 'scada.on-click-action' | translate }} |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<tb-js-func formControlName="clickAction" |
|||
[functionArgs]="['event', 'ctx']" |
|||
helpId="widget/lib/timeseries/cell_style_fn"> |
|||
</tb-js-func> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
</div> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
</div> |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
.tb-scada-symbol-metadata-tag-header { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: row; |
|||
gap: 8px; |
|||
padding-right: 8px; |
|||
} |
|||
@ -0,0 +1,154 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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, EventEmitter, |
|||
forwardRef, |
|||
Input, |
|||
OnChanges, |
|||
OnInit, Output, |
|||
SimpleChanges, ViewChild, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormBuilder, |
|||
UntypedFormGroup, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { IotSvgTag } from '@home/components/widget/lib/svg/iot-svg.models'; |
|||
import { MatExpansionPanel } from '@angular/material/expansion'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-scada-symbol-metadata-tag', |
|||
templateUrl: './scada-symbol-metadata-tag.component.html', |
|||
styleUrls: ['./scada-symbol-metadata-tag.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ScadaSymbolMetadataTagComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class ScadaSymbolMetadataTagComponent implements ControlValueAccessor, OnInit, OnChanges { |
|||
|
|||
@ViewChild('expansionPanel') |
|||
expansionPanel: MatExpansionPanel; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
tags: string[]; |
|||
|
|||
@Output() |
|||
tagRemoved = new EventEmitter(); |
|||
|
|||
availableTags: string[]; |
|||
|
|||
tagFormGroup: UntypedFormGroup; |
|||
|
|||
modelValue: IotSvgTag; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: UntypedFormBuilder, |
|||
private cd: ChangeDetectorRef) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.availableTags = (this.tags || []).concat(); |
|||
this.tagFormGroup = this.fb.group({ |
|||
tag: [null, [Validators.required]], |
|||
stateRenderFunction: [null, []], |
|||
clickAction: [null, []] |
|||
}); |
|||
this.tagFormGroup.valueChanges.subscribe( |
|||
() => this.updateModel() |
|||
); |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (!change.firstChange && change.currentValue !== change.previousValue) { |
|||
if (['tags'].includes(propName)) { |
|||
this.availableTags = (this.tags || []).concat(); |
|||
if (this.modelValue?.tag && !this.availableTags.includes(this.modelValue?.tag)) { |
|||
this.availableTags.push(this.modelValue?.tag); |
|||
this.availableTags.sort(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.tagFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.tagFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: IotSvgTag): void { |
|||
this.modelValue = value; |
|||
if (value?.tag && !(this.tags || []).includes(value?.tag)) { |
|||
this.availableTags = (this.tags || []).concat(value.tag).sort(); |
|||
} |
|||
const clickAction = value?.actions && value?.actions.click ? value.actions.click.actionFunction : null; |
|||
this.tagFormGroup.patchValue( |
|||
{ |
|||
tag: value?.tag, |
|||
stateRenderFunction: value?.stateRenderFunction, |
|||
clickAction |
|||
}, {emitEvent: false} |
|||
); |
|||
} |
|||
|
|||
private updateModel() { |
|||
const value = this.tagFormGroup.value; |
|||
if (value.tag && !this.modelValue?.tag) { |
|||
this.expansionPanel.open(); |
|||
} |
|||
this.modelValue = { |
|||
tag: value.tag, |
|||
stateRenderFunction: value.stateRenderFunction |
|||
}; |
|||
if (value.clickAction) { |
|||
this.modelValue.actions = { |
|||
click: { |
|||
actionFunction: value.clickAction |
|||
} |
|||
}; |
|||
} else { |
|||
this.modelValue.actions = null; |
|||
} |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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-form-panel tb-scada-symbol-metadata-tags"> |
|||
<div *ngIf="tagsFormArray().controls.length; else noTags" class="tb-scada-symbol-metadata-tags-list"> |
|||
<div *ngFor="let tagControl of tagsFormArray().controls; trackBy: trackByTag; let $index = index; let $last = last"> |
|||
<tb-scada-symbol-metadata-tag fxFlex |
|||
[formControl]="tagControl" |
|||
[tags]="availableTags" |
|||
(tagRemoved)="removeTag($index)"> |
|||
</tb-scada-symbol-metadata-tag> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
<button type="button" mat-stroked-button color="primary" (click)="addTag()"> |
|||
{{ 'scada.add-tag' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<ng-template #noTags> |
|||
<span fxLayoutAlign="start center" class="tb-prompt tb-no-tags">{{ 'scada.no-tags' | translate }}</span> |
|||
</ng-template> |
|||
@ -0,0 +1,25 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
.tb-scada-symbol-metadata-tags { |
|||
.tb-scada-symbol-metadata-tags-list { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
} |
|||
.tb-no-tags { |
|||
height: 60px; |
|||
} |
|||
} |
|||
@ -0,0 +1,171 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
ControlValueAccessor, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
UntypedFormArray, |
|||
UntypedFormBuilder, |
|||
UntypedFormControl, |
|||
UntypedFormGroup, |
|||
ValidationErrors, |
|||
Validator |
|||
} from '@angular/forms'; |
|||
import { IotSvgTag } from '@home/components/widget/lib/svg/iot-svg.models'; |
|||
|
|||
const tagValidator = (control: AbstractControl): ValidationErrors | null => { |
|||
const tag: IotSvgTag = control.value; |
|||
if (!tag.tag) { |
|||
return { |
|||
tag: true |
|||
}; |
|||
} |
|||
return null; |
|||
}; |
|||
|
|||
@Component({ |
|||
selector: 'tb-scada-symbol-metadata-tags', |
|||
templateUrl: './scada-symbol-metadata-tags.component.html', |
|||
styleUrls: ['./scada-symbol-metadata-tags.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ScadaSymbolMetadataTagsComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => ScadaSymbolMetadataTagsComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class ScadaSymbolMetadataTagsComponent implements ControlValueAccessor, OnInit, Validator, OnChanges { |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
tags: string[]; |
|||
|
|||
availableTags: string[]; |
|||
|
|||
tagsFormGroup: UntypedFormGroup; |
|||
|
|||
private propagateChange = (_val: any) => {}; |
|||
|
|||
constructor(private fb: UntypedFormBuilder) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.tagsFormGroup = this.fb.group({ |
|||
tags: this.fb.array([]) |
|||
}); |
|||
this.updateAvailableTags(); |
|||
this.tagsFormGroup.valueChanges.subscribe( |
|||
() => { |
|||
let tags: IotSvgTag[] = this.tagsFormGroup.get('tags').value; |
|||
if (tags) { |
|||
tags = tags.filter(t => !!t.tag); |
|||
} |
|||
this.updateAvailableTags(); |
|||
this.propagateChange(tags); |
|||
} |
|||
); |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (!change.firstChange && change.currentValue !== change.previousValue) { |
|||
if (['tags'].includes(propName)) { |
|||
this.updateAvailableTags(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.tagsFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.tagsFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: IotSvgTag[] | undefined): void { |
|||
const tags= value || []; |
|||
this.tagsFormGroup.setControl('tags', this.prepareTagsFormArray(tags), {emitEvent: false}); |
|||
} |
|||
|
|||
public validate(c: UntypedFormControl) { |
|||
const valid = this.tagsFormGroup.valid; |
|||
return valid ? null : { |
|||
tags: { |
|||
valid: false, |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
tagsFormArray(): UntypedFormArray { |
|||
return this.tagsFormGroup.get('tags') as UntypedFormArray; |
|||
} |
|||
|
|||
trackByTag(index: number, tagControl: AbstractControl): any { |
|||
return tagControl; |
|||
} |
|||
|
|||
removeTag(index: number) { |
|||
(this.tagsFormGroup.get('tags') as UntypedFormArray).removeAt(index); |
|||
} |
|||
|
|||
addTag() { |
|||
const tag: IotSvgTag = { |
|||
tag: null |
|||
}; |
|||
const tagsArray = this.tagsFormGroup.get('tags') as UntypedFormArray; |
|||
const tagControl = this.fb.control(tag, [tagValidator]); |
|||
tagsArray.push(tagControl); |
|||
} |
|||
|
|||
private prepareTagsFormArray(tags: IotSvgTag[] | undefined): UntypedFormArray { |
|||
const tagsControls: Array<AbstractControl> = []; |
|||
if (tags) { |
|||
tags.forEach((tag) => { |
|||
tagsControls.push(this.fb.control(tag, [tagValidator])); |
|||
}); |
|||
} |
|||
return this.fb.array(tagsControls); |
|||
} |
|||
|
|||
private updateAvailableTags() { |
|||
const svgTags: IotSvgTag[] = this.tagsFormGroup.get('tags').value; |
|||
const usedTags = (svgTags || []).filter(t => !!t.tag).map(t => t.tag); |
|||
this.availableTags = (this.tags || []).filter(t => !usedTags.includes(t)); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue