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 @@
-
+
rulenode.name @@ -34,13 +34,19 @@ {{ 'rulenode.name-max-length' | translate }} -
- - {{ 'rulenode.debug-mode' | translate }} - - - {{ 'rulenode.singleton-mode' | translate }} - +
+ +
{ ruleChainId?: RuleChainId; type: string; name: string; - debugMode: boolean; + debugStrategy: DebugStrategy; + lastUpdateTs?: number; singletonMode: boolean; queueName?: string; configurationVersion?: number; @@ -345,13 +346,21 @@ export interface FcRuleNode extends FcRuleNodeType { ruleNodeId?: RuleNodeId; additionalInfo?: any; configuration?: RuleNodeConfiguration; - debugMode?: boolean; + debugStrategy?: DebugStrategy; + lastUpdateTs?: number; error?: string; highlighted?: boolean; componentClazz?: string; ruleChainType?: RuleChainType; } +export enum DebugStrategy { + DISABLED = 'DISABLED', + ALL_EVENTS = 'ALL_EVENTS', + ONLY_FAILURE_EVENTS = 'ONLY_FAILURE_EVENTS', + ALL_WITH_FAILURES = 'ALL_WITH_FAILURES', +} + export interface FcRuleEdge extends FcEdge { labels?: string[]; } diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json index 0afd671082..c45a5b4be4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json @@ -4556,7 +4556,6 @@ "deselect-all": "إلغاء تحديد الكل", "rulenode-details": "تفاصيل عقدة القاعدة", "debug-mode": "وضع التصحيح", - "singleton-mode": "وضع المفرد", "configuration": "التكوين", "link": "رابط", "link-details": "تفاصيل رابط عقدة القاعدة", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 0fa19cc176..6185257663 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -995,6 +995,19 @@ "type-timeseries-deleted": "Telemetry deleted", "type-sms-sent": "SMS sent" }, + "debug-strategy": { + "min": "min", + "label": "Debug strategy", + "on-failure": "Failures only (24/7)", + "all-messages": "All messages", + "failures": "Failures", + "all": "All", + "hint": { + "main": "All node debug messages rate limited with:", + "on-failure": "Save all failure debug events without time limit.", + "all-messages": "Save all debug events during time limit." + } + }, "confirm-on-exit": { "message": "You have unsaved changes. Are you sure you want to leave this page?", "html-message": "You have unsaved changes.
Are you sure you want to leave this page?", @@ -2071,6 +2084,7 @@ "column": "Column", "row": "Row" }, + "disabled": "Disabled", "edge": { "edge": "Edge", "edge-instances": "Edge instances", @@ -4014,7 +4028,7 @@ "deselect-all": "Deselect all", "rulenode-details": "Rule node details", "debug-mode": "Debug mode", - "singleton-mode": "Singleton mode", + "singleton": "Singleton", "configuration": "Configuration", "link": "Link", "link-details": "Rule node link details", @@ -4292,6 +4306,9 @@ "maximum-ota-packages-sum-data-size": "Maximum total size of OTA package files (bytes)", "maximum-ota-package-sum-data-size-required": "Maximum total size of OTA package files is required.", "maximum-ota-package-sum-data-size-range": "Maximum total size of OTA package files can't be negative", + "maximum-rule-node-debug-duration-min": "Maximum rule node debug duration (min)", + "maximum-rule-node-debug-duration-min-required": "Maximum rule node debug duration is required.", + "maximum-rule-node-debug-duration-min-range": "Maximum rule node debug duration can't be negative", "rest-requests-for-tenant": "REST requests for tenant", "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages", "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index f653e7179a..1ae18bd51c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -3485,7 +3485,6 @@ "deselect-all": "Deshacer selección de todos", "rulenode-details": "Detalles del nodo de reglas", "debug-mode": "Modo Debug", - "singleton-mode": "Módo único", "configuration": "Configuración", "link": "Enlace", "link-details": "Detalles del enlace del nodo de reglas", diff --git a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json index cb3a96f989..4791ab251f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json +++ b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json @@ -4458,7 +4458,6 @@ "deselect-all": "Deselect all", "rulenode-details": "Rule node details", "debug-mode": "Debug mode", - "singleton-mode": "Singleton mode", "configuration": "Configuration", "link": "Link", "link-details": "Rule node link details", diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json index c16c9c5e40..67d73b8be9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json +++ b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json @@ -4260,7 +4260,6 @@ "deselect-all": "Alles deselecteren", "rulenode-details": "Details van rule nodes", "debug-mode": "Foutopsporingsmodus", - "singleton-mode": "Singleton-modus", "configuration": "Configuratie", "link": "Verbinden", "link-details": "Details van rule nodes links", diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index ac27313248..bb98170a92 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -4474,7 +4474,6 @@ "deselect-all": "Odznacz wszystkie", "rulenode-details": "Szczegóły węzła reguły", "debug-mode": "Tryb debugowania", - "singleton-mode": "Tryb singletonowy", "configuration": "Konfiguracja", "link": "Połączenie", "link-details": "Szczegóły połączenia węzła reguły", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index ac302a78bb..bd2ff3c820 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -3968,7 +3968,6 @@ "deselect-all": "取消选择", "rulenode-details": "规则节点详情", "debug-mode": "调试模式", - "singleton-mode": "单例模式", "configuration": "配置", "link": "链接", "link-details": "规则节点链接详情", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 37068909e0..c3240d7d26 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -1267,6 +1267,23 @@ pre.tb-highlight { .no-wrap { white-space: nowrap; } + + .tb-rounded-btn { + border-radius: 20px; + padding: 0 16px; + + &.active:not(:disabled) { + border-color: $primary !important; + } + + &:disabled, &:not(.active) { + background: rgba(0, 0, 0, 0.06); + } + + &:not(.active) { + color: rgba(0, 0, 0, 0.76); + } + } } /***************