Merge pull request #12719 from maxunbearable/feature/calculated-fields-adjustments-19-02

Calculated fields adjustments
This commit is contained in:
Vladyslav Prykhodko 2025-02-24 12:42:07 +02:00 committed by GitHub
commit 4dad2bbc53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 126 additions and 47 deletions

View File

@ -44,6 +44,7 @@ import {
CalculatedFieldTestScriptDialogData,
getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights,
CalculatedFieldTypeTranslations,
} from '@shared/models/calculated-field.models';
import {
CalculatedFieldDebugDialogComponent,
@ -112,7 +113,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
expressionColumn.sortable = false;
this.columns.push(new EntityTableColumn<CalculatedField>('name', 'common.name', '33%'));
this.columns.push(new EntityTableColumn<CalculatedField>('type', 'common.type', '50px'));
this.columns.push(new EntityTableColumn<CalculatedField>('type', 'common.type', '50px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type))));
this.columns.push(expressionColumn);
this.cellActionDescriptors.push(

View File

@ -73,7 +73,7 @@
</mat-select>
}
</mat-form-field>
<mat-chip-listbox formControlName="key" class="tb-inline-field w-1/6 xs:w-1/3">
<mat-chip-listbox formControlName="key" class="tb-inline-field entity-key-field w-1/6 xs:w-1/3">
<mat-chip>
<div tbTruncateWithTooltip>
{{ group.get('refEntityKey').get('key').value }}

View File

@ -16,6 +16,10 @@
@use '../../../../../../../scss/constants' as constants;
:host {
.entity-key-field {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.tb-form-table-row-cell-buttons {
--mat-badge-legacy-small-size-container-size: 8px;
--mat-badge-small-size-container-overlap-offset: -5px;

View File

@ -74,10 +74,10 @@
[calculatedFieldType]="fieldFormGroup.get('type').value"
/>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel no-gap">
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.expression' | translate }}</div>
@if (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE) {
<mat-form-field class="mat-block" appearance="outline">
<mat-form-field class="mt-3" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="expressionSIMPLE" maxlength="255" [placeholder]="'action.set' | translate" required>
@if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) {
<mat-error>
@ -104,12 +104,13 @@
[editorCompleter]="argumentsEditorCompleter$ | async"
helpId="calculated-field/expression_fn"
>
<div toolbarPrefixButton class="tb-primary-background script-lang-chip">{{ 'api-usage.tbel' | translate }}</div>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'common.test-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="configFormGroup.get('expressionSCRIPT').invalid || configFormGroup.get('arguments').invalid"
[disabled]="configFormGroup.get('arguments').invalid"
(click)="onTestScript()">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
@ -118,7 +119,7 @@
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
[disabled]="configFormGroup.get('expressionSCRIPT').invalid || configFormGroup.get('arguments').invalid">
[disabled]="configFormGroup.get('arguments').invalid">
{{ 'common.test-function' | translate }}
</button>
</div>

View File

@ -19,6 +19,19 @@
width: 869px;
max-width: 100%;
}
.script-lang-chip {
line-height: 20px;
font-size: 14px;
font-weight: 500;
color: white;
border-radius: 100px;
width: 70px;
display: flex;
justify-content: center;
margin-top: 2px;
margin-right: 4px;
}
}
:host ::ng-deep {

View File

@ -162,13 +162,13 @@
<tb-timeinterval
subscriptSizing="dynamic"
appearance="outline"
class="flex-1"
class="time-interval-field flex-1"
formControlName="timeWindow"
/>
</div>
<div class="tb-form-row">
<div class="tb-form-row limit-field-row">
<div class="fixed-title-width">{{ 'calculated-fields.limit' | translate }}</div>
<tb-datapoints-limit class="flex-1" formControlName="limit"/>
<tb-datapoints-limit class="w-full flex-1" formControlName="limit"/>
</div>
}
</div>

View File

@ -15,14 +15,37 @@
*/
@use '../../../../../../../scss/constants' as constants;
$panel-width: 520px;
:host {
display: flex;
width: 520px;
width: $panel-width;
max-width: 100%;
max-height: 100vh;
.fixed-title-width {
@media #{constants.$mat-lt-sm} {
@media #{constants.$mat-xs} {
min-width: 120px;
}
}
}
:host ::ng-deep {
.limit-field-row {
@media screen and (max-width: $panel-width) {
display: flex;
flex-direction: column;
.fixed-title-width {
align-self: flex-start;
padding-top: 8px;
}
}
}
.time-interval-field {
.advanced-input {
flex-direction: column;
}
}
}

View File

@ -28,7 +28,7 @@ import {
CalculatedFieldType,
getCalculatedFieldCurrentEntityFilter
} from '@shared/models/calculated-field.models';
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
import { debounceTime, delay, 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';
@ -38,6 +38,7 @@ import { EntityFilter } from '@shared/models/query/query.models';
import { AliasFilterType } from '@shared/models/alias.models';
import { merge } from 'rxjs';
import { MINUTE } from '@shared/models/time/time.models';
import { TimeService } from '@core/services/time.service';
@Component({
selector: 'tb-calculated-field-argument-panel',
@ -57,6 +58,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
argumentsDataApplied = output<{ value: CalculatedFieldArgumentValue, index: number }>();
readonly defaultLimit = Math.max(this.timeService.getMinDatapointsLimit(), Math.floor(this.timeService.getMaxDatapointsLimit() / 10));
argumentFormGroup = this.fb.group({
argumentName: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]],
refEntityId: this.fb.group({
@ -69,7 +72,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
scope: [{ value: AttributeScope.SERVER_SCOPE, disabled: true }, [Validators.required]],
}),
defaultValue: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
limit: [1000],
limit: [this.defaultLimit],
timeWindow: [MINUTE * 15],
});
@ -92,11 +95,13 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
constructor(
private fb: FormBuilder,
private cd: ChangeDetectorRef,
private popover: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>
private popover: TbPopoverComponent<CalculatedFieldArgumentPanelComponent>,
private timeService: TimeService
) {
this.observeEntityFilterChanges();
this.observeEntityTypeChanges()
this.observeEntityKeyChanges();
this.observeUpdatePosition();
}
get entityType(): ArgumentEntityType {
@ -224,4 +229,15 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit {
typeControl.markAsTouched();
}
}
private observeUpdatePosition(): void {
merge(
this.refEntityIdFormGroup.get('entityType').valueChanges,
this.refEntityKeyFormGroup.get('type').valueChanges,
this.argumentFormGroup.get('timeWindow').valueChanges,
this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)),
)
.pipe(delay(50), takeUntilDestroyed())
.subscribe(() => this.popover.updatePosition());
}
}

View File

@ -39,11 +39,11 @@
<div class="flex flex-1 items-center gap-2">
@if (argumentsTypeMap.get(group.get('argumentName').value) === ArgumentType.Rolling) {
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field flex-1">
<input matInput tb-json-to-string name="values" formControlName="values" placeholder="{{ 'value.json-value' | translate }}*"/>
<input matInput tb-json-to-string name="rollingJson" formControlName="rollingJson" placeholder="{{ 'value.json-value' | translate }}"/>
</mat-form-field>
} @else {
<mat-form-field appearance="outline" class="tb-inline-field w-1/3" subscriptSizing="dynamic">
<input matInput formControlName="ts" type="number" placeholder="{{ 'action.set' | translate }}">
<input matInput formControlName="ts" type="number" placeholder="{{ 'common.timestamp' | translate }}">
</mat-form-field>
<tb-value-input class="argument-value min-w-60 flex-1" [required]="false" [hideJsonEdit]="true" [shortBooleanField]="true" formControlName="value"/>
}

View File

@ -110,14 +110,14 @@ export class CalculatedFieldTestArgumentsComponent extends PageComponent impleme
minWidth: 'min(700px, 100%)',
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: group.value,
jsonValue: this.argumentsTypeMap.get(group.get('argumentName').value) === ArgumentType.Rolling ? group.value.rollingJson : group.value,
required: true,
fillHeight: true
}
}).afterClosed()
.pipe(filter(Boolean))
.subscribe(result => this.argumentsTypeMap.get(group.get('argumentName').value) === ArgumentType.Rolling
? group.patchValue({ timeWindow: (result as CalculatedFieldRollingTelemetryArgumentValue).timeWindow, values: (result as CalculatedFieldRollingTelemetryArgumentValue).values })
? group.get('rollingJson').patchValue({ values: (result as CalculatedFieldRollingTelemetryArgumentValue).values, timeWindow: (result as CalculatedFieldRollingTelemetryArgumentValue).timeWindow })
: group.patchValue({ ts: (result as CalculatedFieldSingleArgumentValue).ts, value: (result as CalculatedFieldSingleArgumentValue).value }) );
}
@ -131,16 +131,15 @@ export class CalculatedFieldTestArgumentsComponent extends PageComponent impleme
private getRollingArgumentFormGroup({ argumentName, timeWindow, values }: CalculatedFieldRollingTelemetryArgumentValue): FormGroup {
return this.fb.group({
timeWindow: [timeWindow ?? {}],
argumentName: [{ value: argumentName, disabled: true }],
values: [values]
rollingJson: [{ values: values ?? [], timeWindow: timeWindow ?? {} }]
}) as FormGroup;
}
private getValue(): CalculatedFieldEventArguments {
return this.argumentsFormArray.getRawValue().reduce((acc, rowItem) => {
const { argumentName, ...value } = rowItem;
acc[argumentName] = value;
const { argumentName, rollingJson = {}, ...value } = rowItem;
acc[argumentName] = { ...rollingJson, ...value };
return acc;
}, {});
}

