Fix Action position in Designer Canvas
[ccsdk/cds.git] / cds-ui / designer-client / src / app / modules / feature-modules / packages / designer / designer.component.ts
1 /*
2 ============LICENSE_START==========================================
3 ===================================================================
4 Copyright (C) 2019 Orange. All rights reserved.
5 ===================================================================
6 Modification Copyright (c) 2020 IBM
7 ===================================================================
8 Modification Copyright (c) 2020 Orange
9 ===================================================================
10
11 Unless otherwise specified, all software contained herein is licensed
12 under the Apache License, Version 2.0 (the License);
13 you may not use this software except in compliance with the License.
14 You may obtain a copy of the License at
15
16     http://www.apache.org/licenses/LICENSE-2.0
17
18 Unless required by applicable law or agreed to in writing, software
19 distributed under the License is distributed on an "AS IS" BASIS,
20 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 See the License for the specific language governing permissions and
22 limitations under the License.
23 ============LICENSE_END============================================
24 */
25
26 import dagre from 'dagre';
27 import graphlib from 'graphlib';
28 import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
29 import * as joint from 'jointjs';
30 import './jointjs/elements/palette.function.element';
31 import './jointjs/elements/action.element';
32 import './jointjs/elements/board.function.element';
33 import { DesignerStore } from './designer.store';
34 import { ActionElementTypeName } from 'src/app/common/constants/app-constants';
35 import { GraphUtil } from './graph.util';
36 import { GraphGenerator } from './graph.generator.util';
37 import { FunctionsStore } from './functions.store';
38 import { Subject } from 'rxjs';
39 import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
40 import { BluePrintDetailModel } from '../model/BluePrint.detail.model';
41 import { ActivatedRoute, Router } from '@angular/router';
42 import { DesignerService } from './designer.service';
43 import { FilesContent, FolderNodeElement } from '../package-creation/mapping-models/metadata/MetaDataTab.model';
44 import { PackageCreationModes } from '../package-creation/creationModes/PackageCreationModes';
45 import { PackageCreationBuilder } from '../package-creation/creationModes/PackageCreationBuilder';
46 import { PackageCreationStore } from '../package-creation/package-creation.store';
47 import { PackageCreationService } from '../package-creation/package-creation.service';
48 import { PackageCreationUtils } from '../package-creation/package-creation.utils';
49 import * as JSZip from 'jszip';
50 import { PackageCreationExtractionService } from '../package-creation/package-creation-extraction.service';
51 import { CBAPackage } from '../package-creation/mapping-models/CBAPacakge.model';
52 import { TopologyTemplate } from './model/designer.topologyTemplate.model';
53 import { ToastrService } from 'ngx-toastr';
54 import { DesignerDashboardState } from './model/designer.dashboard.state';
55 import { NgxUiLoaderService } from 'ngx-ui-loader';
56
57 @Component({
58     selector: 'app-designer',
59     templateUrl: './designer.component.html',
60     styleUrls: ['./designer.component.css'],
61     encapsulation: ViewEncapsulation.None
62 })
63 export class DesignerComponent implements OnInit, OnDestroy {
64
65     controllerSideBar: boolean;
66     actionAttributesSideBar: boolean;
67     functionAttributeSidebar: boolean;
68     viewedPackage: BluePrintDetailModel = new BluePrintDetailModel();
69     customActionName: string;
70     showAction: boolean;
71     cl = 'editBar';
72
73     boardGraph: joint.dia.Graph;
74     boardPaper: joint.dia.Paper;
75
76     paletteGraph: joint.dia.Graph;
77     palettePaper: joint.dia.Paper;
78     ngUnsubscribe = new Subject();
79     opt = { tx: 100, ty: 100 };
80     filesData: any = [];
81     folder: FolderNodeElement = new FolderNodeElement();
82     zipFile: JSZip = new JSZip();
83     cbaPackage: CBAPackage;
84     actions: string[] = [];
85     dataTarget: string;
86     steps: string[];
87     designerState: DesignerDashboardState;
88     currentActionName: string;
89     packageId: any;
90
91     constructor(
92         private designerStore: DesignerStore,
93         private functionStore: FunctionsStore,
94         private packageCreationStore: PackageCreationStore,
95         private packageCreationUtils: PackageCreationUtils,
96         private graphUtil: GraphUtil,
97         private graphGenerator: GraphGenerator,
98         private route: ActivatedRoute,
99         private router: Router,
100         private designerService: DesignerService,
101         private packageCreationService: PackageCreationService,
102         private packageCreationExtractionService: PackageCreationExtractionService,
103         private activatedRoute: ActivatedRoute,
104         private ngxService: NgxUiLoaderService,
105         private toastService: ToastrService) {
106         this.controllerSideBar = true;
107         this.actionAttributesSideBar = false;
108         this.showAction = false;
109         this.functionAttributeSidebar = false;
110
111     }
112
113     _toggleSidebar1() {
114         this.controllerSideBar = !this.controllerSideBar;
115         if (this.controllerSideBar === false) {
116             this.cl = 'editBar2';
117         }
118         if (this.controllerSideBar === true) {
119             this.cl = 'editBar';
120         }
121     }
122
123     _toggleSidebar2() {
124         this.actionAttributesSideBar = !this.actionAttributesSideBar;
125     }
126
127     publishBluePrint() {
128         this.create();
129         this.zipFile.generateAsync({ type: 'blob' })
130             .then(blob => {
131                 const formData = new FormData();
132                 formData.append('file', blob);
133                 this.designerService.publishBlueprint(formData).subscribe(res => {
134                     this.toastService.success('Package Deployed Successfuly');
135                     console.log('Package Deployed...');
136                 }, error => {
137                     this.toastService.error(error.message, 'Package error');
138                     console.log(error);
139                 }, () => {
140                     //  this.deployBluePrint = false;
141                 });
142             });
143     }
144
145
146     /**
147      * - There is a board (main paper) that will the action and function selected from the palette
148      * itmes in this board will be used to create tosca workflow and node templates
149      * - There is also palette , whis contains all the possible functions and actions
150      * that can be dragged into the board
151      * - There is also a fly paper , which is temporarliy paper created on the fly
152      * when items is dragged from the palette- and it's deleted when the item is dropped over
153      * the board.
154      * for more info about the drag and drop algorithem used please visit the following link:
155      * https://stackoverflow.com/a/36932973/1340034
156      */
157
158     ngOnInit() {
159         // this.ngxService.start();
160         this.customActionName = this.route.snapshot.paramMap.get('actionName');
161         if (this.customActionName !== '') {
162             this.showAction = true;
163         }
164         this.initializeBoard();
165         this.initializePalette();
166         this.stencilPaperEventListeners();
167         const id = this.route.snapshot.paramMap.get('id');
168         this.designerService.getPagedPackages(id).subscribe(
169             (bluePrintDetailModels) => {
170                 if (bluePrintDetailModels) {
171                     this.viewedPackage = bluePrintDetailModels[0];
172                     this.packageCreationService.downloadPackage(this.viewedPackage.artifactName + '/'
173                         + this.viewedPackage.artifactVersion)
174                         .subscribe(response => {
175                             const blob = new Blob([response], { type: 'application/octet-stream' });
176                             this.packageCreationExtractionService.extractBlobToStore(blob);
177                         });
178                 }
179             });
180         this.packageCreationStore.state$.subscribe(cba => {
181             this.cbaPackage = cba;
182             console.log(cba.templateTopology.content);
183             this.designerStore.saveSourceContent(cba.templateTopology.content);
184
185         });
186
187         /**
188          * the code to retrieve from server is commented
189          */
190         this.functionStore.state$
191             .pipe(
192                 distinctUntilChanged((a: any, b: any) => JSON.stringify(a) === JSON.stringify(b)),
193                 takeUntil(this.ngUnsubscribe))
194             .subscribe(state => {
195
196                 if (state.serverFunctions) {
197                     console.log('inside subscriotn on functions store -->', state.serverFunctions);
198                     console.log(state);
199                     // this.viewedFunctions = state.functions;
200                     const list = state.serverFunctions;
201
202                     const cells = this.graphUtil.buildPaletteGraphFromList(list);
203                     this.paletteGraph.resetCells(cells);
204
205                     let idx = 0;
206                     cells.forEach(cell => {
207                         cell.translate(5, (cell.attributes.size.height + 5) * idx++);
208                     });
209                 }
210             });
211
212         this.designerStore.state$
213             .pipe(
214                 distinctUntilChanged((a: any, b: any) => JSON.stringify(a) === JSON.stringify(b)),
215                 takeUntil(this.ngUnsubscribe))
216             .subscribe(state => {
217                 this.designerState = state;
218                 if (state.sourceContent) {
219                     console.log('inside desinger.component---> ', state);
220                     // generate graph from store objects if exist
221                     const topologtTemplate: TopologyTemplate = JSON.parse(state.sourceContent);
222                     console.log(topologtTemplate);
223                     delete state.sourceContent;
224                     this.graphGenerator.clear(this.boardGraph);
225                     this.graphGenerator.populate(topologtTemplate, this.boardGraph);
226
227                     console.log('all cells', this.boardGraph.getCells());
228                     /**
229                      * auto arrange elements in graph
230                      * https://resources.jointjs.com/docs/jointjs/v3.1/joint.html#layout.DirectedGraph
231                      */
232                     joint.layout.DirectedGraph.layout(this.boardGraph.getCells(), {
233                         dagre,
234                         graphlib,
235                         setLinkVertices: false,
236                         marginX: 10,
237                         marginY: 10,
238                         clusterPadding: { top: 100, left: 30, right: 10, bottom: 100 },
239                         rankDir: 'TB'
240                     });
241                     this.actions = [];
242                     console.log('here');
243                     for (const workflowsKey in topologtTemplate.workflows) {
244                         if (workflowsKey && !this.actions.includes(workflowsKey)) {
245                             this.actions.push(workflowsKey);
246                             /* tslint:disable:no-string-literal */
247                             if (!this.designerState.template.workflows[workflowsKey]['inputs']) {
248                                 this.designerState.template.workflows[workflowsKey]['inputs'] = {};
249                             }
250                             if (!this.designerState.template.workflows[workflowsKey]['outputs']) {
251                                 this.designerState.template.workflows[workflowsKey]['outputs'] = {};
252                             }
253                         }
254                     }
255                 }
256             });
257
258         // action triggering
259         this.functionStore.retrieveFuntions();
260         this.activatedRoute.paramMap.subscribe(res => {
261             this.packageId = res.get('id');
262         });
263
264     }
265
266     initializePalette() {
267         if (!this.paletteGraph) {
268             this.paletteGraph = new joint.dia.Graph();
269             this.palettePaper = new joint.dia.Paper({
270                 el: $('#palette-paper'),
271                 model: this.paletteGraph,
272                 width: 318,
273                 // height: '100%',
274                 height: $('#palette-paper').height(),
275                 // background: {
276                 //   color: 'rgba(0, 255, 0, 0.3)'
277                 // },
278                 interactive: false
279                 // elements in paletter need to be fixed, please refer to flying paper concept
280             });
281         }
282     }
283
284     initializeBoard() {
285         if (!this.boardGraph) {
286             console.log('initializeBoard...');
287             this.boardGraph = new joint.dia.Graph();
288             this.boardPaper = new joint.dia.Paper({
289                 el: $('#board-paper'),
290                 model: this.boardGraph,
291                 height: 720,
292                 width: 1300,
293                 // origin: { x: 80, y: 70 },
294                 gridSize: 10, // background line sizes
295                 drawGrid: true,
296                 // background: {
297                 // color: 'rgba(0, 255, 0, 0.3)'
298                 // },
299                 cellViewNamespace: joint.shapes
300             });
301
302             this.boardPaper.on('all', element => {
303                 // console.log(element);
304             });
305
306             this.boardPaper.on('link:pointerdown', link => {
307                 console.log(link);
308             });
309
310             this.boardPaper.on('element:pointerdown', element => {
311                 // this.modelSelected.emit(element.model.get('model'));
312             });
313
314             this.boardPaper.on('blank:pointerclick', () => {
315                 // this.selectedModel = undefined;
316             });
317
318             this.boardGraph.on('change:position', (cell) => {
319
320                 const parentId = cell.get('parent');
321                 if (!parentId) {
322                     // this is action
323                     return;
324                 }
325
326                 const parent = this.boardGraph.getCell(parentId);
327
328                 const parentBbox = parent.getBBox();
329                 const cellBbox = cell.getBBox();
330                 if (parentBbox.containsPoint(cellBbox.origin()) &&
331                     parentBbox.containsPoint(cellBbox.topRight()) &&
332                     parentBbox.containsPoint(cellBbox.corner()) &&
333                     parentBbox.containsPoint(cellBbox.bottomLeft())) {
334
335                     // All the four corners of the child are inside
336                     // the parent area.
337                     return;
338                 }
339
340                 // Revert the child position.
341                 cell.set('position', cell.previous('position'));
342             });
343         }
344         console.log('done initializing Board...');
345     }
346
347     insertCustomActionIntoBoard() {
348         console.log('saving action to store action workflow....');
349         let actionName = this.graphUtil.generateNewActionName();
350         while (this.actions.includes(actionName)) {
351             actionName = this.graphUtil.generateNewActionName();
352         }
353         this.graphUtil.createCustomActionWithName(actionName, this.boardGraph);
354         this.designerStore.addDeclarativeWorkFlow(actionName);
355         this.actions.push(actionName);
356     }
357
358     stencilPaperEventListeners() {
359         this.palettePaper.on('cell:pointerdown', (draggedCell, pointerDownEvent, x, y) => {
360
361             $('body').append(`
362         <div id="flyPaper"
363             style="position:fixed;z-index:100;opacity:.7;pointer-event:none;background-color: transparent !important;"></div>`
364             );
365             const flyGraph = new joint.dia.Graph();
366             const flyPaper = new joint.dia.Paper({
367                 el: $('#flyPaper'),
368                 model: flyGraph,
369                 interactive: true
370             });
371             const flyShape = draggedCell.model.clone();
372             const pos = draggedCell.model.position();
373             const offset = {
374                 x: x - pos.x,
375                 y: y - pos.y
376             };
377
378             flyShape.position(0, 0);
379             flyGraph.addCell(flyShape);
380             $('#flyPaper').offset({
381                 left: pointerDownEvent.pageX - offset.x,
382                 top: pointerDownEvent.pageY - offset.y
383             });
384             $('body').on('mousemove.fly', mouseMoveEvent => {
385                 $('#flyPaper').offset({
386                     left: mouseMoveEvent.pageX - offset.x,
387                     top: mouseMoveEvent.pageY - offset.y
388                 });
389             });
390
391             $('body').on('mouseup.fly', mouseupEvent => {
392                 const mouseupX = mouseupEvent.pageX;
393                 const mouseupY = mouseupEvent.pageY;
394                 const target = this.boardPaper.$el.offset();
395                 // Dropped over paper ?
396                 if (mouseupX > target.left &&
397                     mouseupX < target.left + this.boardPaper.$el.width() &&
398                     mouseupY > target.top && y < target.top + this.boardPaper.$el.height()) {
399                     const functionType = this.graphUtil.getFunctionTypeFromPaletteFunction(flyShape);
400                     // step name is CDS realted terminology, please refer to tosca types
401                     const stepName = functionType;
402                     const functionElementForBoard = this.graphUtil.dropFunctionOverActionWithPosition(
403                         stepName, functionType,
404                         mouseupX, mouseupY,
405                         target, offset,
406                         this.boardGraph);
407
408                     const parentCell = this.graphUtil.getParent(functionElementForBoard, this.boardPaper);
409
410                     if (parentCell &&
411                         parentCell.model.attributes.type === ActionElementTypeName &&
412                         this.graphUtil.canEmpedMoreChildern(parentCell.model, this.boardGraph)) {
413
414                         if (this.graphUtil.isEmptyParent(parentCell.model)) {
415                             // first function in action
416                             const actionName = parentCell.model.attributes.attrs['#label'].text;
417                             this.designerStore.addStepToDeclarativeWorkFlow(actionName, stepName, functionType);
418                             if (functionType === 'dg-generic') {
419                                 this.designerStore.addDgGenericNodeTemplate(stepName);
420                             } else {
421                                 this.designerStore.addNodeTemplate(stepName, functionType);
422                             }
423                         } else {
424                             // second action means there was a dg-generic node before
425                             this.designerStore.addNodeTemplate(stepName, functionType);
426                             // this will fail if multiple dg-generic were added
427                             // TODO prevent multi functions of the same type inside the same action
428                             const dgGenericNode = this.graphUtil.getDgGenericChild(parentCell.model, this.boardGraph)[0];
429                             const dgGenericNodeName = this.graphUtil.getFunctionNameFromBoardFunction(dgGenericNode);
430                             this.designerStore.addDgGenericDependency(dgGenericNodeName, stepName);
431                         }
432
433
434                         // Prevent recursive embedding.
435                         if (parentCell &&
436                             parentCell.model.get('parent') !== functionElementForBoard.id) {
437                             parentCell.model.embed(functionElementForBoard);
438                         }
439                     } else {
440                         console.log('function dropped outside action or not allowed, rolling back...');
441                         alert('function dropped outside action or not allowed, rolling back...');
442                         functionElementForBoard.remove();
443                     }
444                 }
445                 $('body').off('mousemove.fly').off('mouseup.fly');
446                 // flyShape.remove();
447                 $('#flyPaper').remove();
448             });
449         });
450         console.log('done stencilPaperEventListeners()...');
451     }
452
453     ngOnDestroy() {
454         this.ngUnsubscribe.next();
455         this.ngUnsubscribe.complete();
456     }
457
458     saveBluePrint() {
459         this.ngxService.start();
460         FilesContent.clear();
461         let packageCreationModes: PackageCreationModes;
462         this.cbaPackage = PackageCreationModes.mapModeType(this.cbaPackage);
463         this.cbaPackage.metaData = PackageCreationModes.setEntryPoint(this.cbaPackage.metaData);
464         packageCreationModes = PackageCreationBuilder.getCreationMode(this.cbaPackage);
465         this.designerStore.state$.subscribe(state => {
466             this.cbaPackage.templateTopology.content = this.packageCreationUtils.transformToJson(state.template);
467         });
468         packageCreationModes.execute(this.cbaPackage, this.packageCreationUtils);
469         this.filesData.push(this.folder.TREE_DATA);
470         this.saveBluePrintToDataBase();
471
472     }
473
474     enrichBluePrint() {
475         this.ngxService.start();
476         this.packageCreationStore.addTopologyTemplate(this.cbaPackage.templateTopology);
477         this.formTreeData();
478         this.enrichPackage();
479         this.designerStore.clear();
480         this.packageCreationStore.clear();
481     }
482
483     private formTreeData() {
484         FilesContent.clear();
485         let packageCreationModes: PackageCreationModes;
486         this.cbaPackage = PackageCreationModes.mapModeType(this.cbaPackage);
487         this.cbaPackage.metaData = PackageCreationModes.setEntryPoint(this.cbaPackage.metaData);
488         packageCreationModes = PackageCreationBuilder.getCreationMode(this.cbaPackage);
489         packageCreationModes.execute(this.cbaPackage, this.packageCreationUtils);
490         this.filesData.push(this.folder.TREE_DATA);
491     }
492     private enrichPackage() {
493         this.create();
494         this.zipFile.generateAsync({ type: 'blob' })
495             .then(blob => {
496                 this.packageCreationService.enrichPackage(blob).subscribe(response => {
497                     console.log('success');
498                     const blobInfo = new Blob([response], { type: 'application/octet-stream' });
499                     this.packageCreationStore.clear();
500                     this.packageCreationExtractionService.extractBlobToStore(blobInfo);
501                     this.toastService.success('Enriched successfully ');
502                 }, err => {
503                     console.log(err);
504                     this.toastService.error(err.message, 'Enrich Failed');
505                 }, () => {
506                     this.ngxService.stop();
507                 });
508             }, error => {
509                 this.toastService.error(error.mesasge, 'error happened ');
510                 console.error('Error -' + error.message);
511             }, () => {
512                 this.ngxService.stop();
513             });
514     }
515
516     create() {
517         this.zipFile = new JSZip();
518         FilesContent.getMapOfFilesNamesAndContent().forEach((value, key) => {
519             this.zipFile.folder(key.split('/')[0]);
520             this.zipFile.file(key, value);
521         });
522
523     }
524
525     saveBluePrintToDataBase() {
526         this.create();
527         this.zipFile.generateAsync({ type: 'blob' })
528             .then(blob => {
529                 this.packageCreationService.savePackage(blob).subscribe(
530                     bluePrintDetailModels => {
531                         this.toastService.info('success updating the package');
532                         const id = bluePrintDetailModels.toString().split('id')[1].split(':')[1].split('"')[1];
533                         this.router.navigate(['/packages/designer/' + id]);
534                         console.log('success');
535                     }, error => {
536                         this.toastService.error('error happened when editing ' + error.message);
537                         console.log('Error -' + error.message);
538                     }, () => {
539                         this.ngxService.stop();
540                     });
541             }, err => { },
542                 () => { this.ngxService.stop(); });
543     }
544
545     openActionAttributes(customActionName: string) {
546         console.log('opening here action attributes');
547         this.currentActionName = customActionName;
548         this.actionAttributesSideBar = true;
549         this.functionAttributeSidebar = false;
550         this.designerStore.setCurrentAction(customActionName);
551         /* tslint:disable:no-string-literal */
552         this.steps = Object.keys(this.designerState.template.workflows[customActionName]['steps']);
553     }
554
555     openFunctionAttributes(customFunctionName: string) {
556         // console.log(customFunctionName);
557         this.actionAttributesSideBar = false;
558         this.functionAttributeSidebar = true;
559         // console.log(this.designerState.template.workflows[this.currentActionName]
560         // ['steps'][customFunctionName]['target']);
561         this.designerStore.setCurrentFunction(this.designerState.template.workflows[this.currentActionName]
562         ['steps'][customFunctionName]['target']);
563     }
564
565     getTarget(stepname) {
566         return this.designerState.template.workflows[this.currentActionName]
567         ['steps'][stepname]['target'];
568     }
569 }