1 /* Copyright (c) 2019 AT&T Intellectual Property. #
\r
3 # Licensed under the Apache License, Version 2.0 (the "License"); #
\r
4 # you may not use this file except in compliance with the License. #
\r
5 # You may obtain a copy of the License at #
\r
7 # http://www.apache.org/licenses/LICENSE-2.0 #
\r
9 # Unless required by applicable law or agreed to in writing, software #
\r
10 # distributed under the License is distributed on an "AS IS" BASIS, #
\r
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
\r
12 # See the License for the specific language governing permissions and #
\r
13 # limitations under the License. #
\r
14 ##############################################################################*/
\r
17 import { Component, OnInit, ViewChild, ElementRef, AfterViewInit, HostListener } from '@angular/core';
\r
18 import minimapModule from 'diagram-js-minimap';
\r
19 import { FileTransferService } from 'app/shared/services/file-transfer.service';
\r
20 import { Buffer } from 'buffer';
\r
21 import propertiesPanelModule from 'bpmn-js-properties-panel';
\r
22 import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda';
\r
23 import * as camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda.json';
\r
24 import * as vthTemplate from './templates/elements.json';
\r
25 import * as $ from 'jquery';
\r
26 import { MatDialog, MatSnackBar } from '@angular/material';
\r
27 import { TestHeadService } from 'app/shared/services/test-head.service';
\r
28 import { GroupService } from 'app/shared/services/group.service';
\r
29 import { TestDefinitionService } from 'app/shared/services/test-definition.service';
\r
30 import { CookieService } from 'ngx-cookie-service';
\r
31 import { FileService } from 'app/shared/services/file.service';
\r
32 import { ActivatedRoute, Router } from '@angular/router';
\r
33 import { BpmnFactoryService } from 'app/shared/factories/bpmn-factory.service';
\r
34 import { Bpmn } from 'app/shared/models/bpmn.model';
\r
35 import { TestDefinitionElement, BpmnInstanceElement } from './test-definition-element.class.js';
\r
36 import { FileUploader } from 'ng2-file-upload';
\r
37 import { Group } from 'app/shared/models/group.model.js';
\r
38 import { AlertSnackbarComponent } from 'app/shared/modules/alert-snackbar/alert-snackbar.component';
\r
39 import { AlertModalComponent } from 'app/shared/modules/alert-modal/alert-modal.component';
\r
41 interface NewVersionOptions {
\r
42 versionIndex: number,
\r
47 selector: 'app-modeler',
\r
48 templateUrl: './modeler.component.pug',
\r
50 './modeler.component.scss',
\r
54 export class ModelerComponent implements OnInit {
\r
56 @ViewChild('container') containerElement: ElementRef;
\r
57 @ViewChild('modeler') modelerElement: ElementRef;
\r
58 @ViewChild('sidebar') sidebarElement: ElementRef;
\r
59 @ViewChild('properties') propertiesElement: ElementRef;
\r
60 @ViewChild('handle') handleElement: ElementRef;
\r
62 @ViewChild('testDefinitionForm') form: any;
\r
63 @ViewChild('scripts') scripts: ElementRef;
\r
64 @ViewChild('file') bpmnFileInput: ElementRef;
\r
66 public qpTestDefinitionId;
\r
68 public ptd: TestDefinitionElement;
\r
69 public uploader: FileUploader;
\r
70 public bpmnUploader: FileUploader;
\r
71 public pStatus: String;
\r
72 public inProgress: Boolean;
\r
73 public groups: Array<Group>;
\r
74 public modeler: Bpmn;
\r
75 public showProperties = true;
\r
76 public isResizing = false;
\r
77 public lastDownX = 0;
\r
78 public propertiesWidth = '500px';
\r
79 public showSidebar = true;
\r
80 public showTestDefinition = false;
\r
81 public bpmnId; //javascript input element
\r
82 public isRefreshed = false;
\r
83 public hasBeenSaved: Boolean = false;
\r
86 public _dialog: MatDialog,
\r
87 private _testHeads: TestHeadService,
\r
88 private _groups: GroupService,
\r
89 private _testDefinitions: TestDefinitionService,
\r
90 private _snack: MatSnackBar,
\r
91 private _fileTransfer: FileTransferService,
\r
92 private _route: ActivatedRoute,
\r
93 private _router: Router,
\r
94 private _bpmnFactory: BpmnFactoryService) {
\r
97 @HostListener('window:beforeunload', ['$event'])
\r
98 canLeavePage($event) {
\r
99 $event.preventDefault();
\r
100 alert('are you sure')
\r
105 this._route.queryParams.subscribe(res => {
\r
106 if (res.testDefinitionId) {
\r
107 this.qpTestDefinitionId = res.testDefinitionId;
\r
109 this.qpTestDefinitionId = null;
\r
115 this._groups.find({
\r
118 }).subscribe(res => {
\r
119 this.groups = res as Array<Group>;
\r
120 this.groups = this._groups.organizeGroups(this.groups);
\r
128 this.setInProgress(true);
\r
130 await this.setTestDefinition();
\r
132 const modelerOptions = {
\r
133 container: this.modelerElement.nativeElement,
\r
135 parent: '#properties'
\r
137 elementTemplates: [vthTemplate],
\r
138 additionalModules: [
\r
140 propertiesPanelModule,
\r
141 propertiesProviderModule
\r
142 // colorPickerModule,
\r
143 // logTestResultDrawModule,
\r
144 // logTestResultPaletteModule
\r
146 moddleExtensions: {
\r
147 camunda: camundaModdleDescriptor
\r
154 // Set up empty modeler
\r
155 await this.setModeler({
\r
157 options: modelerOptions
\r
160 this.setBpmn(false);
\r
162 //set ups draggable properties container
\r
163 $(this.handleElement.nativeElement).on('mousedown', e => {
\r
164 this.lastDownX = e.clientX;
\r
165 this.isResizing = true;
\r
168 $(document).on('mousemove', e => {
\r
169 if (!this.isResizing)
\r
172 var offsetRight = $(this.containerElement.nativeElement).width() - (e.clientX - $(this.containerElement.nativeElement).offset().left);
\r
174 $(this.modelerElement.nativeElement).css('right', offsetRight);
\r
175 $(this.sidebarElement.nativeElement).css('width', offsetRight);
\r
176 }).on('mouseup', e => {
\r
177 this.isResizing = false;
\r
183 /*****************************************
\r
184 * Form Functionality Methods
\r
185 ****************************************/
\r
189 async newWorkflow() {
\r
190 if (this.qpTestDefinitionId) {
\r
191 this._router.navigate([], {
\r
200 this.modeler.download();
\r
204 this.setInProgress(true);
\r
205 let validResult = await this.validateFile();
\r
208 if (this.hasBeenSaved) {
\r
209 await this.updateDefinition();
\r
211 let td = await this.saveDefinition();
\r
212 this._router.navigate([], {
\r
214 testDefinitionId: td['_id']
\r
220 this.snackAlert('Version ' + this.ptd.currentVersionName + ' has been saved');
\r
221 this.setInProgress(false);
\r
222 this.markFormAs('pristine');
\r
225 async deploy(versionName?) {
\r
226 this.inProgress = true;
\r
228 this._testDefinitions.deploy(this.ptd, versionName)
\r
231 this.inProgress = false;
\r
232 if (result['statusCode'] == 200) {
\r
233 this.snackAlert('Test Definition Deployed Successfully')
\r
234 this.ptd.currentInstance.isDeployed = true;
\r
236 this.errorPopup(result.toString());
\r
240 this.errorPopup(err.toString());
\r
241 this.setInProgress(false);
\r
247 async deleteVersion() {
\r
248 let deleteDialog = this._dialog.open(AlertModalComponent, {
\r
250 data: { type: 'confirmation', message: 'Are you sure you want to delete version ' + this.ptd.currentVersionName }
\r
253 deleteDialog.afterClosed().subscribe(
\r
256 this.setInProgress(true);
\r
257 if (this.ptd.bpmnInstances.length == 1) {
\r
258 this._testDefinitions.delete(this.ptd._id).subscribe(
\r
260 this.snackAlert('Test definition was deleted');
\r
261 this.setInProgress(false);
\r
262 this.newWorkflow();
\r
265 this.setInProgress(false);
\r
266 this.errorPopup(err.toString());
\r
270 let version = this.ptd.currentVersionName;
\r
271 // if deleting a version from a definition that has more than 1 version
\r
272 this.ptd.removeBpmnInstance(this.ptd.currentVersionName);
\r
274 //prepare patch request
\r
277 bpmnInstances: this.ptd.bpmnInstances
\r
280 this._testDefinitions.patch(request).subscribe(
\r
283 this.setInProgress(false);
\r
284 this.snackAlert('Version ' + version + ' was deleted');
\r
287 this.setInProgress(false);
\r
288 this.errorPopup(err.toString());
\r
298 /*** UTILITY METHODS ***/
\r
300 //Looks for the definition supplied in the url, or pulls up default workflow
\r
301 async setTestDefinition() {
\r
302 return new Promise((resolve, reject) => {
\r
303 if (this.qpTestDefinitionId) {
\r
304 this._testDefinitions.get(this.qpTestDefinitionId).subscribe(
\r
307 this.ptd = new TestDefinitionElement();
\r
308 this.ptd.setAll(result);
\r
309 this.setAsSaved(true);
\r
313 this.errorPopup(err.toString());
\r
318 //set new test definition
\r
319 this.ptd = new TestDefinitionElement();
\r
326 //will set the selected version. If no version is given, the latest will be selected
\r
327 async setVersion(version?) {
\r
329 //if version not supplied, grab latest
\r
330 this.ptd.switchVersion(version);
\r
332 this.setBpmn(true);
\r
338 async newVersion(options?: NewVersionOptions) {
\r
340 if (options && options.versionIndex != null) {
\r
342 //create new instance and copy xml
\r
343 let instance = this.ptd.newInstance();
\r
344 instance.bpmnFileId = this.ptd.bpmnInstances[options.versionIndex].bpmnFileId;
\r
345 instance.bpmnXml = this.ptd.bpmnInstances[options.versionIndex].bpmnXml;
\r
347 this.ptd.addBpmnInstance(instance);
\r
349 } else if ( options && options.fromFile) {
\r
351 let instance = this.ptd.newInstance();
\r
353 instance.bpmnFileId = '0';
\r
354 let xml = await new Promise((resolve, reject) => {
\r
355 this.fetchFileContents('fileForVersion', xml => {
\r
360 instance.bpmnXml = xml as String;
\r
362 //set the files process definition key
\r
363 let parser = new DOMParser();
\r
364 let xmlDoc = parser.parseFromString(instance.bpmnXml.toString(), "text/xml");
\r
365 //set the process definition key in xml
\r
366 xmlDoc.getElementsByTagName("bpmn:process")[0].attributes.getNamedItem("id").value = this.ptd.processDefinitionKey as string;
\r
368 instance.bpmnXml = (new XMLSerializer()).serializeToString(xmlDoc);
\r
370 this.ptd.addBpmnInstance(instance);
\r
373 this.ptd.addBpmnInstance();
\r
377 this.markFormAs('dirty');
\r
378 this.ptd.currentInstance.bpmnHasChanged = true;
\r
385 async validateFile() {
\r
386 return new Promise((resolve, reject) => {
\r
388 this.modeler.getBpmnXml().then(xml => {
\r
390 this.ptd.currentInstance.bpmnXml = xml.toString();
\r
392 this._testDefinitions.validate(this.ptd)
\r
396 if (result['body'].errors && result['body'].errors != {}) {
\r
397 this.errorPopup(JSON.stringify(result['body'].errors));
\r
400 //this.handleResponse(result, false);
\r
401 //this.ptd.currentInstance.bpmnHasChanged = true;
\r
403 // If any VTH or PFLOs were detected, add to object
\r
404 // Update list of test heads
\r
405 if (result['body']['bpmnVthTaskIds']) {
\r
406 this.ptd.currentInstance.testHeads = result['body'].bpmnVthTaskIds;
\r
407 this.ptd.currentInstance.testHeads.forEach((elem, val) => {
\r
408 this.ptd.currentInstance.testHeads[val]['testHeadId'] = elem['testHead']._id;
\r
409 delete this.ptd.currentInstance.testHeads[val]['testHead'];
\r
414 //Update plfos list
\r
415 if(result['body']['bpmnPfloTaskIds']){
\r
416 this.ptd.currentInstance.pflos = result['body'].bpmnPfloTaskIds;
\r
427 this.errorPopup(err);
\r
434 //returns promise for file object
\r
435 async saveBpmnFile() {
\r
436 return new Promise((resolve, reject) => {
\r
438 this.modeler.getBpmnXml().then(
\r
440 this.ptd.currentInstance.bpmnXml = res as String;
\r
441 this._testDefinitions.validateSave(this.ptd).subscribe(
\r
443 resolve(JSON.parse(result.toString())[0]._id);
\r
446 this.errorPopup(err.toString());
\r
455 async saveDefinition() {
\r
457 return new Promise(async (resolve, reject) => {
\r
459 let fileId = await this.saveBpmnFile();
\r
462 this.ptd.currentInstance.bpmnFileId = fileId as String;
\r
465 delete this.ptd._id;
\r
467 this._testDefinitions.create(this.ptd).subscribe(
\r
473 this.errorPopup(err.message);
\r
474 this.setInProgress(false);
\r
482 async updateDefinition() {
\r
483 return new Promise(async (resolve, reject) => {
\r
485 let versionIndex = this.ptd.currentVersion;
\r
487 // set parameters to be sent with the patch
\r
490 testName: this.ptd.testName,
\r
491 testDescription: this.ptd.testDescription,
\r
492 groupId: this.ptd.groupId
\r
495 // If xml has changed, upload file and patch definition details, else just updated details
\r
496 if (this.ptd.currentInstance.bpmnHasChanged) {
\r
499 let fileId = await this.saveBpmnFile();
\r
501 //set file id in the bpmn instance
\r
503 this.ptd.currentInstance.bpmnFileId = fileId as String;
\r
507 //check if this bpmn version has been saved, else its a new version
\r
508 if (this.ptd.currentInstance.createdAt) {
\r
509 this.ptd.currentInstance.updatedAt = new Date().toISOString();
\r
510 request['bpmnInstances.' + this.ptd.currentVersion] = this.ptd.currentInstance;
\r
512 this.ptd.currentInstance.createdAt = new Date().toISOString();
\r
513 this.ptd.currentInstance.updatedAt = new Date().toISOString();
\r
514 request['$push'] = {
\r
515 bpmnInstances: this.ptd.currentInstance
\r
519 //patch with updated fields
\r
520 this._testDefinitions.patch(request).subscribe(res => {
\r
521 this.ptd.currentInstance.bpmnHasChanged = false;
\r
530 markFormAs(mode: 'dirty' | 'pristine') {
\r
531 if (mode == 'dirty') {
\r
532 this.form.control.markAsDirty();
\r
534 this.form.control.markAsPristine();
\r
539 async checkProcessDefinitionKey() {
\r
540 let foundDefinition = null;
\r
542 this._testDefinitions.check(this.ptd.processDefinitionKey).subscribe(async result => {
\r
543 if (result['statusCode'] == 200) {
\r
544 this.pStatus = 'unique';
\r
546 this.pStatus = 'notUnique';
\r
550 //If process definition key found
\r
551 if (result['body'] && result['body'][0]) {
\r
553 foundDefinition = result['body'][0];
\r
556 //seach mongodb for td with pdk
\r
557 await new Promise((resolve, reject) => {
\r
558 this._testDefinitions.find({
\r
559 processDefinitionKey: this.ptd.processDefinitionKey
\r
560 }).subscribe(res => {
\r
562 if (res['total'] > 0) {
\r
563 foundDefinition = res['data'][0];
\r
572 if (foundDefinition) {
\r
573 if (this.qpTestDefinitionId != foundDefinition._id) {
\r
574 let confirm = this._dialog.open(AlertModalComponent, {
\r
577 type: 'confirmation',
\r
578 message: 'The process definition key "' + this.ptd.processDefinitionKey + '" already exists. Would you like to load the test definition, ' + foundDefinition.testName + ' ? This will delete any unsaved work.'
\r
582 confirm.afterClosed().subscribe(doChange => {
\r
584 this._router.navigate([], {
\r
586 testDefinitionId: foundDefinition._id
\r
590 this.bpmnId.value = '';
\r
595 let tempPK = this.ptd.processDefinitionKey;
\r
598 this.ptd.setProcessDefinitionKey(tempPK);
\r
600 this.ptd.setId(null);
\r
601 this.ptd.setName('');
\r
602 this.ptd.setDescription('');
\r
603 this.ptd.setGroupId('');
\r
604 this.ptd.setVersion(1);
\r
605 this.setAsSaved(false);
\r
608 if (!this.ptd.currentInstance.version) {
\r
609 this.ptd.setNewVersion();
\r
612 this.markFormAs('pristine');
\r
614 this.ptd.currentInstance.bpmnHasChanged = false;
\r
620 setInProgress(mode: Boolean) {
\r
621 this.inProgress = mode;
\r
624 setAsSaved(mode: Boolean) {
\r
625 this.hasBeenSaved = mode;
\r
628 /*****************************************
\r
629 * BPMN Modeler Functions
\r
630 ****************************************/
\r
632 async setBpmn(isNewVersion: Boolean, xml?) {
\r
634 //If a test definition is loaded set bpmnXml with latest version, else set default flow
\r
636 this.ptd.currentInstance.bpmnXml = xml;
\r
638 if (this.ptd._id && this.ptd.currentInstance.bpmnFileId) {
\r
639 if (!this.ptd.currentInstance.bpmnXml) {
\r
640 this.ptd.currentInstance.bpmnXml = await this.getVersionBpmn() as String;
\r
643 this.ptd.currentInstance.bpmnXml = await this.getDefaultFlow() as String;
\r
645 // If it is a blank new version, set the process definition key in xml
\r
646 if (isNewVersion) {
\r
647 let parser = new DOMParser();
\r
649 let xmlDoc = parser.parseFromString(this.ptd.currentInstance.bpmnXml.toString(), "text/xml");
\r
650 //set the process definition key in xml
\r
651 xmlDoc.getElementsByTagName("bpmn:process")[0].attributes.getNamedItem("id").value = this.ptd.processDefinitionKey as string;
\r
653 this.ptd.currentInstance.bpmnXml = (new XMLSerializer()).serializeToString(xmlDoc);
\r
659 await this.modeler.setBpmnXml(this.ptd.currentInstance.bpmnXml);
\r
662 this.bpmnId = (<HTMLInputElement>document.getElementById("camunda-id"));
\r
664 if (!isNewVersion) {
\r
665 //Set process Definition key
\r
666 this.ptd.processDefinitionKey = this.bpmnId.value;
\r
668 //Check the process Definition key to get its test definition loaded in.
\r
670 this.checkProcessDefinitionKey();
\r
673 //Listen for any changes made to the diagram and properties panel
\r
674 this.modeler.getModel().on('element.changed', (event) => {
\r
675 //check to see if process definition key has changed
\r
676 if (event.element.type == 'bpmn:Process' && (this.ptd.processDefinitionKey != event.element.id)) {
\r
677 this.ptd.processDefinitionKey = event.element.id;
\r
678 this.checkProcessDefinitionKey();
\r
681 // If it has been deployed, they cannot edit and save it
\r
682 if (!this.ptd.currentInstance.isDeployed) {
\r
683 this.ptd.currentInstance.bpmnHasChanged = true;
\r
684 this.markFormAs('dirty');
\r
688 this.setInProgress(false);
\r
692 //Open a .bpmn file
\r
695 this.setInProgress(true);
\r
697 this.ptd.switchVersion();
\r
699 this.fetchFileContents('file', val => {
\r
700 this.setBpmn(false, val);
\r
705 //Get the xml of the default bpmn file
\r
706 async getDefaultFlow() {
\r
707 return new Promise((resolve, reject) => {
\r
708 this._fileTransfer.get('5d0a5357e6624a3ef0d16164').subscribe(
\r
710 let bpmn = new Buffer(data as Buffer);
\r
711 resolve(bpmn.toString());
\r
714 this.errorPopup(err.toString());
\r
722 async setModeler(options) {
\r
723 if (!this.modeler) {
\r
724 this.modeler = await this._bpmnFactory.setup(options);
\r
728 async getVersionBpmn() {
\r
729 return new Promise((resolve, reject) => {
\r
730 this._fileTransfer.get(this.ptd.currentInstance.bpmnFileId).subscribe(
\r
732 let bpmn = new Buffer(result as Buffer);
\r
733 resolve(bpmn.toString());
\r
736 this.errorPopup(err.toString());
\r
743 fetchFileContents(elementId, callback) {
\r
745 var fileToLoad = (document.getElementById(elementId))['files'][0];
\r
746 var fileReader = new FileReader();
\r
751 fileReader.onload = function (event) {
\r
752 val = event.target['result'] as string;
\r
755 fileReader.readAsText(fileToLoad);
\r
758 /*****************************************
\r
759 * Page Funtionality Methods
\r
760 ****************************************/
\r
762 toggleSidebar(set: Boolean) {
\r
764 this.showSidebar = false;
\r
765 this.modelerElement.nativeElement.style.right = '0px';
\r
767 this.showSidebar = true;
\r
768 $(this.modelerElement.nativeElement).css('right', $(this.sidebarElement.nativeElement).width());
\r
772 toggleProperties() {
\r
773 if (!this.showProperties) {
\r
774 this.toggleSidebar(true);
\r
775 this.showTestDefinition = false;
\r
776 this.showProperties = true;
\r
778 this.toggleSidebar(false);
\r
779 this.showProperties = false;
\r
783 toggleTestDefinition() {
\r
784 if (!this.showTestDefinition) {
\r
785 this.toggleSidebar(true);
\r
786 this.showProperties = false;
\r
787 this.showTestDefinition = true;
\r
789 this.toggleSidebar(false);
\r
790 this.showTestDefinition = false;
\r
797 this.isRefreshed = false;
\r
799 this.isRefreshed = true;
\r
804 this._snack.openFromComponent(AlertSnackbarComponent, {
\r
813 return this._dialog.open(AlertModalComponent, {
\r