View File

@ -38,6 +38,7 @@
functionName="calculate"
class="expression-edit"
[functionArgs]="functionArgs"
[required]="true"
[disableUndefinedCheck]="true"
[fillHeight]="true"
[highlightRules]="data.argumentsHighlightRules"

View File

@ -26,7 +26,7 @@ import {
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder } from '@angular/forms';
import { FormBuilder, Validators } from '@angular/forms';
import { NEVER, Observable, of, switchMap } from 'rxjs';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
@ -62,7 +62,7 @@ export class CalculatedFieldScriptTestDialogComponent extends DialogComponent<Ca
@ViewChild('expressionContent', {static: true}) expressionContent: JsonContentComponent;
calculatedFieldScriptTestFormGroup = this.fb.group({
expression: [],
expression: ['', Validators.required],
arguments: [],
output: []
});

View File

@ -183,6 +183,7 @@
[class.lt-lg:!hidden]="column.mobileHide"
*matCellDef="let entity; let row = index"
[matTooltip]="cellTooltip(entity, column, row)"
#cellMatTooltip="matTooltip"
matTooltipPosition="above"
[style]="cellStyle(entity, column, row)">
<ng-container [ngSwitch]="column.type">
@ -208,6 +209,8 @@
[copyText]="column.actionCell.onAction(null, entity)"
tooltipText="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}"
tooltipPosition="above"
(mouseover)="cellMatTooltip.hide()"
(mouseleave)="cellMatTooltip.show()"
[icon]="column.actionCell.icon"
[style]="column.actionCell.style">
</tb-copy-button>

