diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts
index b73847ef83..cfc3fed4ba 100644
--- a/ui-ngx/src/app/core/auth/auth.models.ts
+++ b/ui-ngx/src/app/core/auth/auth.models.ts
@@ -27,6 +27,7 @@ export interface SysParamsState {
mobileQrEnabled: boolean;
userSettings: UserSettings;
maxResourceSize: number;
+ maxRuleNodeDebugDurationMinutes: number;
}
export interface SysParams extends SysParamsState {
diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts
index 73e0d04eb1..e2ff1fae25 100644
--- a/ui-ngx/src/app/core/auth/auth.reducer.ts
+++ b/ui-ngx/src/app/core/auth/auth.reducer.ts
@@ -31,7 +31,8 @@ const emptyUserAuthState: AuthPayload = {
persistDeviceStateToTelemetry: false,
mobileQrEnabled: false,
maxResourceSize: 0,
- userSettings: initialUserSettings
+ userSettings: initialUserSettings,
+ maxRuleNodeDebugDurationMinutes: 0,
};
export const initialState: AuthState = {
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
index d511125eec..2e5d698121 100644
--- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
@@ -147,6 +147,22 @@
+
+
+ tenant-profile.maximum-rule-node-debug-duration-min
+
+
+ {{ 'tenant-profile.maximum-rule-node-debug-duration-min-range' | translate}}
+
+
+ {{ 'tenant-profile.maximum-rule-node-debug-duration-min-required' | translate}}
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
index f7261e7957..860328fb80 100644
--- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
+++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
@@ -85,6 +85,7 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
tenantNotificationRequestsRateLimit: [null, []],
tenantNotificationRequestsPerRuleRateLimit: [null, []],
maxTransportMessages: [null, [Validators.required, Validators.min(0)]],
+ maxRuleNodeDebugDurationMinutes: [null, [Validators.required, Validators.min(0)]],
maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]],
maxREExecutions: [null, [Validators.required, Validators.min(0)]],
maxJSExecutions: [null, [Validators.required, Validators.min(0)]],
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts
new file mode 100644
index 0000000000..17176bd4af
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts
@@ -0,0 +1,37 @@
+///
+/// Copyright © 2016-2024 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 { Pipe, PipeTransform } from '@angular/core';
+import { MINUTE } from '@shared/models/time/time.models';
+
+@Pipe({
+ name: 'debugDurationLeft',
+ pure: false,
+ standalone: true,
+})
+export class DebugDurationLeftPipe implements PipeTransform {
+ transform(lastUpdateTs: number, maxRuleNodeDebugDurationMinutes: number): string {
+ const minutes = Math.max(0, Math.floor(this.getDebugTime(lastUpdateTs, maxRuleNodeDebugDurationMinutes) / MINUTE));
+ return `${minutes} min left`;
+ }
+
+ getDebugTime(lastUpdateTs: number, maxRuleNodeDebugDurationMinutes: number): number {
+ const maxDuration = maxRuleNodeDebugDurationMinutes * MINUTE;
+ const updateDuration = lastUpdateTs ? new Date().getTime() - lastUpdateTs : 0;
+ const leftTime = maxDuration - updateDuration;
+ return leftTime > 0 ? leftTime : 0;
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html
new file mode 100644
index 0000000000..800f54ae3e
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html
@@ -0,0 +1,32 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts
new file mode 100644
index 0000000000..9a158d6a85
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts
@@ -0,0 +1,120 @@
+///
+/// Copyright © 2016-2024 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,
+ Input,
+ forwardRef,
+ Renderer2,
+ ViewContainerRef,
+ DestroyRef,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef
+} from '@angular/core';
+import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { SharedModule } from '@shared/shared.module';
+import { DebugStrategy } from '@shared/models/rule-node.models';
+import { DebugDurationLeftPipe } from '@home/pages/rulechain/debug-duration-left.pipe';
+import { getCurrentAuthState } from '@core/auth/auth.selectors';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { TbPopoverService } from '@shared/components/popover.service';
+import { MatButton } from '@angular/material/button';
+import { DebugStrategyPanelComponent } from '@home/pages/rulechain/debug-strategy-panel.component';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { interval } from 'rxjs';
+import { MINUTE } from '@shared/models/time/time.models';
+
+@Component({
+ selector: 'tb-debug-strategy-button',
+ templateUrl: './debug-strategy-button.component.html',
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => DebugStrategyButtonComponent),
+ multi: true
+ }
+ ],
+ standalone: true,
+ imports: [
+ CommonModule,
+ SharedModule,
+ DebugDurationLeftPipe,
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class DebugStrategyButtonComponent implements ControlValueAccessor {
+
+ @Input() disabled = false;
+ @Input() lastUpdateTs: number;
+
+ debugStrategyFormControl: FormControl;
+
+ private onChange: (debugStrategy: DebugStrategy) => void;
+
+ readonly maxRuleNodeDebugDurationMinutes = getCurrentAuthState(this.store).maxRuleNodeDebugDurationMinutes;
+ readonly DebugStrategy = DebugStrategy;
+
+ constructor(protected store: Store,
+ private fb: UntypedFormBuilder,
+ private popoverService: TbPopoverService,
+ private renderer: Renderer2,
+ private viewContainerRef: ViewContainerRef,
+ private destroyRef: DestroyRef,
+ private cdr: ChangeDetectorRef
+ ) {
+ this.debugStrategyFormControl = this.fb.control(DebugStrategy.DISABLED);
+ this.debugStrategyFormControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.onChange(value));
+ interval(0.5 * MINUTE)
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(() => this.cdr.markForCheck());
+ }
+
+ openDebugStrategyPanel($event: Event, matButton: MatButton): void {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ const trigger = matButton._elementRef.nativeElement;
+ const debugStrategy = this.debugStrategyFormControl.value;
+ if (this.popoverService.hasPopover(trigger)) {
+ this.popoverService.hidePopover(trigger);
+ } else {
+ const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer,
+ this.viewContainerRef, DebugStrategyPanelComponent, 'bottom', true, null,
+ { debugStrategy },
+ {},
+ {}, {}, true);
+ debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover;
+ debugStrategyPopover.tbComponentRef.instance.onStrategyApplied.subscribe((strategy: DebugStrategy) => {
+ this.debugStrategyFormControl.patchValue(strategy);
+ this.cdr.markForCheck();
+ debugStrategyPopover.hide();
+ });
+ }
+ }
+
+ registerOnChange(onChange: (debugStrategy: DebugStrategy) => void): void {
+ this.onChange = onChange;
+ }
+
+ registerOnTouched(_: () => {}): void {}
+
+ writeValue(value: DebugStrategy): void {
+ this.debugStrategyFormControl.patchValue(value, { emitEvent: false });
+ this.cdr.markForCheck();
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.html
new file mode 100644
index 0000000000..4f8402ff3f
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.html
@@ -0,0 +1,50 @@
+
+
+
debug-strategy.label
+
+
{{ 'debug-strategy.hint.main' | translate }}
+
+
+
+
+ {{ 'debug-strategy.on-failure' | translate }}
+
+
+
+
+ {{ 'debug-strategy.all-messages' | translate }} {{ ' (' + maxRuleNodeDebugDurationMinutes + ' ' }} {{ ('debug-strategy.min' | translate) + ')'}}
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.ts
new file mode 100644
index 0000000000..87851067de
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.ts
@@ -0,0 +1,94 @@
+///
+/// Copyright © 2016-2024 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, EventEmitter, Input, OnInit } from '@angular/core';
+import { PageComponent } from '@shared/components/page.component';
+import { TbPopoverComponent } from '@shared/components/popover.component';
+import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { AppState } from '@core/core.state';
+import { CommonModule } from '@angular/common';
+import { SharedModule } from '@shared/shared.module';
+import { getCurrentAuthState } from '@core/auth/auth.selectors';
+import { DebugStrategy } from '@shared/models/rule-node.models';
+
+@Component({
+ selector: 'tb-debug-strategy-panel',
+ templateUrl: './debug-strategy-panel.component.html',
+ standalone: true,
+ imports: [
+ SharedModule,
+ CommonModule
+ ]
+})
+export class DebugStrategyPanelComponent extends PageComponent implements OnInit {
+
+ @Input() popover: TbPopoverComponent;
+ @Input() debugStrategy: DebugStrategy;
+
+ debugStrategyFormGroup: UntypedFormGroup;
+
+ onStrategyApplied = new EventEmitter()
+
+ readonly maxRuleNodeDebugDurationMinutes = getCurrentAuthState(this.store).maxRuleNodeDebugDurationMinutes;
+
+ constructor(private fb: UntypedFormBuilder,
+ protected store: Store) {
+ super(store);
+
+ this.debugStrategyFormGroup = this.fb.group({
+ allMessages: [false],
+ onFailure: [false]
+ });
+ }
+
+ ngOnInit(): void {
+ this.updatePanelStrategy();
+ }
+
+ onCancel() {
+ this.popover?.hide();
+ }
+
+ onApply(): void {
+ const allMessages = this.debugStrategyFormGroup.get('allMessages').value;
+ const onFailure = this.debugStrategyFormGroup.get('onFailure').value;
+ if (allMessages && onFailure) {
+ this.onStrategyApplied.emit(DebugStrategy.ALL_WITH_FAILURES);
+ } else if (allMessages) {
+ this.onStrategyApplied.emit(DebugStrategy.ALL_EVENTS);
+ } else if (onFailure) {
+ this.onStrategyApplied.emit(DebugStrategy.ONLY_FAILURE_EVENTS);
+ } else {
+ this.onStrategyApplied.emit(DebugStrategy.DISABLED);
+ }
+ }
+
+ private updatePanelStrategy(): void {
+ switch (this.debugStrategy) {
+ case DebugStrategy.ALL_WITH_FAILURES:
+ this.debugStrategyFormGroup.get('allMessages').patchValue(true, { emitEvent: false });
+ this.debugStrategyFormGroup.get('onFailure').patchValue(true, { emitEvent: false });
+ break;
+ case DebugStrategy.ONLY_FAILURE_EVENTS:
+ this.debugStrategyFormGroup.get('onFailure').patchValue(true, { emitEvent: false });
+ break;
+ case DebugStrategy.ALL_EVENTS:
+ this.debugStrategyFormGroup.get('allMessages').patchValue(true, { emitEvent: false });
+ break;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
index 014a73973c..c64fd2e422 100644
--- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
+++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
@@ -22,7 +22,7 @@