22 changed files with 725 additions and 98 deletions
@ -0,0 +1,71 @@ |
|||
///
|
|||
/// 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 { ChangeDetectorRef, Component, DestroyRef, forwardRef, Renderer2, ViewContainerRef, } from '@angular/core'; |
|||
import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, } from '@angular/forms'; |
|||
import { TbPopoverService } from '@shared/components/popover.service'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { |
|||
CalculatedFieldArgumentsTableComponent |
|||
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; |
|||
import { ArgumentEntityType } from '@shared/models/calculated-field.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-aggregation-arguments-table', |
|||
templateUrl: './calculated-field-arguments-table.component.html', |
|||
styleUrls: [`calculated-field-arguments-table.component.scss`], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => EntityAggregationArgumentsTableComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
}) |
|||
export class EntityAggregationArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent { |
|||
|
|||
constructor( |
|||
protected fb: FormBuilder, |
|||
protected popoverService: TbPopoverService, |
|||
protected viewContainerRef: ViewContainerRef, |
|||
protected cd: ChangeDetectorRef, |
|||
protected renderer: Renderer2, |
|||
protected entityService: EntityService, |
|||
protected destroyRef: DestroyRef, |
|||
protected store: Store<AppState> |
|||
) { |
|||
super(fb, popoverService, viewContainerRef, cd, renderer, entityService, destroyRef, store); |
|||
|
|||
this.argumentNameColumn = 'calculated-fields.argument-name'; |
|||
this.displayColumns = ['name', 'type', 'key', 'actions']; |
|||
this.panelAdditionalCtx = { |
|||
hiddenEntityTypes: true, |
|||
argumentEntityTypes: [ArgumentEntityType.Current], |
|||
hint: 'calculated-fields.entity-aggregation.argument-setting-hint', |
|||
hiddenDefaultValue: true, |
|||
hiddenEntityKeyTypes: true, |
|||
}; |
|||
|
|||
this.isScript = false; |
|||
} |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<div [formGroup]="entityAggregationConfiguration" class="tb-form-panel no-border no-padding"> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title"> |
|||
{{ 'calculated-fields.arguments' | translate }} |
|||
</div> |
|||
<div class="tb-form-hint tb-primary-fill hint-container"> |
|||
{{ 'calculated-fields.entity-aggregation.argument-hint' | translate }} |
|||
</div> |
|||
<tb-entity-aggregation-arguments-table formControlName="arguments" |
|||
[entityId]="entityId" |
|||
[tenantId]="tenantId" |
|||
[entityName]="entityName"/> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.metrics' | translate }}"> |
|||
{{ 'calculated-fields.metrics.metrics' | translate }} |
|||
</div> |
|||
<tb-calculated-field-metrics-table formControlName="metrics" |
|||
simpleMode |
|||
[arguments]="arguments$ | async" |
|||
></tb-calculated-field-metrics-table> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<div class="tb-form-panel-title" tbTruncateWithTooltip tb-hint-tooltip-icon="{{ 'calculated-fields.entity-aggregation.aggregation-interval-hint' | translate }}"> |
|||
{{ 'calculated-fields.entity-aggregation.aggregation-interval' | translate }} |
|||
</div> |
|||
<ng-container formGroupName="interval"> |
|||
<div class="flex items-start gap-3 xs:flex-col xs:items-stretch"> |
|||
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
|||
<mat-label translate>calculated-fields.aggregate-interval-type</mat-label> |
|||
<mat-select formControlName="type" required> |
|||
<mat-option *ngFor="let type of AggIntervalTypes" [value]="type"> |
|||
{{ AggIntervalTypeTranslations.get(type) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<tb-timezone-select |
|||
class="flex-1" |
|||
appearance="outline" |
|||
subscriptSizing="dynamic" |
|||
required |
|||
userTimezoneByDefault |
|||
formControlName="tz"> |
|||
</tb-timezone-select> |
|||
</div> |
|||
@if (entityAggregationConfiguration.get('interval.type').value === AggIntervalType.CUSTOM) { |
|||
<tb-time-unit-input required |
|||
[minTime]="0" |
|||
sameWidthInputs |
|||
appearance="outline" |
|||
subscriptSizing="dynamic" |
|||
labelText="{{ 'calculated-fields.aggregate-interval-value' | translate }}" |
|||
requiredText="{{ 'calculated-fields.aggregate-interval-value-required' | translate }}" |
|||
formControlName="multiplier"> |
|||
</tb-time-unit-input> |
|||
} |
|||
<div class="tb-form-panel stroked"> |
|||
<mat-slide-toggle class="mat-slide flex" formControlName="allowOffsetMillis"> |
|||
<div tb-hint-tooltip-icon="{{ 'calculated-fields.entity-aggregation.apply-offset-hint' | translate }}"> |
|||
{{ 'calculated-fields.entity-aggregation.apply-offset' | translate }} |
|||
</div> |
|||
</mat-slide-toggle> |
|||
@if (entityAggregationConfiguration.get('interval.allowOffsetMillis').value) { |
|||
<tb-time-unit-input required |
|||
[minTime]="1" |
|||
sameWidthInputs |
|||
appearance="outline" |
|||
subscriptSizing="dynamic" |
|||
labelText="{{ 'calculated-fields.entity-aggregation.offset-value' | translate }}" |
|||
requiredText="{{ 'calculated-fields.entity-aggregation.offset-value-required' | translate }}" |
|||
formControlName="offsetMillis"> |
|||
</tb-time-unit-input> |
|||
} |
|||
</div> |
|||
</ng-container> |
|||
<div class="tb-form-panel stroked"> |
|||
<mat-slide-toggle class="mat-slide flex" formControlName="allowWatermark"> |
|||
<div tb-hint-tooltip-icon="{{ 'calculated-fields.entity-aggregation.wait-delay-hint' | translate }}"> |
|||
{{ 'calculated-fields.entity-aggregation.wait-delay' | translate }} |
|||
</div> |
|||
</mat-slide-toggle> |
|||
@if (entityAggregationConfiguration.get('allowWatermark').value) { |
|||
<ng-container formGroupName="watermark"> |
|||
<tb-time-unit-input required |
|||
[minTime]="1" |
|||
sameWidthInputs |
|||
appearance="outline" |
|||
labelText="{{ 'calculated-fields.entity-aggregation.wait-duration' | translate }}" |
|||
hintText="{{ 'calculated-fields.entity-aggregation.wait-duration-hint' | translate }}" |
|||
requiredText="{{ 'calculated-fields.entity-aggregation.wait-duration-required' | translate }}" |
|||
formControlName="duration"> |
|||
</tb-time-unit-input> |
|||
<tb-time-unit-input required |
|||
[minTime]="0" |
|||
[maxTime]="entityAggregationConfiguration.get('watermark.duration').value" |
|||
sameWidthInputs |
|||
appearance="outline" |
|||
labelText="{{ 'calculated-fields.entity-aggregation.check-interval' | translate }}" |
|||
hintText="{{ 'calculated-fields.entity-aggregation.check-interval-hint' | translate }}" |
|||
maxErrorText="{{ 'calculated-fields.entity-aggregation.check-interval-max' | translate }}" |
|||
requiredText="{{ 'calculated-fields.entity-aggregation.check-interval-required' | translate }}" |
|||
formControlName="checkInterval"> |
|||
</tb-time-unit-input> |
|||
</ng-container> |
|||
} |
|||
</div> |
|||
</div> |
|||
<tb-calculate-field-output formControlName="output" |
|||
containerInputClass="grid items-start grid-cols-2 gap-3 xs:grid-cols-1" |
|||
simpleMode |
|||
hiddenName |
|||
disableType |
|||
[entityId]="entityId"></tb-calculate-field-output> |
|||
</div> |
|||
@ -0,0 +1,205 @@ |
|||
///
|
|||
/// 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 { Component, forwardRef, Input } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidationErrors, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { |
|||
AggInterval, |
|||
AggIntervalType, |
|||
AggIntervalTypeTranslations, |
|||
CalculatedFieldEntityAggregationConfiguration, |
|||
CalculatedFieldOutput, |
|||
CalculatedFieldType, |
|||
notEmptyObjectValidator, |
|||
OutputType |
|||
} from '@shared/models/calculated-field.models'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { HOUR, MINUTE, SECOND } from '@shared/models/time/time.models'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
|
|||
interface CalculatedFieldEntityAggregationConfigurationValue extends CalculatedFieldEntityAggregationConfiguration { |
|||
interval: AggInterval & {allowOffsetMillis?: boolean}; |
|||
allowWatermark: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-aggregation-component', |
|||
templateUrl: './entity-aggregation-component.component.html', |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => EntityAggregationComponentComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => EntityAggregationComponentComponent), |
|||
multi: true |
|||
} |
|||
], |
|||
}) |
|||
export class EntityAggregationComponentComponent implements ControlValueAccessor, Validator { |
|||
|
|||
@Input({required: true}) |
|||
entityId: EntityId; |
|||
|
|||
@Input({required: true}) |
|||
tenantId: string; |
|||
|
|||
@Input({required: true}) |
|||
entityName: string; |
|||
|
|||
|
|||
entityAggregationConfiguration = this.fb.group({ |
|||
arguments: this.fb.control({}, notEmptyObjectValidator()), |
|||
metrics: this.fb.control({}, notEmptyObjectValidator()), |
|||
interval: this.fb.group({ |
|||
type: [AggIntervalType.HOUR], |
|||
tz: ['', Validators.required], |
|||
multiplier: [HOUR/SECOND, Validators.required], |
|||
allowOffsetMillis: [false], |
|||
offsetMillis: [MINUTE/SECOND, Validators.required], |
|||
}), |
|||
allowWatermark: [false], |
|||
watermark: this.fb.group({ |
|||
duration: [6 * MINUTE / SECOND, Validators.required], |
|||
checkInterval: [MINUTE / SECOND, Validators.required], |
|||
}), |
|||
output: this.fb.control<CalculatedFieldOutput>({ |
|||
type: OutputType.Timeseries, |
|||
}), |
|||
}); |
|||
|
|||
arguments$ = this.entityAggregationConfiguration.get('arguments').valueChanges.pipe( |
|||
map(argumentsObj => Object.keys(argumentsObj)) |
|||
); |
|||
|
|||
AggIntervalType = AggIntervalType; |
|||
AggIntervalTypes = Object.values(AggIntervalType) as AggIntervalType[]; |
|||
AggIntervalTypeTranslations = AggIntervalTypeTranslations; |
|||
|
|||
private propagateChange: (config: CalculatedFieldEntityAggregationConfiguration) => void = () => { }; |
|||
|
|||
constructor(private fb: FormBuilder) { |
|||
|
|||
this.entityAggregationConfiguration.get('interval.type').valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((type: AggIntervalType) => { |
|||
this.checkAggIntervalType(type); |
|||
}); |
|||
|
|||
this.entityAggregationConfiguration.get('interval.allowOffsetMillis').valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((allow: boolean) => { |
|||
this.checkIntervalDuration(allow); |
|||
}); |
|||
|
|||
this.entityAggregationConfiguration.get('allowWatermark').valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((allow: boolean) => { |
|||
this.checkWatermark(allow); |
|||
}); |
|||
|
|||
this.entityAggregationConfiguration.valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe((value: CalculatedFieldEntityAggregationConfigurationValue) => { |
|||
this.updatedModel(value); |
|||
}); |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.entityAggregationConfiguration.valid || this.entityAggregationConfiguration.disabled ? null : {invalidPropagateConfig: false}; |
|||
} |
|||
|
|||
writeValue(value: CalculatedFieldEntityAggregationConfiguration): void { |
|||
const data: CalculatedFieldEntityAggregationConfigurationValue = { |
|||
...value, |
|||
allowWatermark: isDefinedAndNotNull(value.watermark), |
|||
interval: {...value.interval, allowOffsetMillis: isDefinedAndNotNull(value?.interval?.offsetMillis)} |
|||
} |
|||
this.entityAggregationConfiguration.patchValue(data, {emitEvent: false}); |
|||
this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); |
|||
this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetMillis').value); |
|||
this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); |
|||
setTimeout(() => { |
|||
this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: (config: CalculatedFieldEntityAggregationConfiguration) => void): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(_: any): void { } |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
if (isDisabled) { |
|||
this.entityAggregationConfiguration.disable({emitEvent: false}); |
|||
} else { |
|||
this.entityAggregationConfiguration.enable({emitEvent: false}); |
|||
this.checkAggIntervalType(this.entityAggregationConfiguration.get('interval.type').value); |
|||
this.checkIntervalDuration(this.entityAggregationConfiguration.get('interval.allowOffsetMillis').value); |
|||
this.checkWatermark(this.entityAggregationConfiguration.get('allowWatermark').value); |
|||
} |
|||
} |
|||
|
|||
private updatedModel(value: CalculatedFieldEntityAggregationConfigurationValue): void { |
|||
value.type = CalculatedFieldType.ENTITY_AGGREGATION; |
|||
if (!value.interval.allowOffsetMillis) { |
|||
delete value.interval.offsetMillis; |
|||
} |
|||
delete value.interval.offsetMillis; |
|||
if (!value.allowWatermark) { |
|||
delete value.watermark; |
|||
} |
|||
delete value.allowWatermark; |
|||
this.propagateChange(value); |
|||
} |
|||
|
|||
private checkAggIntervalType(type: AggIntervalType) { |
|||
if (type === AggIntervalType.CUSTOM) { |
|||
this.entityAggregationConfiguration.get('interval.multiplier').enable({emitEvent: false}); |
|||
} else { |
|||
this.entityAggregationConfiguration.get('interval.multiplier').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private checkIntervalDuration(allow: boolean) { |
|||
if (allow) { |
|||
this.entityAggregationConfiguration.get('interval.offsetMillis').enable({emitEvent: false}); |
|||
} else { |
|||
this.entityAggregationConfiguration.get('interval.offsetMillis').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
private checkWatermark(allow: boolean) { |
|||
if (allow) { |
|||
this.entityAggregationConfiguration.get('watermark').enable({emitEvent: false}); |
|||
} else { |
|||
this.entityAggregationConfiguration.get('watermark').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
///
|
|||
/// 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 { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { |
|||
CalculatedFieldOutputModule |
|||
} from '@home/components/calculated-fields/components/output/calculated-field-output.module'; |
|||
import { |
|||
CalculatedFieldArgumentsTableModule |
|||
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module'; |
|||
import { |
|||
EntityAggregationComponentComponent |
|||
} from '@home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component'; |
|||
import { |
|||
CalculatedFieldMetricsTableModule |
|||
} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-table.module'; |
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
CalculatedFieldOutputModule, |
|||
CalculatedFieldArgumentsTableModule, |
|||
CalculatedFieldMetricsTableModule, |
|||
], |
|||
declarations: [ |
|||
EntityAggregationComponentComponent, |
|||
], |
|||
exports: [ |
|||
EntityAggregationComponentComponent, |
|||
] |
|||
}) |
|||
export class EntityAggregationComponentModule { |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
import { NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { |
|||
CalculatedFieldMetricsTableComponent |
|||
} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component'; |
|||
import { |
|||
CalculatedFieldMetricsPanelComponent |
|||
} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component'; |
|||
|
|||
|
|||
@NgModule({ |
|||
imports: [ |
|||
CommonModule, |
|||
SharedModule, |
|||
], |
|||
declarations: [ |
|||
CalculatedFieldMetricsTableComponent, |
|||
CalculatedFieldMetricsPanelComponent |
|||
], |
|||
exports: [ |
|||
CalculatedFieldMetricsTableComponent |
|||
] |
|||
}) |
|||
export class CalculatedFieldMetricsTableModule { |
|||
|
|||
} |
|||
Loading…
Reference in new issue