Browse Source

Improvements, refactoring, review comments resolve

pull/12509/head
mpetrov 2 years ago
parent
commit
0590b5b271
  1. 36
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  2. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts
  3. 184
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html
  4. 13
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss
  5. 44
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts
  6. 15
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  7. 32
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  8. 131
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html
  9. 33
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss
  10. 22
      ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts
  11. 1
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts
  12. 6
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts
  13. 4
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  14. 1
      ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts
  15. 3
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html
  16. 12
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html
  17. 4
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  18. 22
      ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts
  19. 14
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  20. 1
      ui-ngx/src/app/shared/models/constants.ts
  21. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

36
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts

@ -19,7 +19,7 @@ import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.m
import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog';
import { TimePageLink } from '@shared/models/page/page-link';
import { PageLink } from '@shared/models/page/page-link';
import { Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
@ -27,7 +27,7 @@ import { MINUTE } from '@shared/models/time/time.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors';
import { DestroyRef } from '@angular/core';
import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -35,10 +35,10 @@ import { TbPopoverService } from '@shared/components/popover.service';
import { EntityDebugSettingsPanelComponent } from '@home/components/entity/debug/entity-debug-settings-panel.component';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap } from 'rxjs/operators';
import { CalculatedField } from '@shared/models/calculated-field.models';
import { CalculatedField, CalculatedFieldDialogData } from '@shared/models/calculated-field.models';
import { CalculatedFieldDialogComponent } from './components/public-api';
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, TimePageLink> {
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, PageLink> {
// TODO: [Calculated Fields] remove hardcode when BE variable implemented
readonly calculatedFieldsDebugPerTenantLimitsConfiguration =
@ -54,20 +54,16 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
private durationLeft: DurationLeftPipe,
private popoverService: TbPopoverService,
private destroyRef: DestroyRef,
private renderer: Renderer2
) {
super();
this.tableTitle = this.translate.instant('entity.type-calculated-fields');
this.detailsPanelEnabled = false;
this.selectionEnabled = true;
this.searchEnabled = true;
this.pageMode = false;
this.addEnabled = true;
this.entitiesDeleteEnabled = true;
this.actionsColumnTitle = '';
this.entityType = EntityType.CALCULATED_FIELD;
this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD);
this.entitiesFetchFunction = (pageLink: TimePageLink) => this.fetchCalculatedFields(pageLink);
this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink);
this.addEntity = this.addCalculatedField.bind(this);
this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('calculated-fields.delete-title', {title: field.name});
this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text');
@ -102,12 +98,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
);
}
fetchCalculatedFields(pageLink: TimePageLink): Observable<PageData<CalculatedField>> {
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedField>> {
return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink);
}
onOpenDebugConfig($event: Event, { debugSettings = {}, id }: CalculatedField): void {
const { renderer, viewContainerRef } = this.getTable();
const { viewContainerRef } = this.getTable();
if ($event) {
$event.stopPropagation();
}
@ -115,7 +111,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const debugStrategyPopover = this.popoverService.displayPopover(trigger, renderer,
const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer,
viewContainerRef, EntityDebugSettingsPanelComponent, 'bottom', true, null,
{
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration,
@ -125,7 +121,6 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
},
{},
{}, {}, true);
debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover;
debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((settings: EntityDebugSettings) => {
this.onDebugConfigChanged(id.id, settings);
debugStrategyPopover.hide();
@ -133,17 +128,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
}
}
private addCalculatedField(): void {
this.getCalculatedFieldDialog()
private addCalculatedField(): Observable<CalculatedField> {
return this.getCalculatedFieldDialog()
.pipe(
filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField({ entityId: this.entityId, ...calculatedField })),
)
.subscribe((res) => {
if (res) {
this.updateData();
}
});
}
private editCalculatedField(calculatedField: CalculatedField): void {
@ -159,8 +149,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
});
}
private getCalculatedFieldDialog(value = {}, buttonTitle = 'action.add'): Observable<CalculatedField> {
return this.dialog.open<CalculatedFieldDialogComponent, any, CalculatedField>(CalculatedFieldDialogComponent, {
private getCalculatedFieldDialog(value?: CalculatedField, buttonTitle = 'action.add'): Observable<CalculatedField> {
return this.dialog.open<CalculatedFieldDialogComponent, CalculatedFieldDialogData, CalculatedField>(CalculatedFieldDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {

3
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts

@ -21,6 +21,7 @@ import {
DestroyRef,
effect,
input,
Renderer2,
ViewChild,
} from '@angular/core';
import { EntityId } from '@shared/models/id/entity-id';
@ -56,6 +57,7 @@ export class CalculatedFieldsTableComponent {
private durationLeft: DurationLeftPipe,
private popoverService: TbPopoverService,
private cd: ChangeDetectorRef,
private renderer: Renderer2,
private destroyRef: DestroyRef) {
effect(() => {
@ -69,6 +71,7 @@ export class CalculatedFieldsTableComponent {
this.durationLeft,
this.popoverService,
this.destroyRef,
this.renderer
);
this.cd.markForCheck();
}

184
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html

@ -15,96 +15,100 @@
limitations under the License.
-->
<div class="tb-form-table mb-3">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell w-[17%]">{{ 'calculated-fields.argument-name' | translate }}</div>
<div class="tb-form-table-header-cell w-[35%]">{{ 'calculated-fields.datasource' | translate }}</div>
<div class="tb-form-table-header-cell w-[17%]">{{ 'common.type' | translate }}</div>
<div class="tb-form-table-header-cell w-[31%]">{{ 'entity.key' | translate }}</div>
</div>
<div class="tb-form-table-body tb-drop-list">
@for (group of argumentsFormArray.controls; track group) {
<div [formGroup]="group" class="tb-form-table-row">
<mat-form-field appearance="outline" class="tb-inline-field w-1/6" subscriptSizing="dynamic">
<input matInput formControlName="argumentName" placeholder="{{ 'action.set' | translate }}">
</mat-form-field>
@if (group.get('refEntityId')?.get('id')?.value) {
<ng-container [formGroup]="group.get('refEntityId')">
<mat-form-field appearance="outline" class="tb-inline-field w-1/6" subscriptSizing="dynamic">
<mat-select [value]="group.get('refEntityId').get('entityType').value" formControlName="entityType">
<mat-option [value]="group.get('refEntityId').get('entityType').value">
{{ entityTypeTranslations.get(group.get('refEntityId').get('entityType').value)?.type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-entity-autocomplete
class="inline-entity-autocomplete w-1/6"
formControlName="id"
[withLabel]="false"
[placeholder]="'action.set' | translate"
[appearance]="'outline'"
[subscriptSizing]="'dynamic'"
[entityType]="group.get('refEntityId').get('entityType').value"/>
</ng-container>
} @else {
<mat-form-field appearance="outline" class="tb-inline-field w-[calc(33.3%+12px)]" subscriptSizing="dynamic">
<mat-select [value]="'current'" [disabled]="true">
<mat-option [value]="'current'">
{{
(group.get('refEntityId')?.get('entityType')?.value === ArgumentEntityType.Tenant
? 'calculated-fields.argument-current-tenant'
: 'calculated-fields.argument-current') | translate
}}
</mat-option>
</mat-select>
</mat-form-field>
}
<ng-container [formGroup]="group.get('refEntityKey')">
<mat-form-field appearance="outline" class="tb-inline-field w-1/6" subscriptSizing="dynamic">
@if (group.get('refEntityKey').get('type').value; as type) {
<mat-select [value]="type" formControlName="type">
<mat-option [value]="type">
{{ ArgumentTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
<div class="flex flex-col gap-3">
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell w-1/6">{{ 'calculated-fields.argument-name' | translate }}</div>
<div class="tb-form-table-header-cell w-1/3">{{ 'calculated-fields.datasource' | translate }}</div>
<div class="tb-form-table-header-cell w-1/6">{{ 'common.type' | translate }}</div>
<div class="tb-form-table-header-cell w-1/6">{{ 'entity.key' | translate }}</div>
<div class="tb-form-table-header-cell w-24 min-w-24"></div>
</div>
<div class="tb-form-table-body tb-drop-list">
@for (group of argumentsFormArray.controls; track group) {
<div [formGroup]="group" class="tb-form-table-row">
<mat-form-field appearance="outline" class="tb-inline-field w-1/6" subscriptSizing="dynamic">
<input matInput formControlName="argumentName" placeholder="{{ 'action.set' | translate }}">
</mat-form-field>
<section class="flex w-1/3 gap-2">
@if (group.get('refEntityId')?.get('id')?.value) {
<ng-container [formGroup]="group.get('refEntityId')">
<mat-form-field appearance="outline" class="tb-inline-field flex-1" subscriptSizing="dynamic">
<mat-select [value]="group.get('refEntityId').get('entityType').value" formControlName="entityType">
<mat-option [value]="group.get('refEntityId').get('entityType').value">
{{ entityTypeTranslations.get(group.get('refEntityId').get('entityType').value)?.type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-entity-autocomplete
class="flex-1"
formControlName="id"
[inlineField]="true"
[hideLabel]="true"
[placeholder]="'action.set' | translate"
[entityType]="group.get('refEntityId').get('entityType').value"/>
</ng-container>
} @else {
<mat-form-field appearance="outline" class="tb-inline-field flex-1" subscriptSizing="dynamic">
<mat-select [value]="'current'" [disabled]="true">
<mat-option [value]="'current'">
{{
(group.get('refEntityId')?.get('entityType')?.value === ArgumentEntityType.Tenant
? 'calculated-fields.argument-current-tenant'
: 'calculated-fields.argument-current') | translate
}}
</mat-option>
</mat-select>
</mat-form-field>
}
</section>
<ng-container [formGroup]="group.get('refEntityKey')">
<mat-form-field appearance="outline" class="tb-inline-field w-1/6" subscriptSizing="dynamic">
@if (group.get('refEntityKey').get('type').value; as type) {
<mat-select [value]="type" formControlName="type">
<mat-option [value]="type">
{{ ArgumentTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
}
</mat-form-field>
<mat-chip-listbox formControlName="key" class="tb-inline-field w-1/6">
<mat-chip>
<div tbTruncateWithTooltip class="max-w-25">
{{ group.get('refEntityKey').get('key').value }}
</div>
</mat-chip>
</mat-chip-listbox>
</ng-container>
<div class="flex opacity-55">
<button type="button"
mat-icon-button
#button
(click)="manageArgument($event, button, $index)"
[matTooltip]="'action.edit' | translate"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button type="button"
mat-icon-button
(click)="onDelete($index)"
[matTooltip]="'action.delete' | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
} @empty {
<span class="tb-prompt flex items-center justify-center">{{ 'calculated-fields.no-arguments' | translate }}</span>
}
</mat-form-field>
<mat-chip-listbox formControlName="key" class="tb-inline-field w-1/6">
<mat-chip>
<div tbTruncateWithTooltip class="max-w-25">
{{ group.get('refEntityKey').get('key').value }}
</div>
</mat-chip>
</mat-chip-listbox>
</ng-container>
<div class="flex opacity-55">
<button type="button"
mat-icon-button
#button
(click)="manageArgument($event, button, $index)"
[matTooltip]="'action.edit' | translate"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button type="button"
mat-icon-button
(click)="onDelete($index)"
[matTooltip]="'action.delete' | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
} @empty {
<span class="tb-prompt flex items-center justify-center">{{ 'calculated-fields.no-arguments' | translate }}</span>
}
</div>
@if (errorText && this.argumentsFormArray.dirty) {
<tb-error noMargin [error]="errorText | translate" class="pl-3"/>
}
</div>
<div>
<button type="button" mat-stroked-button color="primary" #button (click)="manageArgument($event, button)">
{{ 'calculated-fields.add-argument' | translate }}
</button>
@if (errorText && this.argumentsFormArray.dirty) {
<tb-error noMargin [error]="errorText | translate" class="pl-3"/>
}
</div>
<div>
<button type="button" mat-stroked-button color="primary" #button (click)="manageArgument($event, button)">
{{ 'calculated-fields.add-argument' | translate }}
</button>
</div>
</div>

13
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.scss

@ -14,18 +14,7 @@
* limitations under the License.
*/
:host ::ng-deep {
.inline-entity-autocomplete {
.mat-mdc-form-field-infix {
padding-top: 8px;
padding-bottom: 8px;
min-height: 40px;
width: auto;
.mdc-text-field__input, .mat-mdc-select {
font-weight: 400;
line-height: 20px;
}
}
.tb-inline-field {
a {
font-size: 14px;
}

44
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts

@ -17,7 +17,6 @@
import {
ChangeDetectorRef,
Component,
DestroyRef,
effect,
forwardRef,
input,
@ -29,6 +28,7 @@ import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
@ -51,6 +51,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { isDefinedAndNotNull } from '@core/utils';
import { charsWithNumRegex } from '@shared/models/regex.constants';
import { TbPopoverComponent } from '@shared/components/popover.component';
@Component({
selector: 'tb-calculated-field-arguments-table',
@ -78,20 +79,19 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
errorText = '';
argumentsFormArray = this.fb.array<AbstractControl>([]);
keysPopupClosed = true;
readonly entityTypeTranslations = entityTypeTranslations;
readonly ArgumentTypeTranslations = ArgumentTypeTranslations;
readonly EntityType = EntityType;
readonly ArgumentEntityType = ArgumentEntityType;
private popoverComponent: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>;
private propagateChange: (argumentsObj: Record<string, CalculatedFieldArgument>) => void = () => {};
constructor(
private fb: FormBuilder,
private popoverService: TbPopoverService,
private viewContainerRef: ViewContainerRef,
private destroyRef: DestroyRef,
private cd: ChangeDetectorRef,
private renderer: Renderer2
) {
@ -123,6 +123,9 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
manageArgument($event: Event, matButton: MatButton, index?: number): void {
$event?.stopPropagation();
if (this.popoverComponent && !this.popoverComponent.tbHidden) {
this.popoverComponent.hide();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
@ -135,27 +138,22 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
buttonTitle: this.argumentsFormArray.at(index)?.value ? 'action.apply' : 'action.add',
tenantId: this.tenantId,
};
this.keysPopupClosed = false;
const argumentsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.popoverComponent = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'left', false, null,
ctx,
{},
{}, {}, true);
argumentsPanelPopover.tbComponentRef.instance.popover = argumentsPanelPopover;
argumentsPanelPopover.tbComponentRef.instance.argumentsDataApplied.subscribe(({ value, index }) => {
argumentsPanelPopover.hide();
this.popoverComponent.tbComponentRef.instance.argumentsDataApplied.subscribe(({ value, index }) => {
this.popoverComponent.hide();
const formGroup = this.getArgumentFormGroup(value);
if (isDefinedAndNotNull(index)) {
this.argumentsFormArray.setControl(index, formGroup);
} else {
this.argumentsFormArray.push(formGroup);
}
this.argumentsFormArray.markAsDirty();
formGroup.markAsDirty();
this.cd.markForCheck();
});
argumentsPanelPopover.tbHideStart.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.keysPopupClosed = true;
});
}
}
@ -171,8 +169,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
}
private getArgumentsObject(): Record<string, CalculatedFieldArgument> {
return this.argumentsFormArray.controls.reduce((acc, control) => {
const rawValue = control.getRawValue();
return this.argumentsFormArray.getRawValue().reduce((acc, rawValue) => {
const { argumentName, ...argument } = rawValue as CalculatedFieldArgumentValue;
acc[argumentName] = argument;
return acc;
@ -186,24 +183,15 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
private populateArgumentsFormArray(argumentsObj: Record<string, CalculatedFieldArgument>): void {
Object.keys(argumentsObj).forEach(key => {
this.argumentsFormArray.push(this.fb.group({
argumentName: [key, [Validators.required, Validators.maxLength(255), Validators.pattern(charsWithNumRegex)]],
const value: CalculatedFieldArgumentValue = {
...argumentsObj[key],
...(argumentsObj[key].refEntityId ? {
refEntityId: this.fb.group({
entityType: [{ value: argumentsObj[key].refEntityId.entityType, disabled: true }],
id: [{ value: argumentsObj[key].refEntityId.id , disabled: true }],
}),
} : {}),
refEntityKey: this.fb.group({
type: [{ value: argumentsObj[key].refEntityKey.type, disabled: true }],
key: [{ value: argumentsObj[key].refEntityKey.key, disabled: true }],
}),
}) as AbstractControl);
argumentName: key
};
this.argumentsFormArray.push(this.getArgumentFormGroup(value), {emitEvent: false});
});
}
private getArgumentFormGroup(value: CalculatedFieldArgumentValue): AbstractControl {
private getArgumentFormGroup(value: CalculatedFieldArgumentValue): FormGroup {
return this.fb.group({
...value,
argumentName: [value.argumentName, [Validators.required, Validators.maxLength(255), Validators.pattern(charsWithNumRegex)]],

15
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -19,7 +19,7 @@
<mat-toolbar color="primary">
<h2>{{ 'entity.type-calculated-field' | translate}}</h2>
<span class="flex-1"></span>
<div [tb-help]="helpLink"></div>
<div tb-help="calculatedField"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
@ -37,24 +37,25 @@
@if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) {
<mat-error>
@if (fieldFormGroup.get('name').hasError('required')) {
{{ 'common.hint.name-required' | translate }}
{{ 'common.hint.title-required' | translate }}
} @else if (fieldFormGroup.get('name').hasError('pattern')) {
{{ 'common.hint.name-pattern' | translate }}
{{ 'common.hint.title-pattern' | translate }}
} @else if (fieldFormGroup.get('name').hasError('maxlength')) {
{{ 'common.hint.name-max-length' | translate }}
{{ 'common.hint.title-max-length' | translate }}
}
</mat-error>
}
</mat-form-field>
<tb-entity-debug-settings-button
formControlName="debugSettings"
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched"
[entityLabel]="'debug-settings.calculated-field' | translate"
[debugLimitsConfiguration]="data.debugLimitsConfiguration"
/>
</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'common.type' | translate }}</mat-label>
<mat-select formControlName="type" required>
<mat-select formControlName="type">
@for (type of fieldTypes; track type) {
<mat-option [value]="type">{{ CalculatedFieldTypeTranslations.get(type) | translate}}</mat-option>
}
@ -102,10 +103,10 @@
</div>
<div class="tb-form-panel" [formGroup]="outputFormGroup">
<div class="tb-form-panel-title">{{ 'calculated-fields.output' | translate }}</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-3">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'calculated-fields.output-type' | translate }}</mat-label>
<mat-select formControlName="type" required>
<mat-select formControlName="type">
@for (type of outputTypes; track type) {
<mat-option [value]="type">{{ OutputTypeTranslations.get(type) | translate}}</mat-option>
}

32
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts

@ -18,10 +18,9 @@ import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { helpBaseUrl } from '@shared/models/constants';
import {
CalculatedField,
CalculatedFieldConfiguration,
@ -35,7 +34,6 @@ import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityType } from '@shared/models/entity-type.models';
import { map, startWith } from 'rxjs/operators';
import { isObject } from '@core/utils';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from '@shared/models/rule-node.models';
@ -50,7 +48,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
type: [CalculatedFieldType.SIMPLE],
debugSettings: [],
configuration: this.fb.group({
arguments: [{}],
arguments: this.fb.control({}),
expressionSIMPLE: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]],
expressionSCRIPT: [],
output: this.fb.group({
@ -64,7 +62,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
functionArgs$ = this.configFormGroup.valueChanges
.pipe(
startWith(this.data.value?.configuration ?? {}),
map(configuration => isObject(configuration?.arguments) ? Object.keys(configuration.arguments) : [])
map(configuration => Object.keys(configuration.arguments))
);
readonly OutputTypeTranslations = OutputTypeTranslations;
@ -73,7 +71,6 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
readonly EntityType = EntityType;
readonly CalculatedFieldType = CalculatedFieldType;
readonly ScriptLanguage = ScriptLanguage;
readonly helpLink = `${helpBaseUrl}/[TODO: [Calculated Fields] add valid link]`;
readonly fieldTypes = Object.values(CalculatedFieldType) as CalculatedFieldType[];
readonly outputTypes = Object.values(OutputType) as OutputType[];
readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations;
@ -81,8 +78,8 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData,
public dialogRef: MatDialogRef<CalculatedFieldDialogComponent, CalculatedField>,
public fb: UntypedFormBuilder) {
protected dialogRef: MatDialogRef<CalculatedFieldDialogComponent, CalculatedField>,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.applyDialogData();
this.observeTypeChanges();
@ -104,14 +101,15 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
if (this.fieldFormGroup.valid) {
const { configuration, type, ...rest } = this.fieldFormGroup.value;
const { expressionSIMPLE, expressionSCRIPT, ...restConfig } = configuration;
this.dialogRef.close({ configuration: { ...restConfig, type, expression: configuration['expression'+type] }, ...rest, type });
this.dialogRef.close({ configuration: { ...restConfig, type, expression: configuration['expression'+type] }, ...rest, type } as CalculatedField);
}
}
private applyDialogData(): void {
const { configuration = {}, type = CalculatedFieldType.SIMPLE, ...value } = this.data.value;
const { configuration = {}, type = CalculatedFieldType.SIMPLE, ...value } = this.data.value ?? {};
const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration;
this.fieldFormGroup.patchValue({ configuration: { ...restConfig, ['expression'+type]: expression }, type, ...value }, {emitEvent: false});
const updatedConfig = { ...restConfig , ['expression'+type]: expression };
this.fieldFormGroup.patchValue({ configuration: updatedConfig, type, ...value }, {emitEvent: false});
}
private observeTypeChanges(): void {
@ -131,8 +129,14 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
}
private toggleKeyByCalculatedFieldType(type: CalculatedFieldType): void {
this.outputFormGroup.get('name')[type === CalculatedFieldType.SIMPLE? 'enable' : 'disable']({emitEvent: false});
this.configFormGroup.get('expression'+CalculatedFieldType.SIMPLE)[type === CalculatedFieldType.SIMPLE? 'enable' : 'disable']({emitEvent: false});
this.configFormGroup.get('expression'+CalculatedFieldType.SCRIPT)[type === CalculatedFieldType.SCRIPT? 'enable' : 'disable']({emitEvent: false});
if (type === CalculatedFieldType.SIMPLE) {
this.outputFormGroup.get('name').enable({emitEvent: false});
this.configFormGroup.get('expressionSIMPLE').enable({emitEvent: false});
this.configFormGroup.get('expressionSCRIPT').disable({emitEvent: false});
} else {
this.outputFormGroup.get('name').disable({emitEvent: false});
this.configFormGroup.get('expressionSIMPLE').disable({emitEvent: false});
this.configFormGroup.get('expressionSCRIPT').enable({emitEvent: false});
}
}
}

131
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.html

@ -21,44 +21,38 @@
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.argument-name' | translate }}</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="off" name="value" formControlName="argumentName" maxlength="255" placeholder="{{ 'action.set' | translate }}"/>
<div class="h-5 pr-2">
@if (argumentFormGroup.get('argumentName').touched) {
@if (argumentFormGroup.get('argumentName').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-required' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').hasError('pattern')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-pattern' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').hasError('maxlength')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-max-length' | translate"
class="tb-error">
warning
</mat-icon>
}
}
</div>
</mat-form-field>
</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="off" name="value" formControlName="argumentName" maxlength="255" placeholder="{{ 'action.set' | translate }}"/>
@if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-required' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('pattern')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-pattern' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (argumentFormGroup.get('argumentName').touched && argumentFormGroup.get('argumentName').hasError('maxlength')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-max-length' | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
</div>
<ng-container [formGroup]="refEntityIdFormGroup">
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'entity.entity-type' | translate }}</div>
<div class="fixed-title-width">{{ 'entity.entity-type' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType">
@for (type of argumentEntityTypes; track type) {
@ -67,34 +61,17 @@
</mat-select>
</mat-form-field>
</div>
@if (entityType === ArgumentEntityType.Device || entityType === ArgumentEntityType.Asset) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.device-name' | translate }}</div>
<tb-entity-autocomplete
class="entity-autocomplete w-full"
formControlName="id"
[withLabel]="false"
[placeholder]="'action.set' | translate"
[appearance]="'outline'"
[required]="true"
[iconError]="true"
[subscriptSizing]="'dynamic'"
[entityType]="EntityType.DEVICE"
/>
</div>
} @else if (entityType === ArgumentEntityType.Customer) {
@if (ArgumentEntityTypeParamsMap.has(entityType)) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.customer-name' | translate }}</div>
<div class="fixed-title-width tb-required">{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}</div>
<tb-entity-autocomplete
class="entity-autocomplete w-full"
class="flex flex-1"
formControlName="id"
[withLabel]="false"
[hideLabel]="true"
[inlineField]="true"
[placeholder]="'action.set' | translate"
[appearance]="'outline'"
[iconError]="true"
[required]="true"
[subscriptSizing]="'dynamic'"
[entityType]="EntityType.CUSTOMER"
[entityType]="ArgumentEntityTypeParamsMap.get(entityType).entityType"
/>
</div>
}
@ -123,13 +100,13 @@
@if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Attribute) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.timeseries-key' | translate }}</div>
<tb-entity-key-autocomplete class="w-full" formControlName="key" [dataKeyType]="DataKeyType.timeseries" [entityFilter]="entityFilter"/>
<tb-entity-key-autocomplete class="flex-1" formControlName="key" [dataKeyType]="DataKeyType.timeseries" [entityFilter]="entityFilter"/>
</div>
} @else {
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.attribute-scope' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="scope" class="w-full">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="flex-1">
<mat-select formControlName="scope">
<mat-option [value]="AttributeScope.SERVER_SCOPE">
{{ 'calculated-fields.server-attributes' | translate }}
</mat-option>
@ -149,7 +126,7 @@
<div class="fixed-title-width tb-required">{{ 'calculated-fields.attribute-key' | translate }}</div>
<tb-entity-key-autocomplete
formControlName="key"
class="w-full"
class="flex-1"
[dataKeyType]="DataKeyType.attribute"
[entityFilter]="entityFilter"
[keyScopeType]="argumentFormGroup.get('refEntityKey').get('scope').value"
@ -161,32 +138,28 @@
@if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) {
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'calculated-fields.default-value' | translate }}</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="off" name="value" formControlName="defaultValue" placeholder="{{ 'action.set' | translate }}"/>
</mat-form-field>
</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="off" name="value" formControlName="defaultValue" placeholder="{{ 'action.set' | translate }}"/>
</mat-form-field>
</div>
} @else {
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title">{{ 'calculated-fields.time-window' | translate }}</div>
<div class="tb-form-row no-border">
<tb-timeinterval
subscriptSizing="dynamic"
appearance="outline"
class="w-full"
formControlName="timeWindow"
/>
</div>
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'calculated-fields.time-window' | translate }}</div>
<tb-timeinterval
subscriptSizing="dynamic"
appearance="outline"
class="flex-1"
formControlName="timeWindow"
/>
</div>
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'calculated-fields.limit' | translate }}</div>
<tb-datapoints-limit class="w-full" formControlName="limit"/>
<tb-datapoints-limit class="flex-1" formControlName="limit"/>
</div>
}
</div>
</div>
<div class="tb-flex flex-end">
<div class="flex justify-end gap-2">
<button mat-button
color="primary"
type="button"

33
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.scss

@ -1,33 +0,0 @@
/**
* 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.
*/
:host {
.mat-mdc-form-field {
width: 100%;
}
}
:host ::ng-deep {
.entity-autocomplete {
.mat-mdc-form-field {
width: 100%;
}
}
.mat-mdc-form-field-infix {
display: flex;
}
}

22
ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts

@ -14,20 +14,19 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, ElementRef, Input, OnInit, output, ViewChild } from '@angular/core';
import { ChangeDetectorRef, Component, Input, OnInit, output } from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { PageComponent } from '@shared/components/page.component';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { charsWithNumRegex, noLeadTrailSpacesRegex } from '@shared/models/regex.constants';
import {
ArgumentEntityType,
ArgumentEntityType, ArgumentEntityTypeParamsMap,
ArgumentEntityTypeTranslations,
ArgumentType,
ArgumentTypeTranslations,
CalculatedFieldArgumentValue,
CalculatedFieldType
} from '@shared/models/calculated-field.models';
import { delay, distinctUntilChanged, filter, throttleTime } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
import { EntityType } from '@shared/models/entity-type.models';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DatasourceType } from '@shared/models/widget.models';
@ -41,11 +40,9 @@ import { MINUTE } from '@shared/models/time/time.models';
@Component({
selector: 'tb-calculated-field-argument-panel',
templateUrl: './calculated-field-argument-panel.component.html',
styleUrls: ['./calculated-field-argument-panel.component.scss']
})
export class CalculatedFieldArgumentPanelComponent extends PageComponent implements OnInit {
export class CalculatedFieldArgumentPanelComponent implements OnInit {
@Input() popover: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>;
@Input() buttonTitle: string;
@Input() index: number;
@Input() argument: CalculatedFieldArgumentValue;
@ -53,8 +50,6 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme
@Input() tenantId: string;
@Input() calculatedFieldType: CalculatedFieldType;
@ViewChild('timeseriesInput') timeseriesInput: ElementRef;
argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>();
argumentFormGroup = this.fb.group({
@ -85,13 +80,13 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme
readonly ArgumentTypeTranslations = ArgumentTypeTranslations;
readonly AttributeScope = AttributeScope;
readonly ArgumentEntityType = ArgumentEntityType;
readonly ArgumentEntityTypeParamsMap = ArgumentEntityTypeParamsMap;
constructor(
private fb: FormBuilder,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
private popover: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>
) {
super();
this.observeEntityFilterChanges();
this.observeEntityTypeChanges()
this.observeEntityKeyChanges();
@ -170,7 +165,7 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme
this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)),
this.refEntityKeyFormGroup.get('scope').valueChanges,
)
.pipe(throttleTime(100), delay(50), takeUntilDestroyed())
.pipe(debounceTime(50), takeUntilDestroyed())
.subscribe(() => this.updateEntityFilter(this.entityType));
}
@ -195,7 +190,6 @@ export class CalculatedFieldArgumentPanelComponent extends PageComponent impleme
const typeControl = this.argumentFormGroup.get('refEntityKey').get('type');
typeControl.setValue(null);
typeControl.markAsTouched();
typeControl.updateValueAndValidity();
}
}
}

1
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts

@ -137,7 +137,6 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor
},
{},
{}, {}, true);
debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover;
debugStrategyPopover.tbComponentRef.instance.onSettingsApplied.subscribe((settings: EntityDebugSettings) => {
this.debugSettingsFormGroup.patchValue(settings);
this.cd.markForCheck();

6
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts

@ -48,7 +48,6 @@ import { distinctUntilChanged, map, startWith, switchMap, takeWhile } from 'rxjs
})
export class EntityDebugSettingsPanelComponent extends PageComponent implements OnInit {
@Input() popover: TbPopoverComponent<EntityDebugSettingsPanelComponent>;
@Input({ transform: booleanAttribute }) failuresEnabled = false;
@Input({ transform: booleanAttribute }) allEnabled = false;
@Input() entityLabel: string;
@ -82,7 +81,8 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
onSettingsApplied = new EventEmitter<EntityDebugSettings>();
constructor(private fb: FormBuilder,
private cd: ChangeDetectorRef) {
private cd: ChangeDetectorRef,
private popover: TbPopoverComponent<EntityDebugSettingsPanelComponent>) {
super();
this.debugAllControl.valueChanges.pipe(
@ -107,7 +107,7 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
}
onCancel(): void {
this.popover?.hide();
this.popover.hide();
}
onApply(): void {

4
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -25,7 +25,6 @@ import {
OnChanges,
OnDestroy,
OnInit,
Renderer2,
SimpleChanges,
ViewChild,
ViewContainerRef,
@ -144,8 +143,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
private elementRef: ElementRef,
private fb: FormBuilder,
private zone: NgZone,
public viewContainerRef: ViewContainerRef,
public renderer: Renderer2) {
public viewContainerRef: ViewContainerRef) {
super(store);
}

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

@ -65,7 +65,6 @@ export interface IEntitiesTableComponent {
sort: MatSort;
route: ActivatedRoute;
viewContainerRef: ViewContainerRef;
renderer: Renderer2;
addEnabled(): boolean;
clearSelection(): void;

3
ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html

@ -32,7 +32,8 @@
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity" label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id"/>
</mat-tab>
<mat-tab *ngIf="entity"

12
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html

@ -15,9 +15,13 @@
limitations under the License.
-->
<mat-form-field [formGroup]="selectEntityFormGroup" class="mat-block" [appearance]="appearance" [subscriptSizing]="subscriptSizing"
<mat-form-field [formGroup]="selectEntityFormGroup" class="mat-block"
[appearance]="inlineField ? 'outline' : appearance"
[subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"
[class.tb-inline-field]="inlineField"
[class.flex]="inlineField"
[class]="additionalClasses">
<mat-label *ngIf="withLabel">{{ label | translate }}</mat-label>
<mat-label *ngIf="!hideLabel">{{ label | translate }}</mat-label>
<input matInput type="text"
#entityInput
formControlName="entity"
@ -29,7 +33,7 @@
<a *ngIf="selectEntityFormGroup.get('entity').value && disabled" aria-label="Open device profile" [routerLink]=entityURL>
{{ displayEntityFn(selectEntityFormGroup.get('entity').value) }}
</a>
<mat-icon *ngIf="iconError && selectEntityFormGroup.get('entity').hasError('required') && selectEntityFormGroup.get('entity').touched"
<mat-icon *ngIf="inlineField && selectEntityFormGroup.get('entity').hasError('required') && selectEntityFormGroup.get('entity').touched"
matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
@ -68,7 +72,7 @@
</div>
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="selectEntityFormGroup.get('entity').hasError('required') && !iconError">
<mat-error *ngIf="selectEntityFormGroup.get('entity').hasError('required') && !inlineField">
{{ requiredErrorText | translate }}
</mat-error>
</mat-form-field>

4
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -136,9 +136,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
@coerceBoolean()
allowCreateNew: boolean;
@Input({ transform: booleanAttribute }) withLabel = true;
@Input({ transform: booleanAttribute }) hideLabel = false;
@Input({ transform: booleanAttribute }) iconError = false;
@Input({ transform: booleanAttribute }) inlineField = false;
@Input() placeholder: string;

22
ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, effect, ElementRef, forwardRef, input, ViewChild, } from '@angular/core';
import { Component, effect, ElementRef, forwardRef, input, OnChanges, SimpleChanges, ViewChild, } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
@ -31,6 +31,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { EntitiesKeysByQuery } from '@shared/models/entity.models';
import { EntityFilter } from '@shared/models/query/query.models';
import { isEqual } from '@core/utils';
@Component({
selector: 'tb-entity-key-autocomplete',
@ -48,9 +49,9 @@ import { EntityFilter } from '@shared/models/query/query.models';
}
],
})
export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Validator {
export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Validator, OnChanges {
@ViewChild('keyInput') keyInput: ElementRef;
@ViewChild('keyInput', {static: true}) keyInput: ElementRef;
entityFilter = input.required<EntityFilter>();
dataKeyType = input.required<DataKeyType>();
@ -95,7 +96,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val
constructor(
private fb: FormBuilder,
private entityService: EntityService,
) {
) {
this.keyControl.valueChanges
.pipe(takeUntilDestroyed())
.subscribe(value => this.propagateChange(value));
@ -107,6 +108,19 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val
});
}
ngOnChanges(changes: SimpleChanges): void {
const filterChanged = changes.entityFilter?.previousValue &&
!isEqual(changes.entityFilter.currentValue, changes.entityFilter.previousValue);
const keyScopeChanged = changes.keyScopeType?.previousValue &&
changes.keyScopeType.currentValue !== changes.keyScopeType.previousValue;
const keyTypeChanged = changes.dataKeyType?.previousValue &&
changes.dataKeyType.currentValue !== changes.dataKeyType.previousValue;
if (filterChanged || keyScopeChanged || keyTypeChanged) {
this.keyControl.setValue('', {emitEvent: false});
}
}
clear(): void {
this.keyControl.patchValue('', {emitEvent: true});
setTimeout(() => {

14
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -19,6 +19,7 @@ import { BaseData } from '@shared/models/base-data';
import { CalculatedFieldId } from '@shared/models/id/calculated-field-id';
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityType } from '@shared/models/entity-type.models';
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId {
debugSettings?: EntityDebugSettings;
@ -114,9 +115,20 @@ export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument {
}
export interface CalculatedFieldDialogData {
value: CalculatedField;
value?: CalculatedField;
buttonTitle: string;
entityId: EntityId;
debugLimitsConfiguration: string;
tenantId: string;
}
export interface ArgumentEntityTypeParams {
title: string;
entityType: EntityType
}
export const ArgumentEntityTypeParamsMap =new Map<ArgumentEntityType, ArgumentEntityTypeParams>([
[ArgumentEntityType.Device, { title: 'calculated-fields.device-name', entityType: EntityType.DEVICE }],
[ArgumentEntityType.Asset, { title: 'calculated-fields.asset-name', entityType: EntityType.ASSET }],
[ArgumentEntityType.Customer, { title: 'calculated-fields.customer-name', entityType: EntityType.CUSTOMER }],
])

1
ui-ngx/src/app/shared/models/constants.ts

@ -196,6 +196,7 @@ export const HelpLinks = {
mobileApplication: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/applications/`,
mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`,
mobileQrCode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/mobile-qr-code/`,
calculatedField: `${helpBaseUrl}/docs${docPlatformPrefix}/`,
}
};
/* eslint-enable max-len */

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

@ -1040,6 +1040,7 @@
"limit": "Max values",
"time-window": "Time window",
"customer-name": "Customer name",
"asset-name": "Asset name",
"timeseries": "Time series",
"output": "Output",
"output-type": "Output type",
@ -1107,6 +1108,9 @@
"name-required": "Name is required.",
"name-pattern": "Name is invalid.",
"name-max-length": "Name should be less than 256 characters.",
"title-required": "Title is required.",
"title-pattern": "Title is invalid.",
"title-max-length": "Title should be less than 256 characters.",
"key-required": "Key is required.",
"key-pattern": "Key is invalid.",
"key-max-length": "Key should be less than 256 characters."

Loading…
Cancel
Save