diff --git a/ui-ngx/src/app/core/services/script/node-script-test.service.ts b/ui-ngx/src/app/core/services/script/node-script-test.service.ts index 1a33275111..fa30934f06 100644 --- a/ui-ngx/src/app/core/services/script/node-script-test.service.ts +++ b/ui-ngx/src/app/core/services/script/node-script-test.service.ts @@ -23,8 +23,8 @@ import { NodeScriptTestDialogComponent, NodeScriptTestDialogData } from '@shared/components/dialog/node-script-test-dialog.component'; -import { sortObjectKeys } from '@core/utils'; import { ScriptLanguage } from '@shared/models/rule-node.models'; +import { DebugRuleNodeEventBody } from '@shared/models/event.models'; @Injectable({ providedIn: 'root' @@ -37,56 +37,52 @@ export class NodeScriptTestService { testNodeScript(script: string, scriptType: string, functionTitle: string, functionName: string, argNames: string[], ruleNodeId: string, helpId?: string, - scriptLang?: ScriptLanguage): Observable { - if (ruleNodeId) { + scriptLang?: ScriptLanguage, debugEventBody?: DebugRuleNodeEventBody): Observable { + if (ruleNodeId && !debugEventBody) { return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe( switchMap((debugIn) => { - let msg: any; - let metadata: {[key: string]: string}; - let msgType: string; - if (debugIn) { - if (debugIn.data) { - try { - msg = JSON.parse(debugIn.data); - } catch (e) {} - } - if (debugIn.metadata) { - try { - metadata = JSON.parse(debugIn.metadata); - } catch (e) {} - } - msgType = debugIn.msgType; - } return this.openTestScriptDialog(script, scriptType, functionTitle, - functionName, argNames, msg, metadata, msgType, helpId, scriptLang); + functionName, argNames, debugIn, helpId, scriptLang); }) ); } else { return this.openTestScriptDialog(script, scriptType, functionTitle, - functionName, argNames, null, null, null, helpId, scriptLang); + functionName, argNames, debugEventBody, helpId, scriptLang); } } - private openTestScriptDialog(script: string, scriptType: string, - functionTitle: string, functionName: string, argNames: string[], - msg?: any, metadata?: {[key: string]: string}, msgType?: string, helpId?: string, + private openTestScriptDialog(script: string, scriptType: string, functionTitle: string, functionName: string, + argNames: string[], eventBody: DebugRuleNodeEventBody, helpId?: string, scriptLang?: ScriptLanguage): Observable { + let msg: any; + let metadata: {[key: string]: string}; + let msgType: string; + if (eventBody && eventBody.data) { + try { + msg = JSON.parse(eventBody.data); + } catch (e) {} + } if (!msg) { msg = { temperature: 22.4, humidity: 78 }; } + if (eventBody && eventBody.metadata) { + try { + metadata = JSON.parse(eventBody.metadata); + } catch (e) {} + } if (!metadata) { metadata = { deviceName: 'Test Device', deviceType: 'default', ts: new Date().getTime() + '' }; - } else { - metadata = sortObjectKeys(metadata); } - if (!msgType) { + if (eventBody && eventBody.msgType) { + msgType = eventBody.msgType; + } else { msgType = 'POST_TELEMETRY_REQUEST'; } return this.dialog.open(NodeScriptTestDialogComponent, diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index b868fd1e47..46a7c966c8 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -637,6 +637,10 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } } + cellActionDescriptorsUpdated() { + this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors]; + } + headerCellStyle(column: EntityColumn>) { const index = this.entitiesTableConfig.columns.indexOf(column); let res = this.headerCellStyleCache[index]; diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index a9816a130c..4021d018ce 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -21,7 +21,7 @@ import { EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; -import { DebugEventType, Event, EventType, FilterEventBody } from '@shared/models/event.models'; +import { DebugEventType, Event, EventBody, EventType, FilterEventBody } from '@shared/models/event.models'; import { TimePageLink } from '@shared/models/page/page-link'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; @@ -41,7 +41,7 @@ import { } from '@home/components/event/event-content-dialog.component'; import { isEqual, sortObjectKeys } from '@core/utils'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; -import { ChangeDetectorRef, Injector, StaticProvider, ViewContainerRef } from '@angular/core'; +import { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core'; import { ComponentPortal } from '@angular/cdk/portal'; import { EVENT_FILTER_PANEL_DATA, @@ -61,6 +61,7 @@ export class EventTableConfig extends EntityTableConfig { set eventType(eventType: EventType | DebugEventType) { if (this.eventTypeValue !== eventType) { this.eventTypeValue = eventType; + this.updateCellAction(); this.updateColumns(true); this.updateFilterColumns(); } @@ -84,7 +85,9 @@ export class EventTableConfig extends EntityTableConfig { private debugEventTypes: Array = null, private overlay: Overlay, private viewContainerRef: ViewContainerRef, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + public testButtonLabel?: string, + private debugEventSelected?: EventEmitter) { super(); this.loadDataOnInit = false; this.tableTitle = ''; @@ -120,6 +123,7 @@ export class EventTableConfig extends EntityTableConfig { this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.updateColumns(); + this.updateCellAction(); this.updateFilterColumns(); this.headerActionDescriptors.push({ @@ -349,6 +353,25 @@ export class EventTableConfig extends EntityTableConfig { } } + updateCellAction() { + this.cellActionDescriptors = []; + switch (this.eventType) { + case DebugEventType.DEBUG_RULE_NODE: + if (this.testButtonLabel) { + this.cellActionDescriptors.push({ + name: this.translate.instant('rulenode.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}), + icon: 'bug_report', + isEnabled: (entity) => entity.body.type === 'IN', + onAction: ($event, entity) => { + this.debugEventSelected.next(entity.body); + } + }); + } + break; + } + this.getTable()?.cellActionDescriptorsUpdated(); + } + showContent($event: MouseEvent, content: string, title: string, contentType: ContentType = null, sortKeys = false): void { if ($event) { $event.stopPropagation(); diff --git a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts index fbdd0a07da..b354e49388 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts @@ -17,10 +17,10 @@ import { AfterViewInit, ChangeDetectorRef, - Component, + Component, EventEmitter, Input, OnDestroy, - OnInit, + OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core'; @@ -32,9 +32,10 @@ import { EntitiesTableComponent } from '@home/components/entity/entities-table.c import { EventTableConfig } from './event-table-config'; import { EventService } from '@core/http/event.service'; import { DialogService } from '@core/services/dialog.service'; -import { DebugEventType, EventType } from '@shared/models/event.models'; +import { DebugEventType, EventBody, EventType } from '@shared/models/event.models'; import { Overlay } from '@angular/cdk/overlay'; import { Subscription } from 'rxjs'; +import { isNotEmptyStr } from '@core/utils'; @Component({ selector: 'tb-event-table', @@ -59,6 +60,10 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy { dirtyValue = false; entityIdValue: EntityId; + get active(): boolean { + return this.activeValue; + } + @Input() set active(active: boolean) { if (this.activeValue !== active) { @@ -83,6 +88,28 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy { } } + private functionTestButtonLabelValue: string; + + get functionTestButtonLabel(): string { + return this.functionTestButtonLabelValue; + } + + @Input() + set functionTestButtonLabel(value: string) { + if (isNotEmptyStr(value)) { + this.functionTestButtonLabelValue = value; + } else { + this.functionTestButtonLabelValue = ''; + } + if (this.eventTableConfig) { + this.eventTableConfig.testButtonLabel = this.functionTestButtonLabel; + this.eventTableConfig.updateCellAction(); + } + } + + @Output() + debugEventSelected = new EventEmitter(); + @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; eventTableConfig: EventTableConfig; @@ -114,7 +141,9 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy { this.debugEventTypes, this.overlay, this.viewContainerRef, - this.cd + this.cd, + this.functionTestButtonLabel, + this.debugEventSelected ); } diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts index 033747f6d3..a6e5ada7bf 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -80,6 +80,7 @@ export interface IEntitiesTableComponent { exitFilterMode(): void; resetSortAndFilter(update?: boolean, preserveTimewindow?: boolean): void; columnsUpdated(resetData?: boolean): void; + cellActionDescriptorsUpdated(): void; headerCellStyle(column: EntityColumn>): any; clearCellCache(col: number, row: number): void; cellContent(entity: BaseData, column: EntityColumn>, row: number): any; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts index 73f0d5e14a..3f3071221e 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts @@ -18,14 +18,22 @@ import { AfterViewInit, Component, ComponentRef, + EventEmitter, forwardRef, Input, OnDestroy, OnInit, + Output, ViewChild, ViewContainerRef } from '@angular/core'; -import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; import { IRuleNodeConfigurationComponent, RuleNodeConfiguration, @@ -76,6 +84,12 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On @Input() ruleChainType: RuleChainType; + @Output() + initRuleNode = new EventEmitter(); + + @Output() + changeScript = new EventEmitter(); + nodeDefinitionValue: RuleNodeDefinition; @Input() @@ -85,6 +99,7 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On if (this.nodeDefinitionValue) { this.validateDefinedDirective(); } + setTimeout(() => this.initRuleNode.emit()); } } @@ -98,8 +113,11 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On changeSubscription: Subscription; + changeScriptSubscription: Subscription; + + definedConfigComponent: IRuleNodeConfigurationComponent; + private definedConfigComponentRef: ComponentRef; - private definedConfigComponent: IRuleNodeConfigurationComponent; private configuration: RuleNodeConfiguration; @@ -127,6 +145,14 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On if (this.definedConfigComponentRef) { this.definedConfigComponentRef.destroy(); } + if (this.changeSubscription) { + this.changeSubscription.unsubscribe(); + this.changeSubscription = null; + } + if (this.changeScriptSubscription) { + this.changeScriptSubscription.unsubscribe(); + this.changeScriptSubscription = null; + } } ngAfterViewInit(): void { @@ -199,6 +225,9 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On this.changeSubscription = this.definedConfigComponent.configurationChanged.subscribe((configuration) => { this.updateModel(configuration); }); + if (this.definedConfigComponent?.changeScript) { + this.changeScriptSubscription = this.definedConfigComponent.changeScript.subscribe(() => this.changeScript.emit()); + } } } 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 b2c73a4bdc..34aa8167e3 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 @@ -50,7 +50,9 @@ [ruleNodeId]="ruleNode.ruleNodeId?.id" [ruleChainId]="ruleChainId" [ruleChainType]="ruleChainType" - [nodeDefinition]="ruleNode.component.configurationDescriptor.nodeDefinition"> + [nodeDefinition]="ruleNode.component.configurationDescriptor.nodeDefinition" + (initRuleNode)="initRuleNode.emit($event)" + (changeScript)="changeScript.emit($event)">
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index dad1e715ec..f1d6001c88 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -55,6 +55,12 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O @Input() isAdd = false; + @Output() + initRuleNode = new EventEmitter(); + + @Output() + changeScript = new EventEmitter(); + ruleNodeType = RuleNodeType; entityType = EntityType; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html index 4cdaed942d..5f5594cc1a 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html @@ -111,7 +111,9 @@ [ruleChainId]="ruleChain.id?.id" [ruleChainType]="ruleChainType" [isEdit]="true" - [isReadOnly]="false"> + [isReadOnly]="false" + (initRuleNode)="onRuleNodeInit()" + (changeScript)="switchToFirstTab()"> @@ -119,7 +121,10 @@ [defaultEventType]="debugEventTypes.DEBUG_RULE_NODE" [active]="eventsTab.isActive" [tenantId]="ruleChain.tenantId.id" - [entityId]="editingRuleNode.ruleNodeId"> + [entityId]="editingRuleNode.ruleNodeId" + [functionTestButtonLabel]="ruleNodeTestButtonLabel" + (debugEventSelected)="onDebugEventSelected($event)"> +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index d9c6b1b3bb..6b78d94ac7 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -87,7 +87,7 @@ import { DialogComponent } from '@shared/components/dialog.component'; import { MatMenuTrigger } from '@angular/material/menu'; import { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffer.service'; import { Hotkey } from 'angular2-hotkeys'; -import { DebugEventType, EventType } from '@shared/models/event.models'; +import { DebugEventType, DebugRuleNodeEventBody, EventType } from '@shared/models/event.models'; import { MatMiniFabButton } from '@angular/material/button'; import { TbPopoverService } from '@shared/components/popover.service'; import { VersionControlComponent } from '@home/components/vc/version-control.component'; @@ -153,6 +153,7 @@ export class RuleChainPageComponent extends PageComponent editingRuleNodeAllowCustomLabels = false; editingRuleNodeLinkLabels: {[label: string]: LinkLabel}; editingRuleNodeSourceRuleChainId: string; + ruleNodeTestButtonLabel: string; @ViewChild('tbRuleNode') ruleNodeComponent: RuleNodeDetailsComponent; @ViewChild('tbRuleNodeLink') ruleNodeLinkComponent: RuleNodeLinkComponent; @@ -1277,6 +1278,27 @@ export class RuleChainPageComponent extends PageComponent this.editingRuleNodeLink = deepClone(edge); } + onDebugEventSelected(debugEventBody: DebugRuleNodeEventBody) { + const ruleNodeConfigComponent = this.ruleNodeComponent.ruleNodeConfigComponent; + const ruleNodeConfigDefinedComponent = ruleNodeConfigComponent.definedConfigComponent; + if (ruleNodeConfigComponent.useDefinedDirective() && ruleNodeConfigDefinedComponent.hasScript && ruleNodeConfigDefinedComponent.testScript) { + ruleNodeConfigDefinedComponent.testScript(debugEventBody); + } + } + + onRuleNodeInit() { + const ruleNodeConfigDefinedComponent = this.ruleNodeComponent.ruleNodeConfigComponent.definedConfigComponent; + if (this.ruleNodeComponent.ruleNodeConfigComponent.useDefinedDirective() && ruleNodeConfigDefinedComponent.hasScript) { + this.ruleNodeTestButtonLabel = ruleNodeConfigDefinedComponent.testScriptLabel; + } else { + this.ruleNodeTestButtonLabel = ''; + } + } + + switchToFirstTab() { + this.selectedRuleNodeTabIndex = 0; + } + saveRuleNode() { this.ruleNodeComponent.validate(); if (this.ruleNodeComponent.ruleNodeFormGroup.valid) { diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index d4688546d7..2a7dedabec 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -26,6 +26,8 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { RuleChainType } from '@shared/models/rule-chain.models'; +import { DebugRuleNodeEventBody } from '@shared/models/event.models'; +import { TranslateService } from '@ngx-translate/core'; export interface RuleNodeConfiguration { [key: string]: any; @@ -71,10 +73,14 @@ export interface RuleNodeConfigurationDescriptor { export interface IRuleNodeConfigurationComponent { ruleNodeId: string; ruleChainId: string; + hasScript: boolean; + testScriptLabel?: string; + changeScript?: EventEmitter; ruleChainType: RuleChainType; configuration: RuleNodeConfiguration; configurationChanged: Observable; validate(); + testScript? (debugEventBody?: DebugRuleNodeEventBody); [key: string]: any; } @@ -87,6 +93,8 @@ export abstract class RuleNodeConfigurationComponent extends PageComponent imple ruleChainId: string; + hasScript: boolean = false; + ruleChainType: RuleChainType; configurationValue: RuleNodeConfiguration; @@ -499,3 +507,4 @@ export function getRuleNodeHelpLink(component: RuleNodeComponentDescriptor): str } return 'ruleEngine'; } + 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 cd379fa4dc..fd94e6059e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3467,7 +3467,8 @@ "output": "Output", "test": "Test", "help": "Help", - "reset-debug-mode": "Reset debug mode in all nodes" + "reset-debug-mode": "Reset debug mode in all nodes", + "test-with-this-message": "{{test}} with this message" }, "timezone": { "timezone": "Timezone",