View File

@ -403,7 +403,9 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
new EntityTableColumn<Event>('messageType', 'event.message-type', '100px',
(entity) => entity.body.msgType ?? '-',
() => ({padding: '0 12px 0 0'}),
false
false,
() => ({padding: '0 12px 0 0'}),
(entity) => entity.body.msgType,
),
new EntityActionTableColumn<Event>('arguments', 'event.arguments',
{
@ -539,8 +541,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
case DebugEventType.DEBUG_CALCULATED_FIELD:
this.filterColumns.push(
{key: 'entityId', title: 'event.entity-id'},
{key: 'messageId', title: 'event.message-id'},
{key: 'messageType', title: 'event.message-type'},
{key: 'msgId', title: 'event.message-id'},
{key: 'msgType', title: 'event.message-type'},
{key: 'arguments', title: 'event.arguments'},
{key: 'result', title: 'event.result'},
{key: 'isError', title: 'event.error'},

View File

@ -31,7 +31,12 @@ import {
} from '@home/components/widget/lib/scada/scada-symbol.models';
import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { AceHighlightRule, AceHighlightRules } from '@shared/models/ace/ace.models';
import {
AceHighlightRule,
AceHighlightRules,
dotOperatorHighlightRule,
endGroupHighlightRule
} from '@shared/models/ace/ace.models';
import { HelpLinks, ValueType } from '@shared/models/constants';
import { formPropertyCompletions } from '@shared/models/dynamic-form.models';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
@ -921,17 +926,6 @@ export class ScadaSymbolElement {
const identifierRe = /[a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*/;
const dotOperatorHighlightRule: AceHighlightRule = {
token: 'punctuation.operator',
regex: /[.](?![.])/,
};
const endGroupHighlightRule: AceHighlightRule = {
regex: '',
token: 'empty',
next: 'no_regex'
};
const scadaSymbolCtxObjectHighlightRule: AceHighlightRule = {
token: 'tb.scada-symbol-ctx',
regex: /\bctx\b/,

View File

@ -33,7 +33,7 @@
</mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.STRING" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1">
<input [disabled]="disabled" matInput [required]="required" name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"
placeholder="{{ 'value.string-value' | translate }}*"/>
placeholder="{{ 'value.string-value' | translate }}{{ required ? '*' : ''}}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
@ -45,7 +45,7 @@
</mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.INTEGER" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number flex flex-1">
<input [disabled]="disabled" matInput [required]="required" name="value" type="number" step="1" pattern="^-?[0-9]+$" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"
placeholder="{{ 'value.integer-value' | translate }}*"/>
placeholder="{{ 'value.integer-value' | translate }}{{ required ? '*' : ''}}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
@ -58,7 +58,7 @@
</mat-form-field>
<mat-form-field *ngIf="valueType === valueTypeEnum.DOUBLE" appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute number flex flex-1">
<input [disabled]="disabled" matInput [required]="required" name="value" type="number" step="any" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"
placeholder="{{ 'value.double-value' | translate }}*"/>
placeholder="{{ 'value.double-value' | translate }}{{ required ? '*' : ''}}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
@ -81,7 +81,7 @@
<div *ngIf="valueType === valueTypeEnum.JSON" class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1">
<input [disabled]="disabled" matInput tb-json-to-string [required]="required" name="value" #value="ngModel"
[(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" placeholder="{{ 'value.json-value' | translate }}*"/>
[(ngModel)]="modelValue" (ngModelChange)="onValueChanged()" placeholder="{{ 'value.json-value' | translate }}{{ required ? '*' : ''}}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"

View File

@ -365,5 +365,16 @@ export interface AceHighlightRule {
next?: string;
}
export const dotOperatorHighlightRule: AceHighlightRule = {
token: 'punctuation.operator',
regex: /[.](?![.])/,
};
export const endGroupHighlightRule: AceHighlightRule = {
regex: '',
token: 'empty',
next: 'no_regex'
};

View File

@ -28,7 +28,11 @@ import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType } from '@shared/models/alias.models';
import { Observable } from 'rxjs';
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { AceHighlightRules } from '@shared/models/ace/ace.models';
import {
AceHighlightRules,
dotOperatorHighlightRule,
endGroupHighlightRule
} from '@shared/models/ace/ace.models';
export interface CalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasTenantId, ExportableEntity<CalculatedFieldId> {
debugSettings?: EntityDebugSettings;
@ -337,6 +341,7 @@ export const getCalculatedFieldArgumentsHighlights = (
const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = {
calculatedFieldSingleArgumentValue: [
dotOperatorHighlightRule,
{
token: 'tb.calculated-field-value',
regex: /value/,
@ -346,12 +351,14 @@ const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = {
token: 'tb.calculated-field-ts',
regex: /ts/,
next: 'no_regex'
}
},
endGroupHighlightRule
],
}
const calculatedFieldRollingArgumentValueHighlightRules: AceHighlightRules = {
calculatedFieldRollingArgumentValue: [
dotOperatorHighlightRule,
{
token: 'tb.calculated-field-values',
regex: /values/,
@ -361,12 +368,14 @@ const calculatedFieldRollingArgumentValueHighlightRules: AceHighlightRules = {
token: 'tb.calculated-field-time-window',
regex: /timeWindow/,
next: 'calculatedFieldRollingArgumentTimeWindow'
}
},
endGroupHighlightRule
],
}
const calculatedFieldTimeWindowArgumentValueHighlightRules: AceHighlightRules = {
calculatedFieldRollingArgumentTimeWindow: [
dotOperatorHighlightRule,
{
token: 'tb.calculated-field-start-ts',
regex: /startTs/,
@ -381,6 +390,7 @@ const calculatedFieldTimeWindowArgumentValueHighlightRules: AceHighlightRules =
token: 'tb.calculated-field-limit',
regex: /limit/,
next: 'no_regex'
}
},
endGroupHighlightRule
]
}

View File

@ -1101,6 +1101,7 @@
"username": "Username",
"password": "Password",
"data": "Data",
"timestamp": "Timestamp",
"enter-username": "Enter username",
"enter-password": "Enter password",
"enter-search": "Enter search",