Browse Source

RuleChain UI

pull/2411/head
Igor Kulikov 7 years ago
parent
commit
27984f85cd
  1. 46
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
  2. 70
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  3. 106
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html
  4. 244
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss
  5. 121
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

46
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html

@ -0,0 +1,46 @@
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onDeleteRuleNode($event)"
[fxShow]="!isEdit && !isReadOnly">
{{'rulenode.delete' | translate }}
</button>
<div class="mat-padding" fxLayout="column">
<form #ruleNodeForm="ngForm" [formGroup]="ruleNodeFormGroup">
<fieldset [disabled]="(isLoading$ | async) || !isEdit || isReadOnly">
<section *ngIf="ruleNode.component.type !== ruleNodeType.RULE_CHAIN">
<section fxLayout="column" fxLayout.gt-sm="row">
<mat-form-field class="mat-block">
<mat-label translate>rulenode.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="ruleNodeFormGroup.get('name').hasError('required')">
{{ 'rulenode.name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="debugMode">
{{ 'rulenode.debug-mode' | translate }}
</mat-checkbox>
</section>
TODO: tb-rule-node-config
<div formGroupName="additionalInfo" fxLayout="column">
<mat-form-field class="mat-block">
<mat-label translate>rulenode.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
</section>
<section *ngIf="ruleNode.component.type === ruleNodeType.RULE_CHAIN">
<tb-entity-autocomplete required
[excludeEntityIds]="[ruleChainId]"
[entityType]="entityType.RULE_CHAIN"
formControlName="targetRuleChainId">
</tb-entity-autocomplete>
<div formGroupName="additionalInfo" fxLayout="column">
<mat-form-field class="mat-block">
<mat-label translate>rulenode.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</div>
</section>
</fieldset>
</form>
</div>

70
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<AppState>,
private fb: FormBuilder) {
super(store);
this.ruleNodeFormGroup = this.fb.group({});
}
private buildForm() {
}
ngOnInit(): void {
}
onDeleteRuleNode(event) {
this.deleteRuleNode.emit();
}
}

106
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html

@ -17,6 +17,14 @@
-->
<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen"
fxLayout="column" class="tb-rulechain">
<button mat-button
type="button"
mat-icon-button class="tb-fullscreen-button"
(click)="isFullscreen = !isFullscreen"
matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
<section class="tb-rulechain-container" fxFlex fxLayout="column">
<div class="tb-rulechain-layout" fxFlex fxLayout="row">
<section fxLayout="row"
@ -89,20 +97,90 @@
</div>
</mat-drawer>
<mat-drawer-content>
<div class="tb-absolute-fill tb-rulechain-graph">
<fc-canvas #ruleChainCanvas
id="tb-rulchain-canvas"
[model]="ruleChainModel"
(modelChanged)="onModelChanged()"
[selectedObjects]="selectedObjects"
[edgeStyle]="flowchartConstants.curvedStyle"
[automaticResize]="true"
[nodeWidth]="170"
[nodeHeight]="50"
[dragAnimation]="flowchartConstants.dragAnimationRepaint"
[userCallbacks]="editCallbacks">
</fc-canvas>
</div>
<mat-drawer-container style="width: 100%;">
<mat-drawer-content>
<div class="tb-absolute-fill tb-rulechain-graph">
<fc-canvas #ruleChainCanvas
id="tb-rulchain-canvas"
[model]="ruleChainModel"
(modelChanged)="onModelChanged()"
[selectedObjects]="selectedObjects"
[edgeStyle]="flowchartConstants.curvedStyle"
[automaticResize]="true"
[nodeWidth]="170"
[nodeHeight]="50"
[dragAnimation]="flowchartConstants.dragAnimationRepaint"
[userCallbacks]="editCallbacks">
</fc-canvas>
</div>
<section fxLayout="row" class="layout-wrap tb-footer-buttons" fxLayoutAlign="start end">
<button [disabled]="isLoading$ | async"
mat-fab color="accent" class="tb-btn-footer"
[ngClass]="{'tb-hide': !objectsSelected()}"
(click)="deleteSelected()"
matTooltip="{{ 'rulenode.delete-selected-objects' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || !isDebugModeEnabled()"
mat-fab color="accent" class="tb-btn-footer"
(click)="resetDebugModeInAllNodes()"
matTooltip="{{ 'rulenode.reset-debug-mode' | translate }}"
matTooltipPosition="above">
<mat-icon>bug_report</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || isInvalid || (!isDirtyValue && !isImport)"
mat-fab color="accent" class="tb-btn-footer"
(click)="saveRuleChain()"
matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above">
<mat-icon>done</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || !isDirtyValue"
mat-fab color="accent" class="tb-btn-footer"
(click)="revertRuleChain()"
matTooltip="{{ 'action.decline-changes' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</section>
</mat-drawer-content>
<mat-drawer class="tb-details-drawer"
[opened]="isEditingRuleNode"
mode="over"
position="end">
<tb-details-panel fxFlex
headerTitle="{{editingRuleNode?.name}}"
headerSubtitle="{{ ruleNodeTypeDescriptorsMap.get(editingRuleNode.component.type).name | translate }}"
[isReadOnly]="false"
[isAlwaysEdit]="true"
(closeDetails)="onEditRuleNodeClosed()"
(toggleDetailsEditMode)="onRevertRuleNodeEdit()"
(applyDetails)="saveRuleNode()"
[theForm]="tbEditWidget.widgetForm">
<div class="details-buttons">
<div [tb-help]="helpLinkIdForRuleNodeType()"></div>
</div>
<mat-tab-group fxFlex class="tb-absolute-fill" [(selectedIndex)]="selectedRuleNodeTabIndex">
<mat-tab label="{{ 'rulenode.details' | translate }}">
</mat-tab>
<mat-tab *ngIf="isEditingRuleNode && editingRuleNode.ruleNodeId" label="{{ 'rulenode.events' | translate }}">
</mat-tab>
<mat-tab label="{{ 'rulenode.help' | translate }}">
<div class="tb-rule-node-help">
<div id="tb-node-content" class="mat-padding" fxLayout="column">
<div class="tb-node-title">{{editingRuleNode.component.name}}</div>
<div>&nbsp;</div>
<div class="tb-node-description">{{editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}</div>
<div>&nbsp;</div>
<div class="tb-node-details" [innerHtml]="editingRuleNode.component.configurationDescriptor.nodeDefinition.details"></div>
</div>
</div>
</mat-tab>
</mat-tab-group>
</tb-details-panel>
</mat-drawer>
</mat-drawer-container>
</mat-drawer-content>
</mat-drawer-container>
</div>

244
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 @@
}
}
}
}
}*/

121
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 = '<div class="tb-rule-node-error-tooltip">' +
'<div id="tooltip-content" layout="column">' +
'<div class="tb-node-details">' + node.error + '</div>' +
'</div>' +
'</div>';
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) {

Loading…
Cancel
Save