diff --git a/.github/release.yml b/.github/release.yml index 53b2e3ae65..70852dcbb2 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 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. @@ -23,27 +23,19 @@ changelog: labels: - 'Major Core' - 'Major Rule Engine' - exclude: - labels: - - 'Bug' + - title: 'Major UI' labels: - 'Major UI' - exclude: - labels: - - 'Bug' + - title: 'Major Transport' labels: - 'Major Transport' - exclude: - labels: - - 'Bug' + - title: 'Major Edge' labels: - 'Major Edge' - exclude: - labels: - - 'Bug' + - title: 'Core & Rule Engine' labels: - 'Core' @@ -51,38 +43,63 @@ changelog: exclude: labels: - 'Bug' + - title: 'UI' labels: - 'UI' exclude: labels: - 'Bug' + - title: 'Transport' labels: - 'Transport' exclude: labels: - 'Bug' + - title: 'Edge' labels: - 'Edge' exclude: labels: - 'Bug' + - title: 'Bug: Core & Rule Engine' labels: - - 'Core' - - 'Rule Engine' - 'Bug' + exclude: + labels: + - 'UI' + - 'Transport' + - 'Edge' + - title: 'Bug: UI' labels: - - 'UI' - 'Bug' + exclude: + labels: + - 'Core' + - 'Rule Engine' + - 'Transport' + - 'Edge' + - title: 'Bug: Transport' labels: - - 'Transport' - 'Bug' + exclude: + labels: + - 'Core' + - 'Rule Engine' + - 'UI' + - 'Edge' + - title: 'Bug: Edge' labels: - - 'Edge' - 'Bug' + exclude: + labels: + - 'Core' + - 'Rule Engine' + - 'UI' + - 'Transport' diff --git a/.github/workflows/check-configuration-files.yml b/.github/workflows/check-configuration-files.yml index 561b7d0019..0ea9d61923 100644 --- a/.github/workflows/check-configuration-files.yml +++ b/.github/workflows/check-configuration-files.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 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. diff --git a/.github/workflows/license-header-format.yml b/.github/workflows/license-header-format.yml index 20a1c1350a..0ac41c7bc8 100644 --- a/.github/workflows/license-header-format.yml +++ b/.github/workflows/license-header-format.yml @@ -1,5 +1,5 @@ # -# Copyright © 2016-2025 The Thingsboard Authors +# Copyright © 2016-2026 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. diff --git a/application/pom.xml b/application/pom.xml index 7d304226ad..ab26655052 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -1,6 +1,6 @@ +
+ + + +
+
+
+
+
{{ 'common.general' | translate }}
+
+ + {{ 'entity-field.title' | translate }} + + @if (entityForm.get('name').errors && entityForm.get('name').touched) { + + @if (entityForm.get('name').hasError('required')) { + {{ 'common.hint.title-required' | translate }} + } @else if (entityForm.get('name').hasError('pattern')) { + {{ 'common.hint.title-pattern' | translate }} + } @else if (entityForm.get('name').hasError('maxlength')) { + {{ 'common.hint.title-max-length' | translate }} + } + + } + + + +
+ + +
+ +
+
{{ 'calculated-fields.arguments' | translate }}
+ +
+
+
{{ 'alarm-rule.create-conditions' | translate }}
+
+ + +
+
+
+
{{ 'alarm-rule.clear-condition' | translate }}
+
+
+ + +
+ +
+
+ alarm-rule.no-clear-alarm-rule +
+
+ +
+
+
+ + + {{ 'alarm-rule.advanced-settings' | translate }} + + +
+
+ + {{ 'alarm-rule.propagate-alarm' | translate }} + +
+ @if (configFormGroup.get('propagate').value) { + + + } +
+
+ + {{ 'alarm-rule.propagate-alarm-to-owner' | translate }} + +
+
+ + {{ 'alarm-rule.propagate-alarm-to-tenant' | translate }} + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts new file mode 100644 index 0000000000..6f28a01088 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.ts @@ -0,0 +1,199 @@ +/// +/// Copyright © 2016-2026 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, inject, Inject, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { + CalculatedFieldArgument, + CalculatedFieldConfiguration, + CalculatedFieldInfo, + CalculatedFieldType +} from '@shared/models/calculated-field.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { BaseData } from '@shared/models/base-data'; +import { Observable } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { + CalculatedFieldsTableConfig, + CalculatedFieldsTableEntity +} from '@home/components/calculated-fields/calculated-fields-table-config'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { StringItemsOption } from '@shared/components/string-items-list.component'; +import { RelationTypes } from '@shared/models/relation.models'; +import { + AlarmRule, + AlarmRuleConditionType, + alarmRuleEntityTypeList, + AlarmRuleExpressionType +} from '@shared/models/alarm-rule.models'; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-alarm-rules', + templateUrl: './alarm-rules.component.html', + styleUrls: [] +}) +export class AlarmRulesComponent extends EntityComponent { + + @Input() + standalone = false; + + @Input() + entityName: string; + + ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId); + readonly tenantId = getCurrentAuthUser(this.store).tenantId; + readonly EntityType = EntityType; + readonly alarmRuleEntityTypeList = alarmRuleEntityTypeList; + readonly CalculatedFieldType = CalculatedFieldType; + + private cfFormService = inject(CalculatedFieldFormService); + private destroyRef = inject(DestroyRef); + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: CalculatedFieldInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig, + protected fb: FormBuilder, + protected cd: ChangeDetectorRef, + private entityService: EntityService) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + additionalDebugActionConfig = { + ...this.entitiesTableConfig.additionalDebugActionConfig, + action: () => this.entitiesTableConfig.additionalDebugActionConfig.action( + { id: this.entity.id, ...this.entityFormValue() }, false, + (expression) => { + if (expression) { + this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression}); + this.entityForm.get('configuration').markAsDirty(); + } + }), + }; + + get entityId(): EntityId { + return this.entityForm.get('entityId').value; + } + + get entitiesTableConfig(): CalculatedFieldsTableConfig { + return this.entitiesTableConfigValue; + } + + changeEntity(entity: BaseData): void { + this.entityName = entity?.name; + } + + buildForm(_entity?: CalculatedFieldInfo): FormGroup { + return inject(CalculatedFieldFormService).buildAlarmRuleForm(); + } + + updateForm(entity: CalculatedFieldInfo) { + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {}; + this.entityForm.patchValue({ configuration, debugSettings, entityId, ...value }, {emitEvent: false}); + if (!entityId) { + this.entityForm.get('configuration').disable({emitEvent: false}); + } + } + + onTestScript(expression?: string): Observable { + return this.cfFormService.testScript( + this.entity?.id?.id, + this.entityFormValue(), + this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig), + this.destroyRef, + expression + ); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + this.entityForm.get('entityId').disable({emitEvent: false}); + this.getOwnerId(this.entityId); + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + get arguments(): Record { + return this.entityForm.get('configuration.arguments').value; + } + + get predefinedTypeValues(): StringItemsOption[] { + return RelationTypes.map(type => ({ + name: type, + value: type + })); + } + + get configFormGroup(): FormGroup { + return this.entityForm.get('configuration') as FormGroup; + } + + public removeClearAlarmRule() { + this.configFormGroup.patchValue({clearRule: null}); + this.entityForm.markAsDirty(); + } + + public addClearAlarmRule() { + const clearAlarmRule: AlarmRule = { + condition: { + type: AlarmRuleConditionType.SIMPLE, + expression: { + type: AlarmRuleExpressionType.SIMPLE + } + } + }; + this.configFormGroup.patchValue({clearRule: clearAlarmRule}); + } + + getOwnerId(entityId: EntityId) { + if (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.ASSET) { + this.entityService.getEntity(entityId.entityType, entityId.id, { ignoreLoading: true, ignoreErrors: true }).subscribe( + (entity: AssetInfo | DeviceInfo) => { + if (this.isAssignedToCustomer(entity)) { + this.ownerId = entity.customerId; + } + } + ); + } + } + + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html index 90ae6f7c4b..6a9f8d7f54 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html @@ -1,6 +1,6 @@ -
+
{{ 'alarm-rule.condition' | translate }}
-
{{ 'alarm-rule.schedule-title' | translate }}
-
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss index ac77089857..62876c2476 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts index 08004d7497..8ff7e2c083 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. @@ -137,19 +137,27 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali writeValue(value: AlarmRuleCondition): void { this.modelValue = value; this.updateConditionInfo(); - if (value) { - this.onValidatorChange(); - } + this.recalculateArgumentValidity(); } ngOnChanges(changes: SimpleChanges) { if (changes.arguments) { - if (changes.arguments && !changes.arguments.firstChange && this.modelValue) { - this.onValidatorChange(); + if (changes.arguments && !changes.arguments.firstChange) { + this.recalculateArgumentValidity(); } } } + private recalculateArgumentValidity(): void { + if (!this.modelValue || !this.arguments) { + this.filtersArgumentsValid = true; + this.schedulerArgumentsValid = true; + return; + } + this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, this.arguments); + this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); + } + private isScheduleArgumentValid(obj: any, validArguments: string[]): boolean { const arg = obj?.schedule?.dynamicValueArgument; return !arg || validArguments.includes(arg); @@ -173,21 +181,14 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali return true; } - public conditionSet() { - return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters); + public conditionSet(): boolean { + return !!this.modelValue && !!(this.modelValue.expression?.expression || this.modelValue.expression?.filters); } public validate(control: AbstractControl): ValidationErrors | null { - this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, this.arguments); - this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments)); - this.onValidatorChange = () => { - control.updateValueAndValidity({ emitEvent: true }); - }; - return this.conditionSet() && this.filtersArgumentsValid && this.schedulerArgumentsValid ? null : { - alarmRuleCondition: { - valid: false, - } - }; + const hasCondition = this.conditionSet(); + const argsValid = this.filtersArgumentsValid && this.schedulerArgumentsValid; + return (hasCondition && argsValid) ? null : { alarmRuleCondition: { valid: false } }; } public openFilterDialog($event: Event) { @@ -208,7 +209,6 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali if (result) { this.modelValue = {...this.modelValue, ...result}; this.updateModel(); - this.cd.detectChanges(); } }); } @@ -274,10 +274,10 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali private updateModel() { this.updateConditionInfo(); + this.recalculateArgumentValidity(); this.propagateChange(this.modelValue); - if (this.modelValue) { - this.onValidatorChange(); - } + this.onValidatorChange(); + this.cd.detectChanges(); } public openScheduleDialog($event: Event) { @@ -297,13 +297,12 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali if (result) { this.modelValue.schedule = result; this.updateModel(); - this.cd.detectChanges(); } }); } private updateScheduleText() { - let schedule = this.modelValue?.schedule; + const schedule = this.modelValue?.schedule; this.scheduleText = ''; if (isDefinedAndNotNull(schedule)) { if (schedule.dynamicValueArgument) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html index a7ccc25fe6..47b6281fdd 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -1,6 +1,6 @@ -
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss index cea9c68955..53283aad68 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts index cd6e06f293..1ba769a19e 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss index 6253aa9a4b..f38df65c29 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html index 4caecbcc34..3908d919b3 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html @@ -1,6 +1,6 @@ -
@for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) {
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss index c00cf6af9c..7250c86ab2 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts index 8959d709fa..1c56cdbbad 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. @@ -27,10 +27,9 @@ import { Validator, Validators } from '@angular/forms'; -import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; +import { AlarmSeverity, alarmSeverityColors, alarmSeverityTranslations } from '@shared/models/alarm.models'; import { AlarmRule } from "@shared/models/alarm-rule.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; -import { AlarmSeverityNotificationColors } from "@shared/models/notification.models"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { coerceBoolean } from "@shared/decorators/coercion"; import { Observable } from "rxjs"; @@ -68,7 +67,7 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida alarmSeverityEnum = AlarmSeverity; alarmSeverityTranslationMap = alarmSeverityTranslations; - AlarmSeverityNotificationColors = AlarmSeverityNotificationColors; + AlarmSeverityNotificationColors = alarmSeverityColors; createAlarmRulesFormGroup = this.fb.group({ createAlarmRules: this.fb.array<{severity: AlarmSeverity, alarmRule: AlarmRule}>([]) @@ -115,13 +114,13 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida } createAlarmRulesControls.push(this.fb.group({ severity: [severity, Validators.required], - alarmRule: [createAlarmRule, Validators.required] + alarmRule: [{value: createAlarmRule, disabled: this.disabled}, Validators.required] })); }); } const formArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; - formArray.clear(); - createAlarmRulesControls.forEach(c => formArray.push(c)); + formArray.clear({emitEvent: false}); + createAlarmRulesControls.forEach(c => formArray.push(c, {emitEvent: false})); if (this.disabled) { this.createAlarmRulesFormGroup.disable({emitEvent: false}); } else { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html index c0c5a72d77..194e1cb715 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html @@ -1,6 +1,6 @@ -
{{ 'api-key.edit-description' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.scss b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.scss index 607339a886..5dcf029470 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 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. @@ -14,7 +14,6 @@ * limitations under the License. */ - .tb-edit-api-key-description-panel { --mdc-outlined-text-field-outline-color: rgba(0,0,0,0.12); diff --git a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts index 80985f6121..1614ea1ed3 100644 --- a/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index 0dc4e0a90f..fa791d479e 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -1,6 +1,6 @@ - + + +
+
+
+
+
{{ 'common.general' | translate }}
+
+ + {{ 'entity-field.title' | translate }} + + @if (entityForm.get('name').errors && entityForm.get('name').touched) { + + @if (entityForm.get('name').hasError('required')) { + {{ 'common.hint.title-required' | translate }} + } @else if (entityForm.get('name').hasError('pattern')) { + {{ 'common.hint.title-pattern' | translate }} + } @else if (entityForm.get('name').hasError('maxlength')) { + {{ 'common.hint.title-max-length' | translate }} + } + + } + + + +
+ + + + {{ 'common.type' | translate }} + + + {{ CalculatedFieldTypeTranslations.has(entityForm.get('type').value) + ? (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).name | translate) + : entityForm.get('type').value }} + + @for (type of fieldTypes; track type) { + + + {{ CalculatedFieldTypeTranslations.get(type).name | translate }} + +
+ + {{ CalculatedFieldTypeTranslations.get(type).hint | translate }} + +
+ } +
+
+
+ @switch (entityForm.get('type').value) { + @case (CalculatedFieldType.GEOFENCING) { + + } + @case (CalculatedFieldType.PROPAGATION) { + + } + @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { + + } + @case (CalculatedFieldType.ENTITY_AGGREGATION) { + + } + @default { + + } + } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss new file mode 100644 index 0000000000..695df5063a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.scss @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2026 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. + */ +::ng-deep { + .tbel-script-lang-chip { + line-height: 20px; + font-size: 14px; + font-weight: 500; + color: white; + border-radius: 100px; + width: 70px; + min-width: 70px; + display: flex; + justify-content: center; + margin-top: 2px; + margin-right: 4px; + } + + .tb-js-func { + .ace_tb { + &.ace_calculated-field { + &-ctx { + color: #C52F00; + } + &-args { + color: #185F2A; + } + &-key { + color: #c24c1a; + } + &-time-window, &-values, &-func, &-value, &-ts, &-latestTs { + color: #7214D0; + } + &-start-ts, &-end-ts { + color: #2CAA00; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts new file mode 100644 index 0000000000..20ebe549a2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.ts @@ -0,0 +1,167 @@ +/// +/// Copyright © 2016-2026 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, inject, Inject, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityComponent } from '../../components/entity/entity.component'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { + CalculatedFieldConfiguration, + CalculatedFieldInfo, + calculatedFieldsEntityTypeList, + CalculatedFieldType, + calculatedFieldTypes, + CalculatedFieldTypeTranslations +} from '@shared/models/calculated-field.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { BaseData } from '@shared/models/base-data'; +import { Observable } from 'rxjs'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { + CalculatedFieldsTableConfig, + CalculatedFieldsTableEntity +} from '@home/components/calculated-fields/calculated-fields-table-config'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; +import { AssetInfo } from '@shared/models/asset.models'; +import { DeviceInfo } from '@shared/models/device.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntityService } from '@core/http/entity.service'; + +@Component({ + selector: 'tb-calculated-field', + templateUrl: './calculated-field.component.html', + styleUrls: ['./calculated-field.component.scss'] +}) +export class CalculatedFieldComponent extends EntityComponent { + + @Input() + standalone = false; + + @Input() + entityName: string; + + disabledConfiguration = false; + + ownerId = new TenantId(getCurrentAuthUser(this.store).tenantId); + readonly tenantId = getCurrentAuthUser(this.store).tenantId; + readonly EntityType = EntityType; + readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList; + readonly CalculatedFieldType = CalculatedFieldType; + readonly fieldTypes = calculatedFieldTypes; + readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; + + private cfFormService = inject(CalculatedFieldFormService); + private destroyRef = inject(DestroyRef); + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: CalculatedFieldInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: CalculatedFieldsTableConfig, + protected fb: FormBuilder, + protected cd: ChangeDetectorRef, + private entityService: EntityService) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + additionalDebugActionConfig = { + ...this.entitiesTableConfig.additionalDebugActionConfig, + action: () => this.entitiesTableConfig.additionalDebugActionConfig.action( + { id: this.entity.id, ...this.entityFormValue() }, false, + (expression) => { + if (expression) { + this.entityForm.get('configuration').setValue({...this.entityFormValue().configuration, expression}); + this.entityForm.get('configuration').markAsDirty(); + } + }), + }; + + get entityId(): EntityId { + return this.entityForm.get('entityId').value; + } + + get entitiesTableConfig(): CalculatedFieldsTableConfig { + return this.entitiesTableConfigValue; + } + + changeEntity(entity: BaseData): void { + this.entityName = entity?.name; + } + + buildForm(_entity?: CalculatedFieldInfo): FormGroup { + const form = inject(CalculatedFieldFormService).buildForm(); + inject(CalculatedFieldFormService).setupTypeChange(form, inject(DestroyRef), () => this.isEditValue); + return form; + } + + updateForm(entity: CalculatedFieldInfo) { + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.entityId, ...value } = entity ?? {}; + const preparedConfig = this.cfFormService.prepareConfig(configuration); + this.entityForm.patchValue({ type }, {emitEvent: false, onlySelf: true}); + setTimeout(() => { + this.entityForm.patchValue({ configuration: preparedConfig, debugSettings, entityId, ...value }, {emitEvent: false}); + }); + } + + onTestScript(expression?: string): Observable { + return this.cfFormService.testScript( + this.entity?.id?.id, + this.entityFormValue(), + this.entitiesTableConfig.getTestScriptDialog.bind(this.entitiesTableConfig), + this.destroyRef, + expression + ); + } + + updateFormState() { + if (this.entityForm) { + if (this.isEditValue) { + this.entityForm.enable({emitEvent: false}); + this.entityForm.get('entityId').disable({emitEvent: false}); + this.getOwnerId(this.entityId); + } else { + this.entityForm.disable({emitEvent: false}); + } + } + } + + getOwnerId(entityId: EntityId) { + if (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.ASSET) { + this.entityService.getEntity(entityId.entityType, entityId.id, { ignoreLoading: true, ignoreErrors: true }).subscribe( + (entity: AssetInfo | DeviceInfo) => { + if (this.isAssignedToCustomer(entity)) { + this.ownerId = entity.customerId; + } + } + ); + } + } + + private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { + return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts index e3f5ac9cc2..c58b595ba1 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. @@ -50,6 +50,7 @@ import { import { CalculatedFieldsFilterConfigComponent } from '@home/components/calculated-fields/table-header/calculated-fields-filter-config.component'; +import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component'; @NgModule({ declarations: [ @@ -57,7 +58,8 @@ import { CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestArgumentsComponent, CalculatedFieldsHeaderComponent, - CalculatedFieldsFilterConfigComponent + CalculatedFieldsFilterConfigComponent, + CalculatedFieldComponent, ], imports: [ CommonModule, @@ -72,6 +74,7 @@ import { exports: [ CalculatedFieldDialogComponent, CalculatedFieldScriptTestDialogComponent, + CalculatedFieldComponent, ] }) export class CalculatedFieldsModule {} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 8385b5cb45..efb767f92e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. @@ -20,7 +20,7 @@ import { EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { TranslateService } from '@ngx-translate/core'; import { Direction } from '@shared/models/page/sort-order'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; @@ -35,7 +35,7 @@ import { DestroyRef, Renderer2 } from '@angular/core'; import { EntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; -import { catchError, filter, switchMap, tap } from 'rxjs/operators'; +import { catchError, filter, first, switchMap, tap } from 'rxjs/operators'; import { ArgumentEntityType, ArgumentType, @@ -46,6 +46,7 @@ import { CalculatedFieldsQuery, CalculatedFieldType, CalculatedFieldTypeTranslations, + debugCfActionEnabled, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, PropagationWithExpression, @@ -62,20 +63,26 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity import { DatePipe } from '@angular/common'; import { UtilsService } from "@core/services/utils.service"; import { ActionNotificationShow } from "@core/notification/notification.actions"; -import { CalculatedFieldEventBody, DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models'; +import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; import { CalculatedFieldsHeaderComponent } from '@home/components/calculated-fields/table-header/calculated-fields-header.component'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { CalculatedFieldComponent } from '@home/components/calculated-fields/calculated-field.component'; +import { Router } from '@angular/router'; +import { CalculatedFieldsTabsComponent } from '@home/pages/calculated-fields/calculated-fields-tabs.component'; -type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo; +export type CalculatedFieldsTableEntity = CalculatedField | CalculatedFieldInfo; export class CalculatedFieldsTableConfig extends EntityTableConfig { readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { title: this.translate.instant('action.see-debug-events'), - action: (calculatedField: CalculatedFieldsTableEntity) => this.openDebugEventsDialog.call(this, null, calculatedField), + action: (calculatedField: CalculatedFieldsTableEntity, + openCalculatedFieldEdit = true, + afterCloseCallback?: (expression: string) => void) => this.openDebugEventsDialog.call(this, null, calculatedField, openCalculatedFieldEdit, afterCloseCallback), }; calculatedFieldFilterConfig: CalculatedFieldsQuery; @@ -93,30 +100,35 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { - this.editCalculatedField($event, entity); - this.rowPointer = true; - return true; - }; + this.entityComponent = CalculatedFieldComponent; + this.entityTabsComponent = CalculatedFieldsTabsComponent; + this.rowPointer = true; } - this.tableTitle = this.pageMode ? '' : this.translate.instant('entity.type-calculated-fields'); - this.detailsPanelEnabled = false; + this.tableTitle = this.translate.instant('entity.type-calculated-fields'); + this.detailsPanelEnabled = this.pageMode; this.entityType = EntityType.CALCULATED_FIELD; this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD); + this.entityResources = entityTypeResources.get(EntityType.CALCULATED_FIELD); this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink); this.addEntity = this.getCalculatedFieldDialog.bind(this); + this.saveEntity = (cf) => this.calculatedFieldsService.saveCalculatedField(cf); + this.loadEntity = id => this.calculatedFieldsService.getCalculatedFieldById(id.id); this.deleteEntityTitle = (field) => this.translate.instant('calculated-fields.delete-title', {title: field.name}); this.deleteEntityContent = () => this.translate.instant('calculated-fields.delete-text'); this.deleteEntitiesTitle = count => this.translate.instant('calculated-fields.delete-multiple-title', {count}); this.deleteEntitiesContent = () => this.translate.instant('calculated-fields.delete-multiple-text'); this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id); + + this.onEntityAction = action => this.onCFAction(action); + this.addActionDescriptors = [ { name: this.translate.instant('calculated-fields.create'), @@ -163,7 +175,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, - onAction: ($event, entity) => this.openDebugEventsDialog($event, entity), + onAction: ($event, entity) => + this.pageMode ? this.openDebugTab($event, entity) : this.openDebugEventsDialog($event, entity), }, { name: '', @@ -172,14 +185,16 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline', onAction: ($event, entity) => this.onOpenDebugConfig($event, entity), - }, - { + } + ); + if (!this.pageMode) { + this.cellActionDescriptors.push({ name: this.translate.instant('action.edit'), icon: 'edit', isEnabled: () => true, onAction: ($event, entity) => this.editCalculatedField($event, entity), - } - ); + }) + } } fetchCalculatedFields(pageLink: PageLink): Observable> { @@ -188,7 +203,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig 1) { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + } else { + table.entityDetailsPanel.matTabGroup._tabs.changes.pipe( + first() + ).subscribe(() => { + table.entityDetailsPanel.matTabGroup.selectedIndex = 1; + }) + } + } + } + private editCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity, isDirty = false): void { $event?.stopPropagation(); this.getCalculatedFieldDialog(calculatedField, 'action.apply', isDirty) @@ -222,7 +253,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + private getCalculatedFieldDialog(value?: CalculatedFieldsTableEntity, buttonTitle = 'action.add', isDirty = false, disabledSelectType = false): Observable { const entityId = this.entityId || value?.entityId; const entityName = this.entityName || (value as CalculatedFieldInfo)?.entityName; return this.dialog.open(CalculatedFieldDialogComponent, { @@ -234,10 +265,11 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig void ): void { $event?.stopPropagation(); - const debugActionEnabledFn = (event: DebugEvent) => { - return (calculatedField.type === CalculatedFieldType.SCRIPT || - (calculatedField.type === CalculatedFieldType.PROPAGATION && - calculatedField.configuration.applyExpressionToResolvedArguments) - ) && !!(event as DebugEvent).body.arguments; - }; const onDebugEventSelected = (event: CalculatedFieldEventBody, dialogRef: MatDialogRef) => { - this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments)) + this.getTestScriptDialog(calculatedField, JSON.parse(event.arguments), openCalculatedFieldEdit) .subscribe(expression => dialogRef.close(expression)); }; @@ -270,11 +296,15 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + if (afterCloseCallback) { + afterCloseCallback(value) + } + }); } private exportCalculatedField($event: Event, calculatedField: CalculatedFieldsTableEntity): void { @@ -303,7 +333,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { - if (calculatedField.type === CalculatedFieldType.ALARM) { + if (calculatedField.type === CalculatedFieldType.ALARM || !CalculatedFieldType[calculatedField.type]) { this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('calculated-fields.hint.import-invalid-calculated-field-type'), type: 'error', @@ -315,7 +345,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)), + switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true, true)), filter(Boolean), switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)), filter(Boolean), @@ -354,7 +384,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig this.updateData()); } - private getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { + getTestScriptDialog(calculatedField: CalculatedFieldsTableEntity, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable { if ( calculatedField.type === CalculatedFieldType.SCRIPT || calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION || @@ -394,4 +424,22 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig): boolean { + switch (action.action) { + case 'open': + this.openCalculatedField(action.event, action.entity); + return true; + case 'export': + this.exportCalculatedField(action.event, action.entity); + return true; + } + return false; + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html index 47a3fdeaf4..102a334402 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html @@ -1,6 +1,6 @@
-
+
{{ 'calculated-fields.arguments' | translate }}
@@ -29,7 +29,7 @@ [entityName]="entityName"/>
-
+
{{ 'calculated-fields.metrics.metrics' | translate }}
{ this.entityAggregationConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts index 44bbbc7237..347d99ed15 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html index 9f4e331cc1..bb191e8f26 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html @@ -1,6 +1,6 @@
-
{{ 'calculated-fields.output' | translate }}
+
+ {{ 'calculated-fields.output' | translate }} +
@@ -28,7 +30,7 @@ @if (outputForm.get('type').value === OutputType.Attribute - && (entityId.entityType === EntityType.DEVICE || entityId.entityType === EntityType.DEVICE_PROFILE)) { + && (entityId?.entityType === EntityType.DEVICE || entityId?.entityType === EntityType.DEVICE_PROFILE)) { {{ 'calculated-fields.attribute-scope' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss index 4b0d065555..b0d282b7f4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2025 The Thingsboard Authors + * Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts index f1e8646a6c..972b74a9f7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. @@ -148,7 +148,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val for (const propName of Object.keys(changes)) { const change = changes[propName]; if (change.currentValue !== change.previousValue) { - if (propName === 'simpleMode') { + if (propName === 'simpleMode' && !this.disabled) { this.updatedFormWithMode(); if (!change.firstChange) { this.outputForm.updateValueAndValidity(); @@ -167,7 +167,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val if (value.type === OutputType.Timeseries && value.strategy?.type === OutputStrategyType.IMMEDIATE && value.strategy?.ttl) { this.outputForm.get('strategy.useCustomTtl').setValue(true, {emitEvent: false}); } - this.outputForm.get('type').updateValueAndValidity({onlySelf: true}); + this.outputForm.get('type').updateValueAndValidity({onlySelf: true, emitEvent: false}); } registerOnChange(fn: (config: CalculatedFieldOutput | CalculatedFieldSimpleOutput) => void): void { @@ -253,7 +253,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val private updateTimeSeriesTtl(value: boolean) { if (value) { - this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: true}); + this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: false}); } else { this.outputForm.get('strategy.useCustomTtl').disable({emitEvent: false}); this.outputForm.get('strategy.ttl').disable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts index 83b3970d9b..848aa1d5de 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html index 5e6726219f..5b31034791 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html @@ -1,6 +1,6 @@
-
{{ 'calculated-fields.arguments' | translate }}
+
+ {{ 'calculated-fields.arguments' | translate }} +
-
{{ (isScript ? 'calculated-fields.type.script' : 'calculated-fields.expression') | translate }}
+
+ {{ (isScript ? 'calculated-fields.type.script' : 'calculated-fields.expression') | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts index e6ff1d1bad..fe423e2aaa 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. @@ -151,7 +151,6 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid formValue.expressionSIMPLE = formValue.expression; } this.simpleConfiguration.patchValue(formValue, {emitEvent: false}); - this.updatedFormWithScript(); setTimeout(() => { this.simpleConfiguration.get('arguments').updateValueAndValidity({onlySelf: true}); }); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts index 2e5e14426e..11c6899ae6 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html index f393a9130b..83b758a6d8 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component.html @@ -1,6 +1,6 @@ - + , protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -87,6 +88,8 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { this.mapWidgetConfigForm = this.fb.group({ mapSettings: [settings, []], + timewindowConfig: [getTimewindowConfig(configData.config), []], + showTitle: [configData.config.showTitle, []], title: [configData.config.title, []], titleFont: [configData.config.titleFont, []], @@ -106,15 +109,10 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { actions: [configData.config.actions || {}, []] }); - if (this.trip) { - this.mapWidgetConfigForm.addControl('timewindowConfig', this.fb.control(getTimewindowConfig(configData.config))) - } } protected prepareOutputConfig(config: any): WidgetConfigComponentData { - if (this.trip) { - setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); - } + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); this.widgetConfig.config.settings = config.mapSettings || {}; this.widgetConfig.config.showTitle = config.showTitle; @@ -186,4 +184,20 @@ export class MapBasicConfigComponent extends BasicWidgetConfigComponent { config.enableFullscreen = buttons.includes('fullscreen'); } + public get displayTimewindowConfig(): boolean { + if (this.trip) { + return true; + } else { + return this.widget ? MapModelDefinition.hasTimewindow(this.widget) : false; + } + } + + public get onlyHistoryTimewindow(): boolean { + if (this.trip) { + return false; + } else { + return this.widget ? MapModelDefinition.datasourcesHasOnlyComparisonAggregation(this.widget) : false; + } + } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html index 4dab1821cb..606f092199 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html @@ -1,6 +1,6 @@ -
this.onMouseDown(e.originalEvent)); $(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent)); @@ -220,7 +220,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O widgetActionAbsolute(widgetComponent: WidgetComponent, absolute = false) { return absolute ? true : !(this.widget.showWidgetTitlePanel && !widgetComponent.widgetContext?.embedTitlePanel && - (this.widget.showTitle||this.widget.hasAggregation)) && !widgetComponent.widgetContext?.embedActionsPanel; + (this.widget.showTitle||this.widget.hasTimewindow)) && !widgetComponent.widgetContext?.embedActionsPanel; } onClicked(event: MouseEvent): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html index 1c56707563..115b13c5e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html @@ -1,6 +1,6 @@ +@if (entity) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts new file mode 100644 index 0000000000..003c1f3b71 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm-rules-tabs.component.ts @@ -0,0 +1,49 @@ +/// +/// Copyright © 2016-2026 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 } from '@angular/core'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; +import type { + CalculatedFieldsTableConfig, + CalculatedFieldsTableEntity +} from '@home/components/calculated-fields/calculated-fields-table-config'; +import { debugCfActionEnabled } from '@shared/models/calculated-field.models'; + +@Component({ + selector: 'tb-alarm-rules-tabs', + templateUrl: './alarm-rules-tabs.component.html', + styleUrls: [] +}) +export class AlarmRulesTabsComponent extends EntityTabsComponent { + + readonly DebugEventType = DebugEventType; + readonly EventType = EventType; + + constructor() { + super(); + } + + get debugActionDisabled(): boolean { + return !debugCfActionEnabled(this.entity); + }; + + onDebugEventSelected(event: CalculatedFieldEventBody) { + (this.entitiesTableConfig as CalculatedFieldsTableConfig).getTestScriptDialog(this.entity, JSON.parse(event.arguments)) + .subscribe((expression) => { + }); + }; +} diff --git a/ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts b/ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts index 312259b217..4e77b8f5ff 100644 --- a/ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts +++ b/ui-ngx/src/app/modules/home/pages/alarm/alarm.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. @@ -20,9 +20,12 @@ import { SharedModule } from '@shared/shared.module'; import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { AlarmRoutingModule } from '@home/pages/alarm/alarm-routing.module'; +import { AlarmRulesTabsComponent } from '@home/pages/alarm/alarm-rules-tabs.component'; @NgModule({ - declarations: [], + declarations: [ + AlarmRulesTabsComponent + ], imports: [ CommonModule, SharedModule, diff --git a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts index a57ceb480c..05435d6764 100644 --- a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.module.ts b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.module.ts index 3199537a96..dfeaf0f8e5 100644 --- a/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.module.ts +++ b/ui-ngx/src/app/modules/home/pages/api-usage/api-usage.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts index 2673d5b523..a606477762 100644 --- a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html index 9db6999179..c14adbf8be 100644 --- a/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html @@ -1,6 +1,6 @@ +@if (entity) { + + + +} diff --git a/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts new file mode 100644 index 0000000000..acaacef7a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/calculated-fields/calculated-fields-tabs.component.ts @@ -0,0 +1,57 @@ +/// +/// Copyright © 2016-2026 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 } from '@angular/core'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { CalculatedFieldEventBody, DebugEventType, EventType } from '@shared/models/event.models'; +import type { + CalculatedFieldsTableConfig, + CalculatedFieldsTableEntity +} from '@home/components/calculated-fields/calculated-fields-table-config'; +import { debugCfActionEnabled } from '@shared/models/calculated-field.models'; + +@Component({ + selector: 'tb-calculated-fields-tabs', + templateUrl: './calculated-fields-tabs.component.html', + styleUrls: [] +}) +export class CalculatedFieldsTabsComponent extends EntityTabsComponent { + + readonly DebugEventType = DebugEventType; + readonly EventType = EventType; + + constructor() { + super(); + } + + get debugActionDisabled(): boolean { + return !debugCfActionEnabled(this.entity); + }; + + onDebugEventSelected(event: CalculatedFieldEventBody) { + (this.entitiesTableConfig as CalculatedFieldsTableConfig).getTestScriptDialog(this.entity, JSON.parse(event.arguments), false) + .subscribe((expression) => { + (this.entitiesTableConfig as CalculatedFieldsTableConfig).getTable(); + const entityDetailsPanel = this.entitiesTableConfig.getTable().entityDetailsPanel; + entityDetailsPanel.onToggleEditMode(true); + entityDetailsPanel.selectedTab = 0; + setTimeout(() => { + entityDetailsPanel.detailsForm.get('configuration').setValue({...this.entity.configuration, expression}); + entityDetailsPanel.detailsForm.get('configuration').markAsDirty(); + }); + }); + }; +} diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts index 8eaa13a9f9..55d33c833b 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts @@ -1,5 +1,5 @@ /// -/// Copyright © 2016-2025 The Thingsboard Authors +/// Copyright © 2016-2026 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. diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html b/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html index 0a0e05ffdd..1c0bb7322b 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html +++ b/ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html @@ -1,6 +1,6 @@