Calculated field add/edit basic implementation
This commit is contained in:
parent
7c11848e3d
commit
fd42c51df1
@ -16,7 +16,7 @@
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { PageData } from '@shared/models/page/page-data';
|
||||
import { CalculatedField } from '@shared/models/calculated-field.models';
|
||||
@ -25,64 +25,26 @@ import { PageLink } from '@shared/models/page/page-link';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
// [TODO]: [Calculated fields] - implement when BE ready
|
||||
export class CalculatedFieldsService {
|
||||
|
||||
fieldsMock = [
|
||||
{
|
||||
name: 'Calculated Field 1',
|
||||
type: 'Simple',
|
||||
configuration: {
|
||||
expression: '1 + 2',
|
||||
type: 'SIMPLE',
|
||||
},
|
||||
entityId: '1',
|
||||
id: {
|
||||
id: '1',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Calculated Field 2',
|
||||
type: 'Script',
|
||||
entityId: '2',
|
||||
configuration: {
|
||||
expression: '${power}',
|
||||
type: 'SIMPLE',
|
||||
},
|
||||
id: {
|
||||
id: '2',
|
||||
}
|
||||
}
|
||||
] as any[];
|
||||
|
||||
constructor(
|
||||
private http: HttpClient
|
||||
) { }
|
||||
|
||||
public getCalculatedField(calculatedFieldId: string, config?: RequestConfig): Observable<CalculatedField> {
|
||||
return of(this.fieldsMock[0]);
|
||||
// return this.http.get<any>(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config));
|
||||
public getCalculatedFieldById(calculatedFieldId: string, config?: RequestConfig): Observable<CalculatedField> {
|
||||
return this.http.get<any>(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public saveCalculatedField(calculatedField: any, config?: RequestConfig): Observable<CalculatedField> {
|
||||
return of(this.fieldsMock[1]);
|
||||
// return this.http.post<any>('/api/calculatedField', calculatedField, defaultHttpOptionsFromConfig(config));
|
||||
public saveCalculatedField(calculatedField: CalculatedField, config?: RequestConfig): Observable<CalculatedField> {
|
||||
return this.http.post<any>('/api/calculatedField', calculatedField, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public deleteCalculatedField(calculatedFieldId: string, config?: RequestConfig): Observable<boolean> {
|
||||
return of(true);
|
||||
// return this.http.delete<boolean>(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config));
|
||||
return this.http.delete<boolean>(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
|
||||
public getCalculatedFields(pageLink: PageLink,
|
||||
config?: RequestConfig): Observable<PageData<CalculatedField>> {
|
||||
return of({
|
||||
data: this.fieldsMock,
|
||||
totalPages: 1,
|
||||
totalElements: 2,
|
||||
hasNext: false,
|
||||
});
|
||||
// return this.http.get<PageData<any>>(`/api/calculatedField${pageLink.toQuery()}`,
|
||||
// defaultHttpOptionsFromConfig(config));
|
||||
public getCalculatedFields(pageLink: PageLink, config?: RequestConfig): Observable<PageData<CalculatedField>> {
|
||||
return this.http.get<PageData<any>>(`/api/calculatedFields${pageLink.toQuery()}`,
|
||||
defaultHttpOptionsFromConfig(config));
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,41 +23,33 @@ import { TimePageLink } 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';
|
||||
import { DialogService } from '@core/services/dialog.service';
|
||||
import { MINUTE } from '@shared/models/time/time.models';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { getCurrentAuthState } from '@core/auth/auth.selectors';
|
||||
import { ChangeDetectorRef, DestroyRef, ViewContainerRef } from '@angular/core';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { UtilsService } from '@core/services/utils.service';
|
||||
import { EntityService } from '@core/http/entity.service';
|
||||
import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors';
|
||||
import { DestroyRef } 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';
|
||||
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, switchMap } from 'rxjs/operators';
|
||||
import { catchError, filter, switchMap } from 'rxjs/operators';
|
||||
import { CalculatedField } from '@shared/models/calculated-field.models';
|
||||
import { CalculatedFieldDialogComponent } from './components/public-api';
|
||||
|
||||
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField, TimePageLink> {
|
||||
|
||||
readonly calculatedFieldsDebugPerTenantLimitsConfiguration =
|
||||
getCurrentAuthState(this.store)['calculatedFieldsDebugPerTenantLimitsConfiguration'] || '1:1';
|
||||
readonly maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE;
|
||||
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
|
||||
|
||||
constructor(private calculatedFieldsService: CalculatedFieldsService,
|
||||
private entityService: EntityService,
|
||||
private dialogService: DialogService,
|
||||
private translate: TranslateService,
|
||||
private dialog: MatDialog,
|
||||
public entityId: EntityId = null,
|
||||
private store: Store<AppState>,
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
private overlay: Overlay,
|
||||
private cd: ChangeDetectorRef,
|
||||
private utilsService: UtilsService,
|
||||
private durationLeft: DurationLeftPipe,
|
||||
private popoverService: TbPopoverService,
|
||||
private destroyRef: DestroyRef,
|
||||
@ -67,6 +59,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
||||
this.detailsPanelEnabled = false;
|
||||
this.selectionEnabled = true;
|
||||
this.searchEnabled = true;
|
||||
this.pageMode = false;
|
||||
this.addEnabled = true;
|
||||
this.entitiesDeleteEnabled = true;
|
||||
this.actionsColumnTitle = '';
|
||||
@ -74,6 +67,12 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
||||
this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD);
|
||||
|
||||
this.entitiesFetchFunction = pageLink => this.fetchCalculatedFields(pageLink);
|
||||
this.addEntity = this.addCalculatedField.bind(this);
|
||||
this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name});
|
||||
this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text');
|
||||
this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count});
|
||||
this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text');
|
||||
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
|
||||
|
||||
this.defaultSortOrder = {property: 'name', direction: Direction.DESC};
|
||||
|
||||
@ -97,8 +96,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
||||
name: this.translate.instant('action.edit'),
|
||||
icon: 'edit',
|
||||
isEnabled: () => true,
|
||||
// // [TODO]: [Calculated fields] - implement edit
|
||||
onAction: (_, entity) => {}
|
||||
onAction: (_, entity) => this.editCalculatedField(entity)
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -121,7 +119,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
||||
{
|
||||
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration,
|
||||
maxDebugModeDuration: this.maxDebugModeDuration,
|
||||
entityLabel: this.translate.instant('debug-settings.integration'),
|
||||
entityLabel: this.translate.instant('debug-settings.calculated-field'),
|
||||
...debugSettings
|
||||
},
|
||||
{},
|
||||
@ -134,6 +132,48 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
||||
}
|
||||
}
|
||||
|
||||
private addCalculatedField(): void {
|
||||
this.getCalculatedFieldDialog()
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField({ entityId: this.entityId, ...calculatedField} as any)),
|
||||
)
|
||||
.subscribe((res) => {
|
||||
if (res) {
|
||||
this.updateData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private editCalculatedField(calculatedField: CalculatedField): void {
|
||||
this.getCalculatedFieldDialog(calculatedField, 'action.apply')
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
filter(Boolean),
|
||||
switchMap((updatedCalculatedField) => this.calculatedFieldsService.saveCalculatedField({ ...calculatedField, ...updatedCalculatedField} as any)),
|
||||
)
|
||||
.subscribe((res) => {
|
||||
if (res) {
|
||||
this.updateData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getCalculatedFieldDialog(value = {}, buttonTitle = 'action.add') {
|
||||
return this.dialog.open<CalculatedFieldDialogComponent, any, CalculatedField>(CalculatedFieldDialogComponent, {
|
||||
disableClose: true,
|
||||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
|
||||
data: {
|
||||
value,
|
||||
buttonTitle,
|
||||
entityId: this.entityId,
|
||||
debugLimitsConfiguration: this.calculatedFieldsDebugPerTenantLimitsConfiguration,
|
||||
tenantId: this.tenantId,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private getDebugConfigLabel(debugSettings: EntityDebugSettings): string {
|
||||
const isDebugActive = this.isDebugActive(debugSettings?.allEnabledUntil);
|
||||
|
||||
@ -149,7 +189,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
|
||||
}
|
||||
|
||||
private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void {
|
||||
this.calculatedFieldsService.getCalculatedField(id).pipe(
|
||||
this.calculatedFieldsService.getCalculatedFieldById(id).pipe(
|
||||
switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })),
|
||||
catchError(() => of(null)),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
|
||||
@ -16,24 +16,18 @@
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
Input,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { EntityId } from '@shared/models/id/entity-id';
|
||||
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
|
||||
import { EntityService } from '@core/http/entity.service';
|
||||
import { DialogService } from '@core/services/dialog.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { Overlay } from '@angular/cdk/overlay';
|
||||
import { UtilsService } from '@core/services/utils.service';
|
||||
import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config';
|
||||
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
|
||||
import { TbPopoverService } from '@shared/components/popover.service';
|
||||
@ -51,7 +45,6 @@ export class CalculatedFieldsTableComponent implements OnInit {
|
||||
set entityId(entityId: EntityId) {
|
||||
if (this.entityIdValue !== entityId) {
|
||||
this.entityIdValue = entityId;
|
||||
this.entitiesTable.resetSortAndFilter(this.activeValue);
|
||||
if (!this.activeValue) {
|
||||
this.hasInitialized = true;
|
||||
}
|
||||
@ -78,18 +71,12 @@ export class CalculatedFieldsTableComponent implements OnInit {
|
||||
private entityIdValue: EntityId;
|
||||
|
||||
constructor(private calculatedFieldsService: CalculatedFieldsService,
|
||||
private entityService: EntityService,
|
||||
private dialogService: DialogService,
|
||||
private translate: TranslateService,
|
||||
private dialog: MatDialog,
|
||||
private store: Store<AppState>,
|
||||
private overlay: Overlay,
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
private cd: ChangeDetectorRef,
|
||||
private durationLeft: DurationLeftPipe,
|
||||
private popoverService: TbPopoverService,
|
||||
private destroyRef: DestroyRef,
|
||||
private utilsService: UtilsService) {
|
||||
private destroyRef: DestroyRef) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@ -97,19 +84,13 @@ export class CalculatedFieldsTableComponent implements OnInit {
|
||||
|
||||
this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig(
|
||||
this.calculatedFieldsService,
|
||||
this.entityService,
|
||||
this.dialogService,
|
||||
this.translate,
|
||||
this.dialog,
|
||||
this.entityIdValue,
|
||||
this.store,
|
||||
this.viewContainerRef,
|
||||
this.overlay,
|
||||
this.cd,
|
||||
this.utilsService,
|
||||
this.durationLeft,
|
||||
this.popoverService,
|
||||
this.destroyRef
|
||||
this.destroyRef,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
<!--
|
||||
|
||||
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-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">
|
||||
<mat-select [value]="group.get('refEntityKey').get('type').value" formControlName="type">
|
||||
<mat-option [value]="group.get('refEntityKey').get('type').value">
|
||||
{{ ArgumentTypeTranslations.get(group.get('refEntityKey').get('type').value) | 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>
|
||||
}
|
||||
</div>
|
||||
@if (errorText) {
|
||||
<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>
|
||||
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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 ::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;
|
||||
}
|
||||
}
|
||||
a {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,213 @@
|
||||
///
|
||||
/// 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,
|
||||
DestroyRef,
|
||||
effect,
|
||||
forwardRef,
|
||||
input,
|
||||
Input,
|
||||
Renderer2,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
ControlValueAccessor,
|
||||
FormBuilder,
|
||||
NG_VALIDATORS,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ValidationErrors,
|
||||
Validator,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
ArgumentEntityType,
|
||||
ArgumentType,
|
||||
ArgumentTypeTranslations,
|
||||
CalculatedFieldArgument,
|
||||
CalculatedFieldArgumentValue,
|
||||
CalculatedFieldType,
|
||||
} from '@shared/models/calculated-field.models';
|
||||
import {
|
||||
CalculatedFieldArgumentPanelComponent
|
||||
} from '@home/components/calculated-fields/components/panel/calculated-field-argument-panel.component';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { TbPopoverService } from '@shared/components/popover.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { EntityId } from '@shared/models/id/entity-id';
|
||||
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
|
||||
import { isDefinedAndNotNull } from '@core/utils';
|
||||
import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-calculated-field-arguments-table',
|
||||
templateUrl: './calculated-field-arguments-table.component.html',
|
||||
styleUrls: [`calculated-field-arguments-table.component.scss`],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent),
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => CalculatedFieldArgumentsTableComponent),
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
})
|
||||
export class CalculatedFieldArgumentsTableComponent implements ControlValueAccessor, Validator {
|
||||
|
||||
@Input() entityId: EntityId;
|
||||
@Input() tenantId: string;
|
||||
|
||||
calculatedFieldType = input<CalculatedFieldType>()
|
||||
|
||||
errorText = '';
|
||||
argumentsFormArray = this.fb.array<AbstractControl>([]);
|
||||
keysPopupClosed = true;
|
||||
|
||||
readonly entityTypeTranslations = entityTypeTranslations;
|
||||
readonly ArgumentTypeTranslations = ArgumentTypeTranslations;
|
||||
readonly EntityType = EntityType;
|
||||
readonly ArgumentEntityType = ArgumentEntityType;
|
||||
|
||||
private onChange: (argumentsObj: Record<string, CalculatedFieldArgument>) => void = () => {};
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private popoverService: TbPopoverService,
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
private destroyRef: DestroyRef,
|
||||
private cd: ChangeDetectorRef,
|
||||
private renderer: Renderer2
|
||||
) {
|
||||
this.argumentsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => {
|
||||
this.onChange(this.getArgumentsObject());
|
||||
});
|
||||
effect(() => this.calculatedFieldType() && this.argumentsFormArray.updateValueAndValidity());
|
||||
}
|
||||
|
||||
registerOnChange(fn: (argumentsObj: Record<string, CalculatedFieldArgument>) => void): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(_): void {}
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
if (this.calculatedFieldType() === CalculatedFieldType.SIMPLE
|
||||
&& this.argumentsFormArray.controls.some(control => control.get('refEntityKey').get('type').value === ArgumentType.Rolling)) {
|
||||
this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling';
|
||||
} else if (!this.argumentsFormArray.controls.length) {
|
||||
this.errorText = 'calculated-fields.hint.arguments-empty';
|
||||
} else {
|
||||
this.errorText = '';
|
||||
}
|
||||
return this.errorText ? { argumentsFormArray: false } : null;
|
||||
}
|
||||
|
||||
private getArgumentsObject(): Record<string, CalculatedFieldArgument> {
|
||||
return this.argumentsFormArray.controls.reduce((acc, control) => {
|
||||
const rawValue = control.getRawValue();
|
||||
const { argumentName, ...argument } = rawValue as CalculatedFieldArgumentValue;
|
||||
acc[argumentName] = argument;
|
||||
return acc;
|
||||
}, {} as Record<string, CalculatedFieldArgument>);
|
||||
}
|
||||
|
||||
writeValue(argumentsObj: Record<string, CalculatedFieldArgument>): void {
|
||||
this.argumentsFormArray.clear();
|
||||
Object.keys(argumentsObj).forEach(key => {
|
||||
this.argumentsFormArray.push(this.fb.group({
|
||||
argumentName: [key, [Validators.required, Validators.maxLength(255), Validators.pattern(noLeadTrailSpacesRegex)]],
|
||||
...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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
manageArgument($event: Event, matButton: MatButton, index?: number): void {
|
||||
$event?.stopPropagation();
|
||||
const trigger = matButton._elementRef.nativeElement;
|
||||
if (this.popoverService.hasPopover(trigger)) {
|
||||
this.popoverService.hidePopover(trigger);
|
||||
} else {
|
||||
const ctx = {
|
||||
index,
|
||||
argument: this.argumentsFormArray.at(index)?.getRawValue() ?? {},
|
||||
entityId: this.entityId,
|
||||
calculatedFieldType: this.calculatedFieldType(),
|
||||
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.viewContainerRef, CalculatedFieldArgumentPanelComponent, 'leftBottom', false, null,
|
||||
ctx,
|
||||
{},
|
||||
{}, {}, true);
|
||||
argumentsPanelPopover.tbComponentRef.instance.popover = argumentsPanelPopover;
|
||||
argumentsPanelPopover.tbComponentRef.instance.argumentsDataApplied.subscribe(({ value, index }) => {
|
||||
argumentsPanelPopover.hide();
|
||||
const formGroup = this.getArgumentFormGroup(value);
|
||||
if (isDefinedAndNotNull(index)) {
|
||||
this.argumentsFormArray.setControl(index, formGroup);
|
||||
} else {
|
||||
this.argumentsFormArray.push(formGroup);
|
||||
}
|
||||
this.argumentsFormArray.markAsDirty();
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
argumentsPanelPopover.tbHideStart.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.keysPopupClosed = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getArgumentFormGroup(value: CalculatedFieldArgumentValue): AbstractControl {
|
||||
return this.fb.group({
|
||||
...value,
|
||||
argumentName: [value.argumentName, [Validators.required, Validators.maxLength(255), Validators.pattern(noLeadTrailSpacesRegex)]],
|
||||
...(value.refEntityId ? {
|
||||
refEntityId: this.fb.group({
|
||||
entityType: [{ value: value.refEntityId.entityType, disabled: true }],
|
||||
id: [{ value: value.refEntityId.id , disabled: true }],
|
||||
}),
|
||||
} : {}),
|
||||
refEntityKey: this.fb.group({
|
||||
type: [{ value: value.refEntityKey.type, disabled: true }],
|
||||
key: [{ value: value.refEntityKey.key, disabled: true }],
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
onDelete(index: number): void {
|
||||
this.argumentsFormArray.removeAt(index);
|
||||
this.argumentsFormArray.markAsDirty();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
<!--
|
||||
|
||||
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]="fieldFormGroup" class="h-full w-screen min-w-80 max-w-4xl">
|
||||
<mat-toolbar color="primary">
|
||||
<h2>{{ 'entity.type-calculated-field' | translate}}</h2>
|
||||
<span class="flex-1"></span>
|
||||
<div [tb-help]="helpLink"></div>
|
||||
<button mat-icon-button
|
||||
(click)="cancel()"
|
||||
type="button">
|
||||
<mat-icon class="material-icons">close</mat-icon>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<div mat-dialog-content>
|
||||
<div class="tb-form-panel no-border no-padding">
|
||||
<div class="tb-form-panel">
|
||||
<div class="tb-form-panel-title">{{ 'common.general' | translate }}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label>{{ 'entity-field.title' | translate }}</mat-label>
|
||||
<input matInput maxlength="255" formControlName="name" required>
|
||||
@if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) {
|
||||
<mat-error>
|
||||
@if (fieldFormGroup.get('name').hasError('required')) {
|
||||
{{ 'common.hint.name-required' | translate }}
|
||||
} @else if (fieldFormGroup.get('name').hasError('pattern')) {
|
||||
{{ 'common.hint.name-pattern' | translate }}
|
||||
} @else if (fieldFormGroup.get('name').hasError('maxlength')) {
|
||||
{{ 'common.hint.name-max-length' | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<tb-entity-debug-settings-button
|
||||
formControlName="debugSettings"
|
||||
[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>
|
||||
@for (type of fieldTypes; track type) {
|
||||
<mat-option [value]="type">{{ CalculatedFieldTypeTranslations.get(type) | translate}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<ng-container [formGroup]="configFormGroup">
|
||||
<div class="tb-form-panel">
|
||||
<div class="tb-form-panel-title">{{ 'calculated-fields.arguments' | translate }}</div>
|
||||
<tb-calculated-field-arguments-table
|
||||
formControlName="arguments"
|
||||
[entityId]="data.entityId"
|
||||
[tenantId]="data.tenantId"
|
||||
[calculatedFieldType]="fieldFormGroup.get('type').valueChanges | async"
|
||||
/>
|
||||
</div>
|
||||
<div class="tb-form-panel">
|
||||
<div class="tb-form-panel-title">{{ 'calculated-fields.expression' | translate }}*</div>
|
||||
@if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) {
|
||||
<mat-form-field class="mat-block" appearance="outline">
|
||||
<input matInput formControlName="expressionSIMPLE" maxlength="255" [placeholder]="'action.set' | translate" required>
|
||||
@if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) {
|
||||
<mat-error>
|
||||
@if (configFormGroup.get('expressionSIMPLE').hasError('required')) {
|
||||
{{ 'calculated-fields.hint.expression-required' | translate }}
|
||||
} @else if (configFormGroup.get('expressionSIMPLE').hasError('pattern')) {
|
||||
{{ 'calculated-fields.hint.expression-invalid' | translate }}
|
||||
} @else if (configFormGroup.get('expressionSIMPLE').hasError('maxLength')) {
|
||||
{{ 'calculated-fields.hint.expression-max-length' | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
} @else {
|
||||
<tb-js-func
|
||||
required
|
||||
formControlName="expressionSCRIPT"
|
||||
functionName="calculate"
|
||||
[functionArgs]="functionArgs$ | async"
|
||||
[disableUndefinedCheck]="true"
|
||||
[scriptLanguage]="ScriptLanguage.TBEL"
|
||||
helpId="[TODO]: ADD VALID LINK HERE!!!"
|
||||
/>
|
||||
}
|
||||
</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">
|
||||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label>{{ 'calculated-fields.output-type' | translate }}</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
@for (type of outputTypes; track type) {
|
||||
<mat-option [value]="type">{{ OutputTypeTranslations.get(type) | translate}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (outputFormGroup.get('type').value === OutputType.Attribute) {
|
||||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label>{{ 'calculated-fields.output-type' | translate }}</mat-label>
|
||||
<mat-select formControlName="scope" class="w-full">
|
||||
<mat-option [value]="AttributeScope.SERVER_SCOPE">
|
||||
{{ 'calculated-fields.server-attributes' | translate }}
|
||||
</mat-option>
|
||||
@if (data.entityId.entityType === EntityType.DEVICE) {
|
||||
<mat-option [value]="AttributeScope.SHARED_SCOPE">
|
||||
{{ 'calculated-fields.shared-attributes' | translate }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
@if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) {
|
||||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-label>
|
||||
{{ (outputFormGroup.get('type').value === OutputType.Timeseries
|
||||
? 'calculated-fields.timeseries-key'
|
||||
: 'calculated-fields.attribute-key')
|
||||
| translate }}
|
||||
</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
@if (outputFormGroup.get('name').errors && outputFormGroup.get('name').touched) {
|
||||
<mat-error>
|
||||
@if (outputFormGroup.get('name').hasError('required')) {
|
||||
{{ 'common.hint.key-required' | translate }}
|
||||
} @else if (outputFormGroup.get('name').hasError('pattern')) {
|
||||
{{ 'common.hint.key-pattern' | translate }}
|
||||
} @else if (outputFormGroup.get('name').hasError('maxlength')) {
|
||||
{{ 'common.hint.key-max-length' | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions class="justify-end">
|
||||
<button mat-button color="primary"
|
||||
type="button"
|
||||
cdkFocusInitial
|
||||
(click)="cancel()">
|
||||
{{ 'action.cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary"
|
||||
(click)="add()"
|
||||
[disabled]="fieldFormGroup.invalid || !fieldFormGroup.dirty">
|
||||
{{ data.buttonTitle | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,122 @@
|
||||
///
|
||||
/// 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, 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 { Router } from '@angular/router';
|
||||
import { DialogComponent } from '@shared/components/dialog.component';
|
||||
import { helpBaseUrl } from '@shared/models/constants';
|
||||
import {
|
||||
CalculatedField,
|
||||
CalculatedFieldConfiguration,
|
||||
CalculatedFieldDialogData,
|
||||
CalculatedFieldType,
|
||||
CalculatedFieldTypeTranslations,
|
||||
OutputType,
|
||||
OutputTypeTranslations
|
||||
} from '@shared/models/calculated-field.models';
|
||||
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 } from 'rxjs/operators';
|
||||
import { isObject } from '@core/utils';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ScriptLanguage } from '@shared/models/rule-node.models';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-calculated-field-dialog',
|
||||
templateUrl: './calculated-field-dialog.component.html',
|
||||
})
|
||||
export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFieldDialogComponent, CalculatedField> {
|
||||
|
||||
fieldFormGroup = this.fb.group({
|
||||
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]],
|
||||
type: [CalculatedFieldType.SIMPLE, [Validators.required]],
|
||||
debugSettings: [],
|
||||
configuration: this.fb.group({
|
||||
arguments: [{}],
|
||||
expressionSIMPLE: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]],
|
||||
expressionSCRIPT: [],
|
||||
output: this.fb.group({
|
||||
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]],
|
||||
scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }],
|
||||
type: [OutputType.Timeseries]
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
functionArgs$ = this.fieldFormGroup.get('configuration').valueChanges
|
||||
.pipe(
|
||||
map(configuration => isObject(configuration?.arguments) ? Object.keys(configuration.arguments) : [])
|
||||
);
|
||||
|
||||
readonly OutputTypeTranslations = OutputTypeTranslations;
|
||||
readonly OutputType = OutputType;
|
||||
readonly AttributeScope = AttributeScope;
|
||||
readonly EntityType = EntityType;
|
||||
readonly CalculatedFieldType = CalculatedFieldType;
|
||||
readonly ScriptLanguage = ScriptLanguage;
|
||||
readonly helpLink = `${helpBaseUrl}/[TODO: ADD VALID LINK!!!]`;
|
||||
readonly fieldTypes = Object.values(CalculatedFieldType) as CalculatedFieldType[];
|
||||
readonly outputTypes = Object.values(OutputType) as OutputType[];
|
||||
readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations;
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
protected router: Router,
|
||||
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData,
|
||||
public dialogRef: MatDialogRef<CalculatedFieldDialogComponent, CalculatedField>,
|
||||
public fb: UntypedFormBuilder) {
|
||||
super(store, router, dialogRef);
|
||||
this.applyDialogData();
|
||||
this.outputFormGroup.get('type').valueChanges
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(type => this.toggleScopeByOutputType(type));
|
||||
this.toggleScopeByOutputType(this.outputFormGroup.get('type').value);
|
||||
}
|
||||
|
||||
get configFormGroup(): FormGroup {
|
||||
return this.fieldFormGroup.get('configuration') as FormGroup;
|
||||
}
|
||||
|
||||
get outputFormGroup(): FormGroup {
|
||||
return this.fieldFormGroup.get('configuration').get('output') as FormGroup;
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.dialogRef.close(null);
|
||||
}
|
||||
|
||||
add(): void {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
private applyDialogData(): void {
|
||||
const { configuration = {}, type = CalculatedFieldType.SIMPLE, ...value } = this.data.value;
|
||||
const { expression, ...restConfig } = configuration as CalculatedFieldConfiguration;
|
||||
this.fieldFormGroup.patchValue({ configuration: { ...restConfig, ['expression'+type]: expression }, ...value });
|
||||
}
|
||||
|
||||
private toggleScopeByOutputType(type: OutputType): void {
|
||||
this.outputFormGroup.get('scope')[type === OutputType.Attribute? 'enable' : 'disable']({emitEvent: false});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,198 @@
|
||||
<!--
|
||||
|
||||
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="w-screen max-w-xl" [formGroup]="argumentFormGroup">
|
||||
<div class="tb-form-panel no-border no-padding mb-2">
|
||||
<div class="tb-form-panel-title">{{ 'calculated-fields.argument-settings' | translate }}</div>
|
||||
<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 name="value" formControlName="argumentName" maxlength="255" placeholder="{{ 'action.set' | translate }}"/>
|
||||
@if (argumentFormGroup.get('argumentName').hasError('required') && argumentFormGroup.get('argumentName').touched) {
|
||||
<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') && argumentFormGroup.get('argumentName').touched) {
|
||||
<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') && argumentFormGroup.get('argumentName').touched) {
|
||||
<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>
|
||||
</div>
|
||||
<ng-container [formGroup]="refEntityIdFormGroup">
|
||||
<div class="tb-form-row">
|
||||
<div class="fixed-title-width tb-required">{{ '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) {
|
||||
<mat-option [value]="type">{{ ArgumentEntityTypeTranslations.get(type) | translate }}</mat-option>
|
||||
}
|
||||
</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) {
|
||||
<div class="tb-form-row">
|
||||
<div class="fixed-title-width tb-required">{{ 'calculated-fields.customer-name' | translate }}</div>
|
||||
<tb-entity-autocomplete
|
||||
class="entity-autocomplete w-full"
|
||||
formControlName="id"
|
||||
[withLabel]="false"
|
||||
[placeholder]="'action.set' | translate"
|
||||
[appearance]="'outline'"
|
||||
[iconError]="true"
|
||||
[required]="true"
|
||||
[subscriptSizing]="'dynamic'"
|
||||
[entityType]="EntityType.CUSTOMER"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
<ng-container [formGroup]="refEntityKeyFormGroup">
|
||||
<div class="tb-form-row">
|
||||
<div class="fixed-title-width tb-required">{{ 'calculated-fields.argument-type' | translate }}</div>
|
||||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
|
||||
<mat-select formControlName="type">
|
||||
@for (type of argumentTypes; track type) {
|
||||
<mat-option [value]="type">{{ ArgumentTypeTranslations.get(type) | translate }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (refEntityKeyFormGroup.get('type').hasError('required') && refEntityKeyFormGroup.get('type').touched) {
|
||||
<mat-icon matSuffix
|
||||
matTooltipPosition="above"
|
||||
matTooltipClass="tb-error-tooltip"
|
||||
[matTooltip]="'calculated-fields.hint.argument-type-required' | translate"
|
||||
class="tb-error">
|
||||
warning
|
||||
</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@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 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-option [value]="AttributeScope.SERVER_SCOPE">
|
||||
{{ 'calculated-fields.server-attributes' | translate }}
|
||||
</mat-option>
|
||||
@if ((keyEntityType$ | async) === EntityType.DEVICE) {
|
||||
<mat-option [value]="AttributeScope.CLIENT_SCOPE">
|
||||
{{ 'calculated-fields.client-attributes' | translate }}
|
||||
</mat-option>
|
||||
<mat-option [value]="AttributeScope.SHARED_SCOPE">
|
||||
{{ 'calculated-fields.shared-attributes' | translate }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="tb-form-row">
|
||||
<div class="fixed-title-width tb-required">{{ 'calculated-fields.attribute-key' | translate }}</div>
|
||||
<tb-entity-key-autocomplete
|
||||
formControlName="key"
|
||||
[dataKeyType]="DataKeyType.attribute"
|
||||
[entityFilter]="entityFilter"
|
||||
[keyScopeType]="argumentFormGroup.get('refEntityKey').get('scope').value"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
@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>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="tb-form-row">
|
||||
<div class="fixed-title-width">{{ 'calculated-fields.time-window' | translate }}</div>
|
||||
<div class="tb-flex no-gap">
|
||||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
|
||||
<input matInput type="number" min="0" name="value" formControlName="timeWindow" placeholder="{{ 'action.set' | translate }}"/>
|
||||
<span class="block pr-2" matSuffix>{{ 'common.suffix.ms' | translate }}</span>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tb-form-row">
|
||||
<div class="fixed-title-width">{{ 'calculated-fields.limit' | translate }}</div>
|
||||
<div class="tb-flex no-gap">
|
||||
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
|
||||
<input matInput type="number" min="0" name="value" formControlName="limit" placeholder="{{ 'action.set' | translate }}"/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tb-flex flex-end">
|
||||
<button mat-button
|
||||
color="primary"
|
||||
type="button"
|
||||
(click)="cancel()">
|
||||
{{ 'action.cancel' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
(click)="saveArgument()"
|
||||
[disabled]="argumentFormGroup.invalid || !argumentFormGroup.dirty">
|
||||
{{ buttonTitle | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,201 @@
|
||||
///
|
||||
/// 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, ElementRef, Input, OnInit, output, ViewChild } 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 { noLeadTrailSpacesRegex } from '@shared/models/regex.constants';
|
||||
import {
|
||||
ArgumentEntityType,
|
||||
ArgumentEntityTypeTranslations,
|
||||
ArgumentType,
|
||||
ArgumentTypeTranslations,
|
||||
CalculatedFieldArgumentValue,
|
||||
CalculatedFieldType
|
||||
} from '@shared/models/calculated-field.models';
|
||||
import { debounceTime, delay, distinctUntilChanged, filter, map, startWith } 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';
|
||||
import { EntityId } from '@shared/models/id/entity-id';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { EntityFilter } from '@shared/models/query/query.models';
|
||||
import { AliasFilterType } from '@shared/models/alias.models';
|
||||
import { merge } from 'rxjs';
|
||||
|
||||
@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 {
|
||||
|
||||
@Input() popover: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>;
|
||||
@Input() buttonTitle: string;
|
||||
@Input() index: number;
|
||||
@Input() argument: CalculatedFieldArgumentValue;
|
||||
@Input() entityId: EntityId;
|
||||
@Input() tenantId: string;
|
||||
@Input() calculatedFieldType: CalculatedFieldType;
|
||||
|
||||
@ViewChild('timeseriesInput') timeseriesInput: ElementRef;
|
||||
|
||||
argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>();
|
||||
|
||||
argumentFormGroup = this.fb.group({
|
||||
argumentName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex), Validators.maxLength(255)]],
|
||||
refEntityId: this.fb.group({
|
||||
entityType: [ArgumentEntityType.Current],
|
||||
id: ['']
|
||||
}),
|
||||
refEntityKey: this.fb.group({
|
||||
type: [ArgumentType.LatestTelemetry, [Validators.required]],
|
||||
key: [''],
|
||||
scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }],
|
||||
}),
|
||||
defaultValue: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
|
||||
limit: [null],
|
||||
timeWindow: [null],
|
||||
});
|
||||
|
||||
argumentTypes: ArgumentType[];
|
||||
entityFilter: EntityFilter;
|
||||
keyEntityType$ = this.refEntityIdFormGroup.get('entityType').valueChanges
|
||||
.pipe(
|
||||
startWith(this.refEntityIdFormGroup.get('entityType').value),
|
||||
map(type => type === ArgumentEntityType.Current ? this.entityId.entityType : type)
|
||||
);
|
||||
|
||||
readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[];
|
||||
readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations;
|
||||
readonly ArgumentType = ArgumentType;
|
||||
readonly DataKeyType = DataKeyType;
|
||||
readonly EntityType = EntityType;
|
||||
readonly datasourceType = DatasourceType;
|
||||
readonly ArgumentTypeTranslations = ArgumentTypeTranslations;
|
||||
readonly AttributeScope = AttributeScope;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.observeEntityFilterChanges();
|
||||
this.observeEntityTypeChanges()
|
||||
this.observeEntityKeyChanges();
|
||||
}
|
||||
|
||||
get entityType(): ArgumentEntityType {
|
||||
return this.argumentFormGroup.get('refEntityId').get('entityType').value;
|
||||
}
|
||||
|
||||
get refEntityIdFormGroup(): FormGroup {
|
||||
return this.argumentFormGroup.get('refEntityId') as FormGroup;
|
||||
}
|
||||
|
||||
get refEntityKeyFormGroup(): FormGroup {
|
||||
return this.argumentFormGroup.get('refEntityKey') as FormGroup;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.argumentFormGroup.patchValue(this.argument, {emitEvent: false});
|
||||
this.updateEntityFilter(this.argument.refEntityId?.entityType, true);
|
||||
this.toggleByEntityKeyType(this.argument.refEntityKey?.type);
|
||||
this.setInitialEntityKeyType();
|
||||
|
||||
this.argumentTypes = Object.values(ArgumentType)
|
||||
.filter(type => type !== ArgumentType.Rolling || this.calculatedFieldType === CalculatedFieldType.SCRIPT);
|
||||
}
|
||||
|
||||
saveArgument(): void {
|
||||
this.argumentsDataApplied.emit({ value: this.argumentFormGroup.value as CalculatedFieldArgumentValue, index: this.index });
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.popover.hide();
|
||||
}
|
||||
|
||||
private toggleByEntityKeyType(type: ArgumentType): void {
|
||||
const isAttribute = type === ArgumentType.Attribute;
|
||||
const isRolling = type === ArgumentType.Rolling;
|
||||
this.argumentFormGroup.get('refEntityKey').get('scope')[isAttribute? 'enable' : 'disable']({ emitEvent: false });
|
||||
this.argumentFormGroup.get('limit')[isRolling? 'enable' : 'disable']({ emitEvent: false });
|
||||
this.argumentFormGroup.get('timeWindow')[isRolling? 'enable' : 'disable']({ emitEvent: false });
|
||||
this.argumentFormGroup.get('defaultValue')[isRolling? 'disable' : 'enable']({ emitEvent: false });
|
||||
}
|
||||
|
||||
private updateEntityFilter(entityType: ArgumentEntityType, onInit = false): void {
|
||||
let entityId: EntityId;
|
||||
switch (entityType) {
|
||||
case ArgumentEntityType.Current:
|
||||
entityId = this.entityId
|
||||
break;
|
||||
case ArgumentEntityType.Tenant:
|
||||
entityId = {
|
||||
id: this.tenantId,
|
||||
entityType: EntityType.TENANT
|
||||
};
|
||||
break;
|
||||
default:
|
||||
entityId = this.argumentFormGroup.get('refEntityId').value as any;
|
||||
}
|
||||
if (onInit) {
|
||||
this.argumentFormGroup.get('refEntityKey').get('key').setValue('');
|
||||
}
|
||||
this.entityFilter = {
|
||||
type: AliasFilterType.singleEntity,
|
||||
singleEntity: entityId,
|
||||
};
|
||||
}
|
||||
|
||||
private observeEntityFilterChanges(): void {
|
||||
merge(
|
||||
this.argumentFormGroup.get('refEntityId').get('entityType').valueChanges,
|
||||
this.argumentFormGroup.get('refEntityId').get('id').valueChanges.pipe(filter(Boolean)),
|
||||
this.argumentFormGroup.get('refEntityKey').get('scope').valueChanges,
|
||||
)
|
||||
.pipe(debounceTime(300), delay(50), takeUntilDestroyed())
|
||||
.subscribe(() => this.updateEntityFilter(this.entityType));
|
||||
}
|
||||
|
||||
private observeEntityTypeChanges(): void {
|
||||
this.argumentFormGroup.get('refEntityId').get('entityType').valueChanges
|
||||
.pipe(distinctUntilChanged(), takeUntilDestroyed())
|
||||
.subscribe(type => {
|
||||
this.argumentFormGroup.get('refEntityId').get('id').setValue('');
|
||||
this.argumentFormGroup.get('refEntityId')
|
||||
.get('id')[type === ArgumentEntityType.Tenant || type === ArgumentEntityType.Current ? 'disable' : 'enable']();
|
||||
});
|
||||
}
|
||||
|
||||
private observeEntityKeyChanges(): void {
|
||||
this.argumentFormGroup.get('refEntityKey').get('type').valueChanges
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(type => this.toggleByEntityKeyType(type));
|
||||
}
|
||||
|
||||
private setInitialEntityKeyType(): void {
|
||||
if (this.calculatedFieldType === CalculatedFieldType.SIMPLE && this.argument.refEntityKey?.type === ArgumentType.Rolling) {
|
||||
const typeControl = this.argumentFormGroup.get('refEntityKey').get('type');
|
||||
typeControl.setValue(null);
|
||||
typeControl.markAsTouched();
|
||||
typeControl.updateValueAndValidity();
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly ArgumentEntityType = ArgumentEntityType;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
|
||||
export * from './dialog/calculated-field-dialog.component';
|
||||
export * from './arguments-table/calculated-field-arguments-table.component';
|
||||
@ -185,6 +185,16 @@ import { EntityChipsComponent } from '@home/components/entity/entity-chips.compo
|
||||
import { DashboardViewComponent } from '@home/components/dashboard-view/dashboard-view.component';
|
||||
import { CalculatedFieldsTableComponent } from '@home/components/calculated-fields/calculated-fields-table.component';
|
||||
import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
|
||||
import { CalculatedFieldDialogComponent } from '@home/components/calculated-fields/components/dialog/calculated-field-dialog.component';
|
||||
import {
|
||||
EntityDebugSettingsButtonComponent
|
||||
} from '@home/components/entity/debug/entity-debug-settings-button.component';
|
||||
import {
|
||||
CalculatedFieldArgumentsTableComponent
|
||||
} from '@home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component';
|
||||
import {
|
||||
CalculatedFieldArgumentPanelComponent
|
||||
} from '@home/components/calculated-fields/components/panel/calculated-field-argument-panel.component';
|
||||
|
||||
@NgModule({
|
||||
declarations:
|
||||
@ -330,6 +340,9 @@ import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
|
||||
EntityChipsComponent,
|
||||
DashboardViewComponent,
|
||||
CalculatedFieldsTableComponent,
|
||||
CalculatedFieldDialogComponent,
|
||||
CalculatedFieldArgumentsTableComponent,
|
||||
CalculatedFieldArgumentPanelComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -341,7 +354,8 @@ import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
|
||||
SnmpDeviceProfileTransportModule,
|
||||
StatesControllerModule,
|
||||
DeviceCredentialsModule,
|
||||
DeviceProfileCommonModule
|
||||
DeviceProfileCommonModule,
|
||||
EntityDebugSettingsButtonComponent
|
||||
],
|
||||
exports: [
|
||||
RouterTabsComponent,
|
||||
@ -468,6 +482,9 @@ import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe';
|
||||
EntityChipsComponent,
|
||||
DashboardViewComponent,
|
||||
CalculatedFieldsTableComponent,
|
||||
CalculatedFieldDialogComponent,
|
||||
CalculatedFieldArgumentsTableComponent,
|
||||
CalculatedFieldArgumentPanelComponent,
|
||||
],
|
||||
providers: [
|
||||
WidgetComponentService,
|
||||
|
||||
@ -19,6 +19,7 @@ import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { DeviceInfo } from '@shared/models/device.models';
|
||||
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
|
||||
import { EntityType } from '@shared/models/entity-type.models';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-device-tabs',
|
||||
@ -27,6 +28,8 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone
|
||||
})
|
||||
export class DeviceTabsComponent extends EntityTabsComponent<DeviceInfo> {
|
||||
|
||||
readonly EntityType = EntityType;
|
||||
|
||||
constructor(protected store: Store<AppState>) {
|
||||
super(store);
|
||||
}
|
||||
@ -34,5 +37,4 @@ export class DeviceTabsComponent extends EntityTabsComponent<DeviceInfo> {
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -17,10 +17,11 @@
|
||||
-->
|
||||
<mat-form-field [formGroup]="selectEntityFormGroup" class="mat-block" [appearance]="appearance" [subscriptSizing]="subscriptSizing"
|
||||
[class]="additionalClasses">
|
||||
<mat-label>{{ label | translate }}</mat-label>
|
||||
<mat-label *ngIf="withLabel">{{ label | translate }}</mat-label>
|
||||
<input matInput type="text"
|
||||
#entityInput
|
||||
formControlName="entity"
|
||||
[placeholder]="placeholder"
|
||||
(focusin)="onFocus()"
|
||||
[required]="required"
|
||||
[matAutocomplete]="entityAutocomplete"
|
||||
@ -28,6 +29,14 @@
|
||||
<a *ngIf="selectEntityFormGroup.get('entity').value && disabled" aria-label="Open device profile" [routerLink]=entityURL>
|
||||
{{ displayEntityFn(selectEntityFormGroup.get('entity').value) }}
|
||||
</a>
|
||||
<mat-icon *ngIf="selectEntityFormGroup.get('entity').hasError('required') && iconError"
|
||||
matSuffix
|
||||
matTooltipPosition="above"
|
||||
matTooltipClass="tb-error-tooltip"
|
||||
[matTooltip]="requiredErrorText | translate"
|
||||
class="tb-error">
|
||||
warning
|
||||
</mat-icon>
|
||||
<button *ngIf="selectEntityFormGroup.get('entity').value && !disabled"
|
||||
type="button"
|
||||
matSuffix mat-icon-button aria-label="Clear"
|
||||
@ -59,7 +68,7 @@
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
<mat-error *ngIf="selectEntityFormGroup.get('entity').hasError('required')">
|
||||
<mat-error *ngIf="selectEntityFormGroup.get('entity').hasError('required') && !iconError">
|
||||
{{ requiredErrorText | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
|
||||
import {
|
||||
AfterViewInit,
|
||||
booleanAttribute,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
@ -135,6 +136,12 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
|
||||
@coerceBoolean()
|
||||
allowCreateNew: boolean;
|
||||
|
||||
@Input({ transform: booleanAttribute }) withLabel = true;
|
||||
|
||||
@Input({ transform: booleanAttribute }) iconError = false;
|
||||
|
||||
@Input() placeholder: string;
|
||||
|
||||
@Input()
|
||||
subscriptSizing: SubscriptSizing = 'fixed';
|
||||
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<mat-form-field class="tb-flex no-gap !w-full" appearance="outline" subscriptSizing="dynamic">
|
||||
<input matInput type="text" placeholder="{{ 'entity.key-name' | translate }}"
|
||||
#keyInput
|
||||
[formControl]="keyControl"
|
||||
required
|
||||
(focusin)="keyInputSubject.next()"
|
||||
[matAutocomplete]="keysAutocomplete">
|
||||
@if (keyControl.value) {
|
||||
<button type="button"
|
||||
matSuffix mat-icon-button aria-label="Clear"
|
||||
(click)="clear()">
|
||||
<mat-icon class="material-icons">close</mat-icon>
|
||||
</button>
|
||||
}
|
||||
<mat-autocomplete
|
||||
class="tb-autocomplete"
|
||||
#keysAutocomplete="matAutocomplete">
|
||||
@for (key of filteredKeys$ | async; track key) {
|
||||
<mat-option [value]="key"><span [innerHTML]="key | highlight: searchText"></span></mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
@ -0,0 +1,134 @@
|
||||
///
|
||||
/// 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, effect, ElementRef, forwardRef, input, ViewChild, } from '@angular/core';
|
||||
import {
|
||||
ControlValueAccessor,
|
||||
FormBuilder,
|
||||
NG_VALIDATORS,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ValidationErrors,
|
||||
Validator,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { map, startWith, switchMap } from 'rxjs/operators';
|
||||
import { combineLatest, of, Subject } from 'rxjs';
|
||||
import { EntityService } from '@core/http/entity.service';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-entity-key-autocomplete',
|
||||
templateUrl: './entity-key-autocomplete.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => EntityKeyAutocompleteComponent),
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: NG_VALIDATORS,
|
||||
useExisting: forwardRef(() => EntityKeyAutocompleteComponent),
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
host: {
|
||||
class: 'w-full'
|
||||
}
|
||||
})
|
||||
export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Validator {
|
||||
|
||||
@ViewChild('keyInput') keyInput: ElementRef;
|
||||
|
||||
entityFilter = input.required<EntityFilter>();
|
||||
dataKeyType = input.required<DataKeyType>();
|
||||
keyScopeType = input<AttributeScope>();
|
||||
|
||||
keyControl = this.fb.control('', [Validators.required]);
|
||||
searchText = '';
|
||||
keyInputSubject = new Subject<void>();
|
||||
|
||||
private onChange: (value: string) => void;
|
||||
private cachedResult: EntitiesKeysByQuery;
|
||||
|
||||
keys$ = this.keyInputSubject.asObservable()
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
return this.cachedResult ? of(this.cachedResult) : this.entityService.findEntityKeysByQuery({
|
||||
pageLink: { page: 0, pageSize: 100 },
|
||||
entityFilter: this.entityFilter(),
|
||||
}, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType());
|
||||
}),
|
||||
map(result => {
|
||||
this.cachedResult = result;
|
||||
switch (this.dataKeyType()) {
|
||||
case DataKeyType.attribute:
|
||||
return result.attribute;
|
||||
case DataKeyType.timeseries:
|
||||
return result.timeseries;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
filteredKeys$ = combineLatest([this.keys$, this.keyControl.valueChanges.pipe(startWith(''))])
|
||||
.pipe(
|
||||
map(([keys, searchText = '']) => {
|
||||
this.searchText = searchText;
|
||||
return searchText ? keys.filter(item => item.toLowerCase().includes(searchText.toLowerCase())) : keys;
|
||||
})
|
||||
);
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private entityService: EntityService,
|
||||
) {
|
||||
this.keyControl.valueChanges
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(value => this.onChange(value));
|
||||
effect(() => {
|
||||
if (this.keyScopeType() || this.entityFilter() && this.dataKeyType()) {
|
||||
this.cachedResult = null;
|
||||
this.searchText = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.keyControl.patchValue('', {emitEvent: true});
|
||||
setTimeout(() => {
|
||||
this.keyInput.nativeElement.blur();
|
||||
this.keyInput.nativeElement.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
registerOnChange(onChange: (value: string) => void): void {
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
registerOnTouched(_): void {}
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
return this.keyControl.valid ? null : { keyControl: false };
|
||||
}
|
||||
|
||||
writeValue(value: string): void {
|
||||
this.keyControl.patchValue(value, {emitEvent: false});
|
||||
}
|
||||
}
|
||||
@ -20,9 +20,11 @@ import {
|
||||
ElementRef,
|
||||
forwardRef,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Renderer2,
|
||||
SimpleChanges,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
ViewEncapsulation
|
||||
@ -67,7 +69,7 @@ import { catchError } from 'rxjs/operators';
|
||||
],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator {
|
||||
export class JsFuncComponent implements OnInit, OnChanges, OnDestroy, ControlValueAccessor, Validator {
|
||||
|
||||
@ViewChild('javascriptEditor', {static: true})
|
||||
javascriptEditorElmRef: ElementRef;
|
||||
@ -177,6 +179,13 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
|
||||
private http: HttpClient) {
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.functionArgs) {
|
||||
this.updateFunctionArgsString();
|
||||
this.updateFunctionLabel();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.functionTitle || this.label) {
|
||||
this.hideBrackets = true;
|
||||
@ -184,22 +193,6 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
|
||||
if (!this.resultType || this.resultType.length === 0) {
|
||||
this.resultType = 'nocheck';
|
||||
}
|
||||
if (this.functionArgs) {
|
||||
this.functionArgs.forEach((functionArg) => {
|
||||
if (this.functionArgsString.length > 0) {
|
||||
this.functionArgsString += ', ';
|
||||
}
|
||||
this.functionArgsString += functionArg;
|
||||
});
|
||||
}
|
||||
if (this.functionTitle) {
|
||||
this.functionLabel = `${this.functionTitle}: f(${this.functionArgsString})`;
|
||||
} else if (this.label) {
|
||||
this.functionLabel = this.label;
|
||||
} else {
|
||||
this.functionLabel =
|
||||
`function ${this.functionName ? this.functionName : ''}(${this.functionArgsString})${this.hideBrackets ? '' : ' {'}`;
|
||||
}
|
||||
const editorElement = this.javascriptEditorElmRef.nativeElement;
|
||||
let editorOptions: Partial<Ace.EditorOptions> = {
|
||||
mode: 'ace/mode/javascript',
|
||||
@ -329,6 +322,25 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
|
||||
);
|
||||
}
|
||||
|
||||
private updateFunctionArgsString(): void {
|
||||
this.functionArgsString = '';
|
||||
if (this.functionArgs) {
|
||||
this.functionArgsString = this.functionArgs.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
private updateFunctionLabel(): void {
|
||||
if (this.functionTitle) {
|
||||
this.functionLabel = `${this.functionTitle}: f(${this.functionArgsString})`;
|
||||
} else if (this.label) {
|
||||
this.functionLabel = this.label;
|
||||
} else {
|
||||
this.functionLabel =
|
||||
`function ${this.functionName ? this.functionName : ''}(${this.functionArgsString})${this.hideBrackets ? '' : ' {'}`;
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
validateOnSubmit(): Observable<void> {
|
||||
if (!this.disabled) {
|
||||
this.cleanupJsErrors();
|
||||
|
||||
@ -13,29 +13,109 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import { EntityDebugSettings, HasTenantId, HasVersion } from '@shared/models/entity.models';
|
||||
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';
|
||||
|
||||
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId {
|
||||
type: CalculatedFieldType;
|
||||
debugSettings?: EntityDebugSettings;
|
||||
externalId?: string;
|
||||
configuration: CalculatedFieldConfiguration;
|
||||
type: CalculatedFieldType;
|
||||
}
|
||||
|
||||
export enum CalculatedFieldType {
|
||||
SIMPLE = 'SIMPLE',
|
||||
COMPLEX = 'COMPLEX',
|
||||
}
|
||||
|
||||
export interface CalculatedFieldConfiguration {
|
||||
type: CalculatedFieldConfigType;
|
||||
expression: string;
|
||||
arguments: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export enum CalculatedFieldConfigType {
|
||||
SIMPLE = 'SIMPLE',
|
||||
SCRIPT = 'SCRIPT',
|
||||
}
|
||||
|
||||
export const CalculatedFieldTypeTranslations = new Map<CalculatedFieldType, string>(
|
||||
[
|
||||
[CalculatedFieldType.SIMPLE, 'calculated-fields.type.simple'],
|
||||
[CalculatedFieldType.SCRIPT, 'calculated-fields.type.script'],
|
||||
]
|
||||
)
|
||||
|
||||
export interface CalculatedFieldConfiguration {
|
||||
type: CalculatedFieldType;
|
||||
expression: string;
|
||||
arguments: Record<string, CalculatedFieldArgument>;
|
||||
}
|
||||
|
||||
export enum ArgumentEntityType {
|
||||
Current = 'CURRENT',
|
||||
Device = 'DEVICE',
|
||||
Asset = 'ASSET',
|
||||
Customer = 'CUSTOMER',
|
||||
Tenant = 'TENANT',
|
||||
}
|
||||
|
||||
export const ArgumentEntityTypeTranslations = new Map<ArgumentEntityType, string>(
|
||||
[
|
||||
[ArgumentEntityType.Current, 'calculated-fields.argument-current'],
|
||||
[ArgumentEntityType.Device, 'calculated-fields.argument-device'],
|
||||
[ArgumentEntityType.Asset, 'calculated-fields.argument-asset'],
|
||||
[ArgumentEntityType.Customer, 'calculated-fields.argument-customer'],
|
||||
[ArgumentEntityType.Tenant, 'calculated-fields.argument-tenant'],
|
||||
]
|
||||
)
|
||||
|
||||
export enum ArgumentType {
|
||||
Attribute = 'ATTRIBUTE',
|
||||
LatestTelemetry = 'TS_LATEST',
|
||||
Rolling = 'TS_ROLLING',
|
||||
}
|
||||
|
||||
export enum OutputType {
|
||||
Attribute = 'ATTRIBUTES',
|
||||
Timeseries = 'TIME_SERIES',
|
||||
}
|
||||
|
||||
export const OutputTypeTranslations = new Map<OutputType, string>(
|
||||
[
|
||||
[OutputType.Attribute, 'calculated-fields.attribute'],
|
||||
[OutputType.Timeseries, 'calculated-fields.timeseries'],
|
||||
]
|
||||
)
|
||||
|
||||
export const ArgumentTypeTranslations = new Map<ArgumentType, string>(
|
||||
[
|
||||
[ArgumentType.Attribute, 'calculated-fields.attribute'],
|
||||
[ArgumentType.LatestTelemetry, 'calculated-fields.latest-telemetry'],
|
||||
[ArgumentType.Rolling, 'calculated-fields.rolling'],
|
||||
]
|
||||
)
|
||||
|
||||
export interface CalculatedFieldArgument {
|
||||
refEntityKey: RefEntityKey;
|
||||
defaultValue?: string;
|
||||
refEntityId?: RefEntityKey;
|
||||
limit?: number;
|
||||
timeWindow?: number;
|
||||
}
|
||||
|
||||
export interface RefEntityKey {
|
||||
key: string;
|
||||
type: ArgumentType;
|
||||
scope?: AttributeScope;
|
||||
}
|
||||
|
||||
export interface RefEntityKey {
|
||||
entityType: ArgumentEntityType;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument {
|
||||
argumentName: string;
|
||||
}
|
||||
|
||||
export interface CalculatedFieldDialogData {
|
||||
value: CalculatedField;
|
||||
buttonTitle: string;
|
||||
entityId: EntityId;
|
||||
debugLimitsConfiguration: string;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
@ -61,3 +61,4 @@ export * from './widgets-bundle.model';
|
||||
export * from './window-message.model';
|
||||
export * from './usage.models';
|
||||
export * from './query/query.models';
|
||||
export * from './regex.constants';
|
||||
|
||||
17
ui-ngx/src/app/shared/models/regex.constants.ts
Normal file
17
ui-ngx/src/app/shared/models/regex.constants.ts
Normal file
@ -0,0 +1,17 @@
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
|
||||
export const noLeadTrailSpacesRegex = /^\S+(?: \S+)*$/;
|
||||
@ -224,6 +224,7 @@ import { IntervalOptionsConfigPanelComponent } from '@shared/components/time/int
|
||||
import { GroupingIntervalOptionsComponent } from '@shared/components/time/aggregation/grouping-interval-options.component';
|
||||
import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component';
|
||||
import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row.component';
|
||||
import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component';
|
||||
|
||||
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
|
||||
return markedOptionsService;
|
||||
@ -432,7 +433,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
ImageGalleryDialogComponent,
|
||||
WidgetButtonComponent,
|
||||
HexInputComponent,
|
||||
ScadaSymbolInputComponent
|
||||
ScadaSymbolInputComponent,
|
||||
EntityKeyAutocompleteComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -694,7 +696,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
|
||||
EmbedImageDialogComponent,
|
||||
ImageGalleryDialogComponent,
|
||||
WidgetButtonComponent,
|
||||
ScadaSymbolInputComponent
|
||||
ScadaSymbolInputComponent,
|
||||
EntityKeyAutocompleteComponent,
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
|
||||
@ -996,6 +996,7 @@
|
||||
"failures": "Failures",
|
||||
"entity": "entity",
|
||||
"rule-node": "rule node",
|
||||
"calculated-field": "calculated field",
|
||||
"hint": {
|
||||
"main": "All node debug messages rate limited with:",
|
||||
"main-limited": "All {{entity}} debug messages will be rate-limited, with a maximum of {{msg}} messages allowed per {{time}}.",
|
||||
@ -1007,7 +1008,56 @@
|
||||
"expression": "Expression",
|
||||
"no-found": "No calculated fields found",
|
||||
"list": "{ count, plural, =1 {One calculated field} other {List of # calculated fields} }",
|
||||
"selected-fields": "{ count, plural, =1 {1 calculated field} other {# calculated fields} } selected"
|
||||
"selected-fields": "{ count, plural, =1 {1 calculated field} other {# calculated fields} } selected",
|
||||
"type": {
|
||||
"simple": "Simple",
|
||||
"script": "Script"
|
||||
},
|
||||
"arguments": "Arguments",
|
||||
"argument-name": "Argument name",
|
||||
"datasource": "Datasource",
|
||||
"add-argument": "Add argument",
|
||||
"no-arguments": "No arguments configured",
|
||||
"argument-settings": "Argument settings",
|
||||
"argument-current": "Current entity",
|
||||
"argument-current-tenant": "Current tenant",
|
||||
"argument-device": "Device",
|
||||
"argument-asset": "Asset",
|
||||
"argument-customer": "Customer",
|
||||
"argument-tenant": "Current tenant",
|
||||
"argument-type": "Argument type",
|
||||
"attribute": "Attribute",
|
||||
"timeseries-key": "Time series key",
|
||||
"device-name": "Device name",
|
||||
"latest-telemetry": "Latest telemetry",
|
||||
"rolling": "Rolling",
|
||||
"attribute-scope": "Attribute scope",
|
||||
"server-attributes": "Server attributes",
|
||||
"client-attributes": "Client attributes",
|
||||
"shared-attributes": "Shared attributes",
|
||||
"attribute-key": "Attribute key",
|
||||
"default-value": "Default value",
|
||||
"limit": "Limit",
|
||||
"time-window": "Time window",
|
||||
"customer-name": "Customer name",
|
||||
"timeseries": "Time series",
|
||||
"output": "Output",
|
||||
"output-type": "Output type",
|
||||
"delete-title": "Are you sure you want to delete the calculated field '{{title}}'?",
|
||||
"delete-text": "Be careful, after the confirmation the calculated field and all related data will become unrecoverable.",
|
||||
"delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 calculated field} other {# calculated fields} }?",
|
||||
"delete-multiple-text": "Be careful, after the confirmation all selected calculated fields will be removed and all related data will become unrecoverable.",
|
||||
"hint": {
|
||||
"arguments-simple-with-rolling": "Simple type calculated field should not contain keys with rolling type.",
|
||||
"arguments-empty": "Arguments should not be empty.",
|
||||
"expression-required": "Expression is required.",
|
||||
"expression-invalid": "Expression is invalid",
|
||||
"expression-max-length": "Expression length should be less than 255 characters.",
|
||||
"argument-name-required": "Argument name is required.",
|
||||
"argument-name-pattern": "Argument name is invalid.",
|
||||
"argument-name-max-length": "Argument name should be less than 256 characters.",
|
||||
"argument-type-required": "Argument type is required."
|
||||
}
|
||||
},
|
||||
"confirm-on-exit": {
|
||||
"message": "You have unsaved changes. Are you sure you want to leave this page?",
|
||||
@ -1035,6 +1085,7 @@
|
||||
"common": {
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"general": "General",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"enter-username": "Enter username",
|
||||
@ -1047,7 +1098,19 @@
|
||||
"open-details-page": "Open details page",
|
||||
"not-found": "Not found",
|
||||
"documentation": "Documentation",
|
||||
"time-left": "{{time}} left"
|
||||
"time-left": "{{time}} left",
|
||||
"suffix": {
|
||||
"s": "s",
|
||||
"ms": "ms"
|
||||
},
|
||||
"hint": {
|
||||
"name-required": "Name is required.",
|
||||
"name-pattern": "Name is invalid.",
|
||||
"name-max-length": "Name 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."
|
||||
}
|
||||
},
|
||||
"content-type": {
|
||||
"json": "Json",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user