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 new file mode 100644 index 0000000000..4770b90fe3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -0,0 +1,46 @@ + +
+
+
+
+
+ + rulenode.name + + + {{ 'rulenode.name-required' | translate }} + + + + {{ 'rulenode.debug-mode' | translate }} + +
+ TODO: tb-rule-node-config +
+ + rulenode.description + + +
+
+
+ + +
+ + rulenode.description + + +
+
+
+
+
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 new file mode 100644 index 0000000000..b3995a5f09 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -0,0 +1,70 @@ +import { + AfterViewInit, + Component, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; +import { ISearchableComponent } from '@home/models/searchable-component.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, NgForm } from '@angular/forms'; +import { FcRuleNode } from './rulechain-page.models'; +import { RuleNodeType } from '@shared/models/rule-node.models'; +import { EntityType } from '@shared/models/entity-type.models'; + +@Component({ + selector: 'tb-rule-node', + templateUrl: './rule-node-details.component.html', + styleUrls: [] +}) +export class RuleNodeDetailsComponent extends PageComponent implements OnInit { + + @ViewChild('ruleNodeForm', {static: true}) ruleNodeForm: NgForm; + + ruleNodeValue: FcRuleNode; + + @Input() + set ruleNode(ruleNode: FcRuleNode) { + this.ruleNodeValue = ruleNode; + } + + @Input() + ruleChainId: string; + + @Input() + isEdit: boolean; + + @Input() + isReadOnly: boolean; + + @Output() + deleteRuleNode = new EventEmitter(); + + ruleNodeType = RuleNodeType; + entityType = EntityType; + + ruleNodeFormGroup: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + this.ruleNodeFormGroup = this.fb.group({}); + } + + private buildForm() { + } + + ngOnInit(): void { + } + + onDeleteRuleNode(event) { + this.deleteRuleNode.emit(); + } + +} 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 2525745ec8..0b0a7507cc 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 @@ -17,6 +17,14 @@ -->
+
-
- - -
+ + +
+ + +
+ +
+ + +
+
+
+ + + + + + +
+
+
{{editingRuleNode.component.name}}
+
 
+
{{editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}
+
 
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss index 9743609b04..4a8fbeb404 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss @@ -14,99 +14,203 @@ * limitations under the License. */ -:host { +.tb-rulechain { width: 100%; height: 100%; - .tb-rulechain { - width: 100%; - height: 100%; - section.tb-header-buttons.tb-library-open { - position: absolute; - top: 0; - left: 0; - z-index: 2; - pointer-events: none; + button.mat-button.mat-icon-button.tb-fullscreen-button { + position: absolute; + top: 10px; + right: 10px; + background: #ccc; + color: #666; + opacity: .85; + margin: 0 6px; + z-index: 2; + } + section.tb-header-buttons.tb-library-open { + position: absolute; + top: 0; + left: 0; + z-index: 2; + pointer-events: none; - .mat-button.tb-btn-open-library { - top: 0; - left: 0; - width: 36px; - height: 36px; - margin: 4px 0 0 4px; - line-height: 36px; - opacity: .5; + .mat-fab { + .mat-button-wrapper { + padding: 0; } } - .tb-rulechain-library { - z-index: 1; - width: 250px; - min-width: 250px; - .mat-toolbar { + .mat-button.tb-btn-open-library { + top: 0; + left: 0; + width: 36px; + height: 36px; + margin: 4px 0 0 4px; + line-height: 36px; + opacity: .5; + } + } + + .tb-rulechain-library { + z-index: 1; + width: 250px; + min-width: 250px; + + .mat-toolbar { + height: 48px; + min-height: 48px; + padding: 0; + + .mat-toolbar-tools { height: 48px; - min-height: 48px; - padding: 0; + padding: 0 6px; + font-size: 14px; - .mat-toolbar-tools { - height: 48px; - padding: 0 6px; - font-size: 14px; - - .mat-button.mat-icon-button { - margin: 0; - - &.tb-small { - width: 32px; - height: 32px; - min-height: 32px; - padding: 6px; + .mat-form-field { + .mat-form-field-infix { + width: auto; + } + } + + .mat-button.mat-icon-button { + margin: 0; + + &.tb-small { + width: 32px; + height: 32px; + min-height: 32px; + padding: 6px; + line-height: 20px; + mat-icon { + width: 20px; + min-width: 20px; + height: 20px; + min-height: 20px; + font-size: 20px; line-height: 20px; - mat-icon { - width: 20px; - min-width: 20px; - height: 20px; - min-height: 20px; - font-size: 20px; - line-height: 20px; - } } } } } - .tb-rulechain-library-panel-group { - overflow-x: hidden; - overflow-y: auto; - .mat-expansion-panel { - border-radius: 0px; - &:last-child { - margin-bottom: 5px; + } + .tb-rulechain-library-panel-group { + overflow-x: hidden; + overflow-y: auto; + .mat-expansion-panel { + border-radius: 0px; + &:last-child { + margin-bottom: 5px; + } + .mat-expansion-panel-header { + background: #e6e6e6; + &:hover { + background: #dadada; } + } + &.mat-expanded { .mat-expansion-panel-header { - background: #e6e6e6; + border-bottom: 1px solid; + border-color: #909090; } - &.mat-expanded { - .mat-expansion-panel-header { - border-bottom: 1px solid; - border-color: #909090; - } - } - } - .tb-panel-title { - min-width: 140px; - user-select: none; } - .fc-canvas { - background: #f9f9f9; + } + .mat-expansion-panel-body { + padding: 0; + } + .tb-panel-title { + min-width: 140px; + user-select: none; + } + .fc-canvas { + background: #f9f9f9; + } + } + } + .tb-rulechain-graph { + z-index: 0; + } + .fc-canvas { + .fc-node { + border-radius: 8px; + &.fc-selected { + &:not(.fc-edit) { + margin: -3px; + border: solid 3px #f00; } } } - .tb-rulechain-graph { - z-index: 0; + + .fc-edit { + .fc-nodeedit, + .fc-nodedelete { + border: solid 2px #fff; + background: #f83e05; + outline: none; + } + } + + .fc-arrow-marker { + polygon { + fill: #808080; + stroke: #808080; + } + } + + .fc-arrow-marker-selected { + polygon { + fill: #f00; + stroke: #f00; + } + } + + .fc-edge { + outline: none; + stroke: #808080; + + &.fc-selected { + stroke: #f00; + } + + &.fc-hover { + stroke: #808080; + } + } + + .edge-endpoint { + fill: #808080; + } + + .fc-edge-label { + opacity: 1 !important; + &:focus { + outline: 0; + } + + &.fc-selected { + .fc-edge-label-text { + span { + color: #fff !important; + background-color: #f00 !important; + border: solid #f00 !important; + } + } + } + + .fc-edge-label-text { + font-size: 14px !important; + font-weight: 600 !important; + + span { + background-color: #fff !important; + color: #003a79 !important; + border: solid 2px #003a79 !important; + } + } } } } -:host ::ng-deep { +/*:host ::ng-deep { .tb-rulechain { section.tb-header-buttons.tb-library-open { .mat-fab { @@ -211,4 +315,4 @@ } } } -} +}*/ 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 9e05502f19..3d1d0d8712 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 @@ -14,7 +14,17 @@ /// limitations under the License. /// -import { AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import { + AfterViewInit, + Component, + ElementRef, + OnInit, + QueryList, + ViewChild, + ViewChildren, + ViewEncapsulation, + HostBinding +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -31,7 +41,7 @@ import { RuleChain, ruleChainNodeComponent } from '@shared/models/rule-chain.models'; -import { FlowchartConstants, UserCallbacks, NgxFlowchartComponent } from 'ngx-flowchart/dist/ngx-flowchart'; +import { FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart/dist/ngx-flowchart'; import { RuleNodeComponentDescriptor, RuleNodeType, @@ -42,13 +52,14 @@ import { FcRuleEdge, FcRuleNode, FcRuleNodeModel, FcRuleNodeType, FcRuleNodeType import { RuleChainService } from '@core/http/rule-chain.service'; import { fromEvent, of } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; -import Timeout = NodeJS.Timeout; import { ISearchableComponent } from '../../models/searchable-component.models'; +import Timeout = NodeJS.Timeout; @Component({ selector: 'tb-rulechain-page', templateUrl: './rulechain-page.component.html', - styleUrls: ['./rulechain-page.component.scss'] + styleUrls: ['./rulechain-page.component.scss'], + encapsulation: ViewEncapsulation.None }) export class RuleChainPageComponent extends PageComponent implements AfterViewInit, OnInit, HasDirtyFlag, ISearchableComponent { @@ -57,6 +68,9 @@ export class RuleChainPageComponent extends PageComponent return this.isDirtyValue || this.isImport; } + @HostBinding('style.width') width = '100%'; + @HostBinding('style.height') height = '100%'; + @ViewChild('ruleNodeSearchInput', {static: false}) ruleNodeSearchInputField: ElementRef; @ViewChild('ruleChainCanvas', {static: true}) ruleChainCanvas: NgxFlowchartComponent; @@ -69,6 +83,7 @@ export class RuleChainPageComponent extends PageComponent isImport: boolean; isDirtyValue: boolean; + isInvalid = false; errorTooltips: {[nodeId: string]: JQueryTooltipster.ITooltipsterInstance} = {}; isFullscreen = false; @@ -445,11 +460,14 @@ export class RuleChainPageComponent extends PageComponent }); } this.isDirtyValue = false; + this.updateRuleNodesHighlight(); + this.validate(); } onModelChanged() { console.log('Model changed!'); this.isDirtyValue = true; + this.validate(); } typeHeaderMouseEnter(event: MouseEvent, ruleNodeType: RuleNodeType) { @@ -517,7 +535,7 @@ export class RuleChainPageComponent extends PageComponent }); } - updateRuleNodesHighlight() { + private updateRuleNodesHighlight() { for (const ruleNode of this.ruleChainModel.nodes) { ruleNode.highlighted = false; } @@ -530,7 +548,98 @@ export class RuleChainPageComponent extends PageComponent } } } - this.ruleChainCanvas.modelService.detectChanges(); + if (this.ruleChainCanvas) { + this.ruleChainCanvas.modelService.detectChanges(); + } + } + + objectsSelected(): boolean { + return this.ruleChainCanvas.modelService.nodes.getSelectedNodes().length > 0 || + this.ruleChainCanvas.modelService.edges.getSelectedEdges().length > 0; + } + + deleteSelected() { + this.ruleChainCanvas.modelService.deleteSelected(); + } + + isDebugModeEnabled(): boolean { + const res = this.ruleChainModel.nodes.find((node) => node.debugMode); + return typeof res !== 'undefined'; + } + + resetDebugModeInAllNodes() { + this.ruleChainModel.nodes.forEach((node) => { + if (node.component.type !== RuleNodeType.INPUT && node.component.type !== RuleNodeType.RULE_CHAIN) { + node.debugMode = false; + } + }); + } + + validate() { + setTimeout(() => { + this.isInvalid = false; + this.ruleChainModel.nodes.forEach((node) => { + if (node.error) { + this.isInvalid = true; + } + this.updateNodeErrorTooltip(node); + }); + }, 0); + } + + saveRuleChain() { + // TODO: + } + + revertRuleChain() { + this.createRuleChainModel(); + } + + private updateNodeErrorTooltip(node: FcRuleNode) { + if (node.error) { + const element = $('#' + node.id); + let tooltip = this.errorTooltips[node.id]; + if (!tooltip || !element.hasClass('tooltipstered')) { + element.tooltipster( + { + theme: 'tooltipster-shadow', + delay: 0, + animationDuration: 0, + trigger: 'custom', + triggerOpen: { + click: false, + tap: false + }, + triggerClose: { + click: false, + tap: false, + scroll: false + }, + side: 'top', + trackOrigin: true + } + ); + const content = '
' + + '
' + + '
' + node.error + '
' + + '
' + + '
'; + const contentElement = $(content); + tooltip = element.tooltipster('instance'); + tooltip.isErrorTooltip = true; + tooltip.content(contentElement); + this.errorTooltips[node.id] = tooltip; + } + setTimeout(() => { + tooltip.open(); + }, 0); + } else { + if (this.errorTooltips[node.id]) { + const tooltip = this.errorTooltips[node.id]; + tooltip.destroy(); + delete this.errorTooltips[node.id]; + } + } } private displayTooltip(event: MouseEvent, content: string) {