Browse Source

UI: Improvement popover in cf; Simplify style

pull/14141/head
Vladyslav_Prykhodko 10 months ago
parent
commit
522369383b
  1. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  2. 8
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html
  3. 19
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.scss
  4. 19
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  5. 58
      ui-ngx/src/app/modules/home/components/calculated-fields/components/common/calculated-field-panel.scss
  6. 421
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html
  7. 20
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss
  8. 33
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts
  9. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html
  10. 76
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.scss
  11. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
  12. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html
  13. 220
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.html
  14. 16
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.ts
  15. 20
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.html
  16. 76
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.scss
  17. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.ts
  18. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html
  19. 21
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss
  20. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts

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

@ -111,7 +111,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
const expressionColumn = new EntityTableColumn<CalculatedField>('expression', 'calculated-fields.expression', '300px');
const expressionColumn = new EntityTableColumn<CalculatedField>('expression', 'calculated-fields.expression', '250px');
expressionColumn.sortable = false;
expressionColumn.cellContentFunction = entity => {
const expressionLabel = this.getExpressionLabel(entity);
@ -124,7 +124,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.columns.push(new DateEntityTableColumn<CalculatedField>('createdTime', 'common.created-time', this.datePipe, '150px'));
this.columns.push(new EntityTableColumn<CalculatedField>('name', 'common.name', '33%'));
this.columns.push(new EntityTableColumn<CalculatedField>('type', 'common.type', '80px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type))));
this.columns.push(new EntityTableColumn<CalculatedField>('type', 'common.type', '170px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type)), () => ({whiteSpace: 'nowrap' })));
this.columns.push(expressionColumn);
this.cellActionDescriptors.push(

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

@ -15,9 +15,9 @@
limitations under the License.
-->
<div class="w-full 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-config-panel" [formGroup]="argumentFormGroup">
<div class="tb-config-panel-title tb-form-panel-title">{{ 'calculated-fields.metrics.metric-settings' | translate }}</div>
<div class="tb-config-panel-content tb-form-panel no-border no-padding">
@if (hint) {
<div class="tb-form-hint tb-primary-fill hint-container">
{{ hint | translate }}
@ -190,7 +190,7 @@
}
</div>
</div>
<div class="flex justify-end gap-2">
<div class="tb-config-panel-buttons">
<button mat-button
color="primary"
type="button"

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

@ -15,22 +15,9 @@
*/
@use '../../../../../../../scss/constants' as constants;
$panel-width: 520px;
:host {
display: flex;
width: $panel-width;
max-width: 100%;
max-height: 100vh;
.fixed-title-width {
@media #{constants.$mat-xs} {
min-width: 120px;
}
}
.limit-field-row {
@media screen and (max-width: $panel-width) {
@media screen and (max-width: 520px) {
display: flex;
flex-direction: column;
@ -40,6 +27,10 @@ $panel-width: 520px;
}
}
}
.tb-primary-fill {
overflow: visible;
}
}
:host ::ng-deep {

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

@ -36,7 +36,7 @@ import {
CalculatedFieldArgumentValue,
getCalculatedFieldCurrentEntityFilter
} from '@shared/models/calculated-field.models';
import { debounceTime, delay, distinctUntilChanged, filter } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
import { EntityType } from '@shared/models/entity-type.models';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DatasourceType } from '@shared/models/widget.models';
@ -52,12 +52,11 @@ import { Store } from '@ngrx/store';
import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { TenantId } from '@shared/models/id/tenant-id';
import { deduplicationStrategiesHintTranslations } from '@home/components/rule-node/rule-node-config.models';
@Component({
selector: 'tb-calculated-field-argument-panel',
templateUrl: './calculated-field-argument-panel.component.html',
styleUrls: ['./calculated-field-argument-panel.component.scss']
styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-argument-panel.component.scss']
})
export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewInit {
@ -122,7 +121,6 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
this.observeEntityFilterChanges();
this.observeArgumentTypeChanges();
this.observeEntityKeyChanges();
this.observeUpdatePosition();
}
get entityType(): ArgumentEntityType {
@ -302,17 +300,6 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
};
}
private observeUpdatePosition(): void {
merge(
this.argumentType.valueChanges,
this.refEntityKeyFormGroup.get('type').valueChanges,
this.argumentFormGroup.get('timeWindow').valueChanges,
this.argumentFormGroup.get('refEntityId').valueChanges.pipe(filter(Boolean)),
)
.pipe(delay(50), takeUntilDestroyed())
.subscribe(() => this.popover.updatePosition());
}
private updatedRefEntityIdState(type: ArgumentEntityType): void {
const isEntityWithId = !!type && type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current;
this.argumentFormGroup.get('refEntityId')[isEntityWithId ? 'enable' : 'disable']();
@ -320,6 +307,4 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
this.entityNameSubject.next(null);
}
}
protected readonly deduplicationStrategiesHintTranslations = deduplicationStrategiesHintTranslations;
}

58
ui-ngx/src/app/modules/home/components/calculated-fields/components/common/calculated-field-panel.scss

@ -0,0 +1,58 @@
/**
* Copyright © 2016-2025 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 '../../../../../../../scss/constants';
:host {
.tb-config-panel {
width: 520px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-lt-md} {
width: 90vw;
}
.tb-config-panel-title {
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
font-weight: 500;
font-size: 16px;
}
.tb-config-panel-content {
display: flex;
flex-direction: column;
gap: 16px;
overflow: auto;
.fixed-title-width {
@media #{$mat-xs} {
min-width: 120px;
}
}
}
.tb-config-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}
}

421
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html

@ -15,23 +15,23 @@
limitations under the License.
-->
<div class="w-full max-w-xl" [formGroup]="geofencingFormGroup">
<div class="tb-form-panel no-border no-padding mb-2">
<div class="tb-form-panel-title">{{ 'calculated-fields.geofencing-zone-groups-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.name' | translate }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="new-name" name="value" formControlName="name" maxlength="255" placeholder="{{ 'action.set' | translate }}"/>
@if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-required' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('duplicateName')) {
<div class="tb-config-panel" [formGroup]="geofencingFormGroup">
<div class="tb-config-panel-title tb-form-panel-title">{{ 'calculated-fields.geofencing-zone-groups-settings' | translate }}</div>
<div class="tb-config-panel-content tb-form-panel no-border no-padding">
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.name' | translate }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="new-name" name="value" formControlName="name" maxlength="255"
placeholder="{{ 'action.set' | translate }}"/>
@if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-required' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('duplicateName')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
@ -40,216 +40,217 @@
warning
</mat-icon>
} @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('pattern')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-pattern' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('maxlength')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-max-length' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('forbiddenName')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-forbidden' | translate"
class="tb-error">
warning
</mat-icon>
}
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-pattern' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('maxlength')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-max-length' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('forbiddenName')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-forbidden' | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
</div>
<ng-container [formGroup]="refEntityIdFormGroup">
<div class="tb-form-row">
<div class="fixed-title-width">{{ '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>
<ng-container [formGroup]="refEntityIdFormGroup">
@if (ArgumentEntityTypeParamsMap.has(entityType)) {
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'entity.entity-type' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType">
@for (type of argumentEntityTypes; track type) {
<mat-option [value]="type">{{ ArgumentEntityTypeTranslations.get(type) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@if (ArgumentEntityTypeParamsMap.has(entityType)) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}</div>
<tb-entity-autocomplete class="flex-1"
#entityAutocomplete
formControlName="id"
inlineField
appearance="outline"
[placeholder]="'action.set' | translate"
required
[entityType]="ArgumentEntityTypeParamsMap.get(entityType).entityType"
(entityChanged)="entityNameSubject.next($event?.name)"/>
</div>
}
</ng-container>
<ng-container [formGroup]="refDynamicSourceFormGroup">
<div class="tb-form-panel stroked" *ngIf="entityType === ArgumentEntityType.RelationQuery">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header>{{ 'calculated-fields.entity-zone-relationship' | translate }}</mat-expansion-panel-header>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-actions-header"></div>
<div class="tb-form-table-header-cell" translate>calculated-fields.level</div>
<div class="tb-form-table-header-cell flex-1" translate>calculated-fields.direction-level</div>
<div class="tb-form-table-header-cell flex-1 tb-required" translate>calculated-fields.relation-type</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
@if (levelsFormArray()?.controls?.length) {
<div class="tb-form-table-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="keyDrop($event)">
@for (keyControl of levelsFormArray().controls; track trackByKey;) {
<div cdkDrag [cdkDragDisabled]="!dragEnabled" class="tb-draggable-form-table-row">
<div class="tb-form-table-row-cell-buttons">
<button mat-icon-button
type="button"
cdkDragHandle
class="lt-lg:!hidden"
[class.tb-hidden]="!dragEnabled"
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
<div class="tb-form-row no-border flex-1" [formGroup]="keyControl">
<div class="level-text">{{ $index+1 }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="direction">
@for (direction of GeofencingDirectionList; track direction) {
<mat-option [value]="direction">{{ GeofencingDirectionLevelTranslations.get(direction) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
additionalClass="tb-suffix-show-on-hover"
class="flex-1"
appearance="outline"
panelWidth=""
required
[errorText]="'calculated-fields.hint.relation-type-required' | translate"
formControlName="relationType">
</tb-string-autocomplete>
</div>
<div class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button
(click)="removeKey($index)"
matTooltip="{{ 'calculated-fields.delete-level' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
}
</div>
} @else {
<span class="tb-prompt flex items-center justify-center">{{ 'calculated-fields.no-level' | translate }}</span>
}
@if (levelsFormArray().errors) {
<tb-error noMargin error="{{ 'calculated-fields.levels-required' | translate }}" style="padding-left: 12px;"></tb-error>
}
</div>
<div>
@if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) {
<div class="tb-form-hint tb-primary-fill max-args-warning flex items-center gap-2">
<mat-icon>warning</mat-icon>
<span>{{ 'calculated-fields.max-allowed-levels-error' | translate }}</span>
</div>
} @else {
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ 'calculated-fields.add-level' | translate }}
</button>
}
</div>
</mat-expansion-panel>
<div class="fixed-title-width tb-required">{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}</div>
<tb-entity-autocomplete class="flex-1"
#entityAutocomplete
formControlName="id"
inlineField
appearance="outline"
[placeholder]="'action.set' | translate"
required
[entityType]="ArgumentEntityTypeParamsMap.get(entityType).entityType"
(entityChanged)="entityNameSubject.next($event?.name)"/>
</div>
</ng-container>
<ng-container>
@if (entityFilter.singleEntity?.id) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{'calculated-fields.hint.perimeter-attribute-key' | translate}}">
{{ 'calculated-fields.perimeter-attribute-key' | translate }}
}
</ng-container>
<ng-container [formGroup]="refDynamicSourceFormGroup">
<div class="tb-form-panel stroked" *ngIf="entityType === ArgumentEntityType.RelationQuery">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header>{{ 'calculated-fields.entity-zone-relationship' | translate }}</mat-expansion-panel-header>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-actions-header"></div>
<div class="tb-form-table-header-cell" translate>calculated-fields.level</div>
<div class="tb-form-table-header-cell flex-1" translate>calculated-fields.direction-level</div>
<div class="tb-form-table-header-cell tb-required flex-1" translate>calculated-fields.relation-type</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
@if (entityType === ArgumentEntityType.RelationQuery) {
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="new-name" name="value" formControlName="perimeterKeyName" maxlength="255" placeholder="{{ 'action.set' | translate }}"/>
@if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.perimeter-attribute-key-required' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('pattern')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.perimeter-attribute-key-pattern' | translate"
class="tb-error">
warning
</mat-icon>
@if (levelsFormArray()?.controls?.length) {
<div class="tb-form-table-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="keyDrop($event)">
@for (keyControl of levelsFormArray().controls; track trackByKey; ) {
<div cdkDrag [cdkDragDisabled]="!dragEnabled" class="tb-draggable-form-table-row">
<div class="tb-form-table-row-cell-buttons">
<button mat-icon-button
type="button"
cdkDragHandle
class="lt-lg:!hidden"
[class.tb-hidden]="!dragEnabled"
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
<div class="tb-form-row no-border flex-1" [formGroup]="keyControl">
<div class="level-text">{{ $index + 1 }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="direction">
@for (direction of GeofencingDirectionList; track direction) {
<mat-option [value]="direction">{{ GeofencingDirectionLevelTranslations.get(direction) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
additionalClass="tb-suffix-show-on-hover"
class="flex-1"
appearance="outline"
panelWidth=""
required
[errorText]="'calculated-fields.hint.relation-type-required' | translate"
formControlName="relationType">
</tb-string-autocomplete>
</div>
<div class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button
(click)="removeKey($index)"
matTooltip="{{ 'calculated-fields.delete-level' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
}
</mat-form-field>
</div>
} @else {
<tb-entity-key-autocomplete class="flex-1" formControlName="perimeterKeyName" [dataKeyType]="DataKeyType.attribute" [entityFilter]="entityFilter" [keyScopeType]="AttributeScope.SERVER_SCOPE"/>
<span class="tb-prompt flex items-center justify-center">{{ 'calculated-fields.no-level' | translate }}</span>
}
@if (levelsFormArray().errors) {
<tb-error noMargin error="{{ 'calculated-fields.levels-required' | translate }}" style="padding-left: 12px;"></tb-error>
}
</div>
}
<div>
@if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) {
<div class="tb-form-hint tb-primary-fill max-args-warning flex items-center gap-2">
<mat-icon>warning</mat-icon>
<span>{{ 'calculated-fields.max-allowed-levels-error' | translate }}</span>
</div>
} @else {
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ 'calculated-fields.add-level' | translate }}
</button>
}
</div>
</mat-expansion-panel>
</div>
</ng-container>
<ng-container>
@if (entityFilter.singleEntity?.id) {
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'calculated-fields.hint.report-strategy' | translate}}">{{ 'calculated-fields.report-strategy' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="reportStrategy">
@for (strategy of GeofencingReportStrategyList; track strategy) {
<mat-option [value]="strategy">{{ GeofencingReportStrategyTranslations.get(strategy) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</ng-container>
<div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide" formControlName="createRelationsWithMatchedZones" (click)="$event.stopPropagation()">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.create-relation-with-matched-zones' | translate }}">
{{ 'calculated-fields.create-relation-with-matched-zones' | translate }}
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{'calculated-fields.hint.perimeter-attribute-key' | translate}}">
{{ 'calculated-fields.perimeter-attribute-key' | translate }}
</div>
</mat-slide-toggle>
<div class="tb-form-row" [class.!hidden]="!geofencingFormGroup.get('createRelationsWithMatchedZones').value">
<div class="fixed-title-width">{{ 'calculated-fields.direction' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="direction">
@for (direction of GeofencingDirectionList; track direction) {
<mat-option [value]="direction">{{ GeofencingDirectionTranslations.get(direction) | translate }}</mat-option>
@if (entityType === ArgumentEntityType.RelationQuery) {
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="new-name" name="value" formControlName="perimeterKeyName" maxlength="255" placeholder="{{ 'action.set' | translate }}"/>
@if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.perimeter-attribute-key-required' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('pattern')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.perimeter-attribute-key-pattern' | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-select>
</mat-form-field>
</mat-form-field>
} @else {
<tb-entity-key-autocomplete class="flex-1" formControlName="perimeterKeyName"
[dataKeyType]="DataKeyType.attribute" [entityFilter]="entityFilter"
[keyScopeType]="AttributeScope.SERVER_SCOPE"/>
}
</div>
<div class="tb-form-row" [class.!hidden]="!geofencingFormGroup.get('createRelationsWithMatchedZones').value">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.relation-type' | translate }}</div>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
additionalClass="tb-suffix-show-on-hover"
class="flex-1"
appearance="outline"
panelWidth=""
required
[errorText]="'calculated-fields.hint.relation-type-required' | translate"
formControlName="relationType">
</tb-string-autocomplete>
}
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'calculated-fields.hint.report-strategy' | translate}}">{{ 'calculated-fields.report-strategy' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="reportStrategy">
@for (strategy of GeofencingReportStrategyList; track strategy) {
<mat-option [value]="strategy">{{ GeofencingReportStrategyTranslations.get(strategy) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</ng-container>
<div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide" formControlName="createRelationsWithMatchedZones" (click)="$event.stopPropagation()">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.create-relation-with-matched-zones' | translate }}">
{{ 'calculated-fields.create-relation-with-matched-zones' | translate }}
</div>
</mat-slide-toggle>
<div class="tb-form-row" [class.!hidden]="!geofencingFormGroup.get('createRelationsWithMatchedZones').value">
<div class="fixed-title-width">{{ 'calculated-fields.direction' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="direction">
@for (direction of GeofencingDirectionList; track direction) {
<mat-option [value]="direction">{{ GeofencingDirectionTranslations.get(direction) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row" [class.!hidden]="!geofencingFormGroup.get('createRelationsWithMatchedZones').value">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.relation-type' | translate }}</div>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
additionalClass="tb-suffix-show-on-hover"
class="flex-1"
appearance="outline"
panelWidth=""
required
[errorText]="'calculated-fields.hint.relation-type-required' | translate"
formControlName="relationType">
</tb-string-autocomplete>
</div>
</div>
</div>
<div class="flex justify-end gap-2">
<div class="tb-config-panel-buttons">
<button mat-button
color="primary"
type="button"

20
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.scss

@ -15,20 +15,7 @@
*/
@import '../../../../../../../scss/constants';
$panel-width: 520px;
:host {
display: flex;
width: $panel-width;
max-width: 100%;
max-height: 80vh;
.fixed-title-width {
@media #{$mat-xs} {
min-width: 120px;
}
}
.level-text {
display: flex;
justify-content: center;
@ -58,7 +45,7 @@ $panel-width: 520px;
}
.limit-field-row {
@media screen and (max-width: $panel-width) {
@media screen and (max-width: 520px) {
display: flex;
flex-direction: column;
@ -71,11 +58,6 @@ $panel-width: 520px;
}
:host ::ng-deep {
.time-interval-field {
.advanced-input {
flex-direction: column;
}
}
tb-entity-autocomplete {
.mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper {
padding-right: 0 !important;

33
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts

@ -32,14 +32,13 @@ import {
ArgumentEntityTypeTranslations,
CalculatedFieldGeofencing,
CalculatedFieldGeofencingValue,
CalculatedFieldType,
GeofencingDirectionLevelTranslations,
GeofencingDirectionTranslations,
GeofencingReportStrategy,
GeofencingReportStrategyTranslations,
getCalculatedFieldCurrentEntityFilter
} from '@shared/models/calculated-field.models';
import { debounceTime, delay, distinctUntilChanged, map } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
import { EntityType } from '@shared/models/entity-type.models';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
@ -58,7 +57,7 @@ import { CdkDragDrop } from "@angular/cdk/drag-drop";
@Component({
selector: 'tb-calculated-field-geofencing-zone-groups-panel',
templateUrl: './calculated-field-geofencing-zone-groups-panel.component.html',
styleUrls: ['./calculated-field-geofencing-zone-groups-panel.component.scss']
styleUrls: ['../common/calculated-field-panel.scss', './calculated-field-geofencing-zone-groups-panel.component.scss']
})
export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit, AfterViewInit {
@ -67,7 +66,6 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
@Input() entityId: EntityId;
@Input() tenantId: string;
@Input() entityName: string;
@Input() calculatedFieldType: CalculatedFieldType;
@Input() usedNames: string[];
@ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent;
@ -118,7 +116,6 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
this.observeEntityFilterChanges();
this.observeEntityTypeChanges();
this.observeUpdatePosition();
this.observeCreateRelationZonesChanges();
}
@ -161,26 +158,12 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search))));
}
private observeMaxLevelChanges(): void {
this.refDynamicSourceFormGroup.get('maxLevel').valueChanges
.pipe(takeUntilDestroyed())
.subscribe(value => this.validateFetchLastLevelOnly(value));
}
private observeCreateRelationZonesChanges(): void {
this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges
.pipe(takeUntilDestroyed())
.subscribe(value => this.validateDirectionAndRelationType(value));
}
private validateFetchLastLevelOnly(maxLevel = 1): void {
if (maxLevel > 1) {
this.refDynamicSourceFormGroup.get('fetchLastLevelOnly').enable({emitEvent: false});
} else {
this.refDynamicSourceFormGroup.get('fetchLastLevelOnly').disable({emitEvent: false});
}
}
private validateDirectionAndRelationType(createRelation = false): void {
if (createRelation) {
this.geofencingFormGroup.get('direction').enable({emitEvent: false});
@ -304,21 +287,11 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
};
}
private observeUpdatePosition(): void {
merge(
this.refEntityIdFormGroup.get('entityType').valueChanges,
this.refEntityIdFormGroup.get('id').valueChanges,
this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges
)
.pipe(delay(50), takeUntilDestroyed())
.subscribe(() => this.popover.updatePosition());
}
levelsFormArray(): UntypedFormArray {
return this.refDynamicSourceFormGroup.get('levels') as UntypedFormArray;
}
trackByKey(index: number, keyControl: AbstractControl): any {
trackByKey(_index: number, keyControl: AbstractControl): any {
return keyControl;
}

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html

@ -35,7 +35,7 @@
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'entityType'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="entity-type-header w-1/5 xs:hidden">
<mat-header-cell mat-sort-header *matHeaderCellDef class="w-1/5 xs:hidden">
{{ 'entity.entity-type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let geofenceZone" class="w-1/5 xs:hidden">

76
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.scss

@ -1,76 +0,0 @@
/**
* Copyright © 2016-2025 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 {
.arguments-table {
min-height: 108px;
&-with-error {
min-height: 150px;
}
.mat-mdc-table {
table-layout: fixed;
}
.key-text {
font-size: 13px;
}
.copy-argument-name {
visibility: hidden;
transition: visibility 0.1s;
}
.argument-name-cell:hover {
.copy-argument-name {
visibility: visible;
}
}
}
.max-args-warning {
.mat-icon {
color: #FAA405;
}
}
.tb-form-table-row-cell-buttons {
--mat-badge-legacy-small-size-container-size: 8px;
--mat-badge-small-size-container-overlap-offset: -5px;
--mat-badge-small-size-text-size: 0;
}
}
:host ::ng-deep {
.arguments-table:not(.arguments-table-with-error) {
.mdc-data-table__row:last-child .mat-mdc-cell {
border-bottom: none;
}
}
.arguments-table {
.mat-mdc-header-row.mat-row-select .mat-mdc-header-cell.entity-type-header {
padding: 0 28px 0 0;
}
}
.copy-argument-name {
.mat-icon {
font-size: 16px;
padding: 4px;
}
}
}

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts

@ -37,7 +37,6 @@ import {
ArgumentEntityType,
CalculatedFieldGeofencing,
CalculatedFieldGeofencingValue,
CalculatedFieldType,
GeofencingReportStrategyTranslations,
} from '@shared/models/calculated-field.models';
import { MatButton } from '@angular/material/button';
@ -63,7 +62,7 @@ import {
@Component({
selector: 'tb-calculated-field-geofencing-zone-groups-table',
templateUrl: './calculated-field-geofencing-zone-groups-table.component.html',
styleUrls: [`calculated-field-geofencing-zone-groups-table.component.scss`],
styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -157,7 +156,6 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
index,
zone,
entityId: this.entityId,
calculatedFieldType: CalculatedFieldType.GEOFENCING,
buttonTitle: isExists ? 'action.apply' : 'action.add',
tenantId: this.tenantId,
entityName: this.entityName,
@ -168,7 +166,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
renderer: this.renderer,
componentType: CalculatedFieldGeofencingZoneGroupsPanelComponent,
hostView: this.viewContainerRef,
preferredPlacement: isExists ? ['left', 'leftTop', 'leftBottom'] : ['topRight', 'right', 'rightTop'],
preferredPlacement: isExists ? ['leftOnly', 'leftTopOnly', 'leftBottomOnly'] : ['rightOnly', 'rightTopOnly', 'rightBottomOnly'],
context: ctx,
isModal: true
});

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html

@ -43,7 +43,7 @@
</div>
@if (simpleMode) {
@if (hiddenName) {
<div class="flex items-start gap-3">
<div class="grid grid-cols-2 items-start gap-3">
<ng-container *ngTemplateOutlet="decimalsByDefaultField"></ng-container>
<ng-content select=".simpleMode"></ng-content>
</div>

220
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.html

@ -15,23 +15,23 @@
limitations under the License.
-->
<div class="w-full max-w-xl" [formGroup]="metricForm">
<div class="tb-form-panel no-border no-padding mb-2">
<div class="tb-form-panel-title">{{ 'calculated-fields.metrics.metric-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.metrics.metric-name' | translate }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="new-name" name="value" formControlName="name" maxlength="255" placeholder="{{ 'action.set' | translate }}"/>
@if (metricForm.get('name').touched && metricForm.get('name').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-required' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (metricForm.get('name').touched && metricForm.get('name').hasError('duplicateName')) {
<div class="tb-config-panel" [formGroup]="metricForm">
<div class="tb-config-panel-title">{{ 'calculated-fields.metrics.metric-settings' | translate }}</div>
<div class="tb-config-panel-content tb-form-panel no-border no-padding">
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.metrics.metric-name' | translate }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="new-name" name="value" formControlName="name" maxlength="255"
placeholder="{{ 'action.set' | translate }}"/>
@if (metricForm.get('name').touched && metricForm.get('name').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-required' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (metricForm.get('name').touched && metricForm.get('name').hasError('duplicateName')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
@ -40,100 +40,59 @@
warning
</mat-icon>
} @else if (metricForm.get('name').touched && metricForm.get('name').hasError('pattern')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-pattern' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (metricForm.get('name').touched && metricForm.get('name').hasError('maxlength')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-max-length' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (metricForm.get('name').touched && metricForm.get('name').hasError('forbiddenName')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-forbidden' | translate"
class="tb-error">
warning
</mat-icon>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-pattern' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (metricForm.get('name').touched && metricForm.get('name').hasError('maxlength')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-max-length' | translate"
class="tb-error">
warning
</mat-icon>
} @else if (metricForm.get('name').touched && metricForm.get('name').hasError('forbiddenName')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-forbidden' | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'calculated-fields.metrics.aggregation' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="function">
@for (aggFunction of AggFunctions; track aggFunction) {
<mat-option [value]="aggFunction">{{ AggFunctionTranslations.get(aggFunction) | translate }}</mat-option>
}
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'calculated-fields.metrics.aggregation' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="function">
@for (aggFunction of AggFunctions; track aggFunction) {
<mat-option [value]="aggFunction">{{ AggFunctionTranslations.get(aggFunction) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [(expanded)]="filterExpanded"
[disabled]="!metricForm.get('allowFilter').value">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide flex items-stretch justify-center" formControlName="allowFilter" (click)="$event.stopPropagation()">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.metrics.filter-hint' | translate }}">
{{ 'calculated-fields.metrics.filter' | translate }}
</div>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-js-func required
formControlName="filter"
functionName="filter"
[functionArgs]="functionArgs"
[disableUndefinedCheck]="true"
[scriptLanguage]="ScriptLanguage.TBEL"
[highlightRules]="highlightRules"
[editorCompleter]="editorCompleter"
[helpPopupStyle]="{ width: '1200px' }"
helpId="calculated-field/expression_fn">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
<div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [(expanded)]="filterExpanded"
[disabled]="!metricForm.get('allowFilter').value">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
<mat-slide-toggle class="mat-slide flex items-stretch justify-center" formControlName="allowFilter"
(click)="$event.stopPropagation()">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.metrics.filter-hint' | translate }}">
{{ 'calculated-fields.metrics.filter' | translate }}
</div>
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</div>
<ng-container formGroupName="input">
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'calculated-fields.metrics.value-source' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
@for (inputType of AggInputTypes; track inputType) {
<mat-option [value]="inputType">{{ AggInputTypeTranslations.get(inputType) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@if (this.metricForm.get('input.type').value === AggInputType.key) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.argument-name' | translate }}</div>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
additionalClass="tb-suffix-show-on-hover"
class="flex-1"
appearance="outline"
panelWidth=""
required
[errorText]="'calculated-fields.hint.argument-name-required' | translate"
formControlName="key">
</tb-string-autocomplete>
</div>
} @else {
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-js-func required
formControlName="function"
formControlName="filter"
functionName="filter"
[functionArgs]="functionArgs"
[disableUndefinedCheck]="true"
@ -146,11 +105,52 @@
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</div>
</tb-js-func>
}
</ng-container>
</ng-template>
</mat-expansion-panel>
</div>
<ng-container formGroupName="input">
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'calculated-fields.metrics.value-source' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
@for (inputType of AggInputTypes; track inputType) {
<mat-option [value]="inputType">{{ AggInputTypeTranslations.get(inputType) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@if (this.metricForm.get('input.type').value === AggInputType.key) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required">{{ 'calculated-fields.argument-name' | translate }}</div>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
additionalClass="tb-suffix-show-on-hover"
class="flex-1"
appearance="outline"
panelWidth=""
required
[errorText]="'calculated-fields.hint.argument-name-required' | translate"
formControlName="key">
</tb-string-autocomplete>
</div>
} @else {
<tb-js-func required
formControlName="function"
functionName="filter"
[functionArgs]="functionArgs"
[disableUndefinedCheck]="true"
[scriptLanguage]="ScriptLanguage.TBEL"
[highlightRules]="highlightRules"
[editorCompleter]="editorCompleter"
[helpPopupStyle]="{ width: '1200px' }"
helpId="calculated-field/expression_fn">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</div>
</tb-js-func>
}
</ng-container>
</div>
<div class="flex justify-end gap-2">
<div class="tb-config-panel-buttons">
<button mat-button
color="primary"
type="button"

16
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.ts

@ -25,10 +25,10 @@ import {
AggInputTypeTranslations,
CalculatedFieldAggMetricValue
} from '@shared/models/calculated-field.models';
import { delay, map } from 'rxjs/operators';
import { map } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityFilter } from '@shared/models/query/query.models';
import { merge, Observable, of } from 'rxjs';
import { Observable, of } from 'rxjs';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { AceHighlightRules } from '@shared/models/ace/ace.models';
@ -40,7 +40,7 @@ interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricVal
@Component({
selector: 'tb-calculated-field-metrics-panel',
templateUrl: './calculated-field-metrics-panel.component.html',
styleUrls: ['./calculated-field-metrics-panel.component.scss']
styleUrl: '../common/calculated-field-panel.scss',
})
export class CalculatedFieldMetricsPanelComponent implements OnInit {
@ -80,7 +80,6 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
private fb: FormBuilder,
private popover: TbPopoverComponent<CalculatedFieldMetricsPanelComponent>
) {
this.observeUpdatePosition();
this.observeFilterAllowChange();
this.observeInputTypeChange();
}
@ -164,13 +163,4 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
return forbiddenNames.includes(trimmedValue) ? { forbiddenName: true } : null;
};
}
private observeUpdatePosition(): void {
merge(
this.metricForm.get('allowFilter').valueChanges,
this.metricForm.get('input.type').valueChanges
)
.pipe(delay(50), takeUntilDestroyed())
.subscribe(() => this.popover.updatePosition());
}
}

20
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.html

@ -20,10 +20,10 @@
<table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="!w-1/3 xs:!w-1/2">
<mat-header-cell mat-sort-header *matHeaderCellDef class="!w-1/5 xs:!w-full sm:!w-1/2">
<div tbTruncateWithTooltip>{{ 'calculated-fields.metrics.metric-name' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let metric" class="argument-name-cell w-1/3 xs:w-1/2">
<mat-cell *matCellDef="let metric" class="argument-name-cell w-1/5 xs:w-full sm:w-1/2">
<div class="flex items-center">
<div tbTruncateWithTooltip class="flex-1">{{ metric.name }}</div>
<tb-copy-button class="copy-argument-name"
@ -35,26 +35,28 @@
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'function'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="entity-type-header w-1/5 xs:hidden">
<mat-header-cell mat-sort-header *matHeaderCellDef class="w-1/5 xs:hidden lt-md:w-1/2">
{{ 'calculated-fields.metrics.aggregation' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let metric" class="w-1/5 xs:hidden">
<mat-cell *matCellDef="let metric" class="w-1/5 xs:hidden lt-md:w-1/2">
<div tbTruncateWithTooltip>{{ AggFunctionTranslations.get(metric.function) | translate }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'filter'">
<mat-header-cell mat-sort-header *matHeaderCellDef class="entity-type-header w-1/5 xs:hidden">
<mat-header-cell mat-sort-header *matHeaderCellDef class="w-1/5 lt-md:hidden">
{{ 'calculated-fields.metrics.filtered' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let metric" class="w-1/5 xs:hidden">
<mat-icon>{{ metric.filter ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>
<mat-cell *matCellDef="let metric" class="w-1/5 lt-md:hidden">
<div>
<mat-icon class="ml-4 align-middle">{{ metric.filter ? 'check_box' : 'check_box_outline_blank' }}</mat-icon>
</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'valueSource'">
<mat-header-cell *matHeaderCellDef class="w-1/4 xs:hidden">
<mat-header-cell mat-sort-header *matHeaderCellDef class="w-2/5 lt-md:hidden">
{{ 'calculated-fields.metrics.value-source' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let metric" class="w-1/4 xs:hidden">
<mat-cell *matCellDef="let metric" class="w-2/5 lt-md:hidden">
<div tbTruncateWithTooltip>{{ AggInputTypeTranslations.get(metric.input.type) | translate }}</div>
</mat-cell>
</ng-container>

76
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.scss

@ -1,76 +0,0 @@
/**
* Copyright © 2016-2025 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 {
.arguments-table {
min-height: 108px;
&-with-error {
min-height: 150px;
}
.mat-mdc-table {
table-layout: fixed;
}
.key-text {
font-size: 13px;
}
.copy-argument-name {
visibility: hidden;
transition: visibility 0.1s;
}
.argument-name-cell:hover {
.copy-argument-name {
visibility: visible;
}
}
}
.max-args-warning {
.mat-icon {
color: #FAA405;
}
}
.tb-form-table-row-cell-buttons {
--mat-badge-legacy-small-size-container-size: 8px;
--mat-badge-small-size-container-overlap-offset: -5px;
--mat-badge-small-size-text-size: 0;
}
}
:host ::ng-deep {
.arguments-table:not(.arguments-table-with-error) {
.mdc-data-table__row:last-child .mat-mdc-cell {
border-bottom: none;
}
}
.arguments-table {
.mat-mdc-header-row.mat-row-select .mat-mdc-header-cell.entity-type-header {
padding: 0 28px 0 0;
}
}
.copy-argument-name {
.mat-icon {
font-size: 16px;
padding: 4px;
}
}
}

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-table.component.ts

@ -58,7 +58,7 @@ import { AceHighlightRules } from '@shared/models/ace/ace.models';
@Component({
selector: 'tb-calculated-field-metrics-table',
templateUrl: './calculated-field-metrics-table.component.html',
styleUrls: [`calculated-field-metrics-table.component.scss`],
styleUrls: [`../calculated-field-arguments/calculated-field-arguments-table.component.scss`],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -219,6 +219,8 @@ export class CalculatedFieldMetricsTableComponent implements ControlValueAccesso
switch (column) {
case 'function':
return metric.function;
case 'valueSource':
return metric.input?.type;
case 'filter':
return isDefinedAndNotNull(metric.filter).toString();
default:

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html

@ -71,8 +71,8 @@
<tb-calculate-field-output formControlName="output" [entityId]="entityId" simpleMode hiddenName>
<div class="tb-form-row simpleMode flex-1">
<mat-slide-toggle class="mat-slide" formControlName="useLatestTs">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}" translate>
calculated-fields.use-latest-timestamp
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.use-latest-timestamp</div>
</div>
</mat-slide-toggle>
</div>

21
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/calculated-field-metrics-panel.component.scss → ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.scss

@ -13,19 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../scss/constants';
:host ::ng-deep {
.simpleMode {
min-width: 0;
$panel-width: 520px;
.mat-slide {
overflow: hidden;
:host {
display: flex;
width: $panel-width;
max-width: 100%;
max-height: 80vh;
.mdc-form-field {
width: 100%;
.fixed-title-width {
@media #{$mat-xs} {
min-width: 120px;
.mdc-label {
min-width: 0;
}
}
}
}
}

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts

@ -47,6 +47,7 @@ import { AppState } from '@core/core.state';
@Component({
selector: 'tb-related-entities-aggregation-component',
templateUrl: './related-entities-aggregation-component.component.html',
styleUrl: './related-entities-aggregation-component.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,

Loading…
Cancel
Save