15 changed files with 505 additions and 70 deletions
@ -0,0 +1,70 @@ |
|||||
|
///
|
||||
|
/// 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-related-aggregation-arguments-table', |
||||
|
templateUrl: './calculated-field-arguments-table.component.html', |
||||
|
styleUrls: [`calculated-field-arguments-table.component.scss`], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => RelatedAggregationArgumentsTableComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
}) |
||||
|
export class RelatedAggregationArgumentsTableComponent 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, |
||||
|
defaultValueRequired: true, |
||||
|
argumentEntityTypes: [ArgumentEntityType.Current], |
||||
|
hint: 'calculated-fields.hint.setting-arguments-aggregation' |
||||
|
}; |
||||
|
|
||||
|
this.isScript = false; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="relatedAggregationConfiguration" class="tb-form-panel no-border no-padding"> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" tbTruncateWithTooltip tb-hint-tooltip-icon="{{ 'calculated-fields.hint.aggregation-path-related-entities' | translate }}"> |
||||
|
{{ 'calculated-fields.aggregation-path-related-entities' | translate }} |
||||
|
</div> |
||||
|
<div class="flex gap-3 xs:flex-col" formGroupName="relation"> |
||||
|
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic" hideRequiredMarker> |
||||
|
<mat-label>{{ 'calculated-fields.direction' | translate }}</mat-label> |
||||
|
<mat-select formControlName="direction"> |
||||
|
@for (direction of Directions; track direction) { |
||||
|
<mat-option [value]="direction">{{ PropagationDirectionTranslations.get(direction) | translate }}</mat-option> |
||||
|
} |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)" |
||||
|
class="flex-1" |
||||
|
panelWidth="" |
||||
|
additionalClass="" |
||||
|
required |
||||
|
[label]="'calculated-fields.relation-type' | translate" |
||||
|
[errorText]="'calculated-fields.hint.relation-type-required' | translate" |
||||
|
formControlName="relationType"> |
||||
|
</tb-string-autocomplete> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-panel"> |
||||
|
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.arguments-aggregation' | translate }}"> |
||||
|
{{ 'calculated-fields.arguments' | translate }} |
||||
|
</div> |
||||
|
<tb-related-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' | translate }} |
||||
|
</div> |
||||
|
<tb-time-unit-input required |
||||
|
appearance="outline" |
||||
|
subscriptSizing="dynamic" |
||||
|
labelText="{{ 'calculated-fields.deduplication-interval' | translate }}" |
||||
|
requiredText="{{ 'calculated-fields.deduplication-interval-required' | translate }}" |
||||
|
minErrorText="{{ 'calculated-fields.deduplication-interval-min' | translate: {sec: minAllowedDeduplicationIntervalInSecForCF} }}" |
||||
|
[minTime]="minAllowedDeduplicationIntervalInSecForCF" |
||||
|
formControlName="deduplicationIntervalInSec"> |
||||
|
</tb-time-unit-input> |
||||
|
</div> |
||||
|
<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> |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</tb-calculate-field-output> |
||||
|
</div> |
||||
@ -0,0 +1,154 @@ |
|||||
|
///
|
||||
|
/// 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 { Observable, of } from 'rxjs'; |
||||
|
import { |
||||
|
CalculatedFieldOutput, |
||||
|
CalculatedFieldRelatedAggregationConfiguration, |
||||
|
CalculatedFieldType, |
||||
|
getCalculatedFieldArgumentsEditorCompleter, |
||||
|
getCalculatedFieldArgumentsHighlights, |
||||
|
OutputType, |
||||
|
PropagationDirectionTranslations |
||||
|
} from '@shared/models/calculated-field.models'; |
||||
|
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { ScriptLanguage } from '@app/shared/models/rule-node.models'; |
||||
|
import { EntitySearchDirection } from '@shared/models/relation.models'; |
||||
|
import { getCurrentAuthState } from '@core/auth/auth.selectors'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-related-entities-aggregation-component', |
||||
|
templateUrl: './related-entities-aggregation-component.component.html', |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => RelatedEntitiesAggregationComponentComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
}) |
||||
|
export class RelatedEntitiesAggregationComponentComponent implements ControlValueAccessor, Validator { |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
entityId: EntityId; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
tenantId: string; |
||||
|
|
||||
|
@Input({required: true}) |
||||
|
entityName: string; |
||||
|
|
||||
|
relatedAggregationConfiguration = this.fb.group({ |
||||
|
relation: this.fb.group({ |
||||
|
direction: [EntitySearchDirection.FROM, Validators.required], |
||||
|
relationType: ['Contains', Validators.required], |
||||
|
}), |
||||
|
arguments: this.fb.control({}), |
||||
|
deduplicationIntervalInSec: [], |
||||
|
output: this.fb.control<CalculatedFieldOutput>({ |
||||
|
scope: AttributeScope.SERVER_SCOPE, |
||||
|
type: OutputType.Timeseries, |
||||
|
}), |
||||
|
useLatestTs: [false] |
||||
|
}); |
||||
|
|
||||
|
readonly ScriptLanguage = ScriptLanguage; |
||||
|
readonly CalculatedFieldType = CalculatedFieldType; |
||||
|
readonly OutputType = OutputType; |
||||
|
readonly Directions = Object.values(EntitySearchDirection) as Array<EntitySearchDirection>; |
||||
|
readonly PropagationDirectionTranslations = PropagationDirectionTranslations; |
||||
|
readonly minAllowedDeduplicationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedDeduplicationIntervalInSecForCF; |
||||
|
|
||||
|
|
||||
|
functionArgs$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( |
||||
|
map(argumentsObj => ['ctx', ...Object.keys(argumentsObj)]) |
||||
|
); |
||||
|
|
||||
|
argumentsEditorCompleter$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( |
||||
|
map(argumentsObj => getCalculatedFieldArgumentsEditorCompleter(argumentsObj ?? {})) |
||||
|
); |
||||
|
|
||||
|
argumentsHighlightRules$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( |
||||
|
map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) |
||||
|
); |
||||
|
|
||||
|
private propagateChange: (config: CalculatedFieldRelatedAggregationConfiguration) => void = () => { }; |
||||
|
|
||||
|
constructor(private fb: FormBuilder, |
||||
|
private store: Store<AppState>) { |
||||
|
|
||||
|
this.relatedAggregationConfiguration.valueChanges.pipe( |
||||
|
takeUntilDestroyed() |
||||
|
).subscribe((value: CalculatedFieldRelatedAggregationConfiguration) => { |
||||
|
this.updatedModel(value); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.relatedAggregationConfiguration.valid || this.relatedAggregationConfiguration.status === "DISABLED" ? null : {invalidPropagateConfig: false}; |
||||
|
} |
||||
|
|
||||
|
writeValue(value: CalculatedFieldRelatedAggregationConfiguration): void { |
||||
|
this.relatedAggregationConfiguration.patchValue(value, {emitEvent: false}); |
||||
|
setTimeout(() => { |
||||
|
this.relatedAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: (config: CalculatedFieldRelatedAggregationConfiguration) => void): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_: any): void { } |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
if (isDisabled) { |
||||
|
this.relatedAggregationConfiguration.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.relatedAggregationConfiguration.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fetchOptions(searchText: string): Observable<Array<string>> { |
||||
|
const search = searchText ? searchText?.toLowerCase() : ''; |
||||
|
return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); |
||||
|
} |
||||
|
|
||||
|
private updatedModel(value: CalculatedFieldRelatedAggregationConfiguration): void { |
||||
|
value.type = CalculatedFieldType.RELATED_ENTITIES_AGGREGATION; |
||||
|
this.propagateChange(value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
///
|
||||
|
/// 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 { |
||||
|
RelatedEntitiesAggregationComponentComponent |
||||
|
} from '@home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
SharedModule, |
||||
|
CalculatedFieldOutputModule, |
||||
|
CalculatedFieldArgumentsTableModule, |
||||
|
], |
||||
|
declarations: [ |
||||
|
RelatedEntitiesAggregationComponentComponent, |
||||
|
], |
||||
|
exports: [ |
||||
|
RelatedEntitiesAggregationComponentComponent, |
||||
|
] |
||||
|
}) |
||||
|
export class RelatedEntitiesAggregationComponentModule { |
||||
|
} |
||||
Loading…
Reference in new issue