Merge "Refactor, fix code formatting and add unittests"
[dcaegen2/platform.git] / mod2 / ui / src / app / blueprints / blueprints.component.ts
1 /* 
2  *  # ============LICENSE_START=======================================================
3  *  # Copyright (c) 2020 AT&T Intellectual Property. All rights reserved.
4  *  # ================================================================================
5  *  # Licensed under the Apache License, Version 2.0 (the "License");
6  *  # you may not use this file except in compliance with the License.
7  *  # You may obtain a copy of the License at
8  *  #
9  *  #      http://www.apache.org/licenses/LICENSE-2.0
10  *  #
11  *  # Unless required by applicable law or agreed to in writing, software
12  *  # distributed under the License is distributed on an "AS IS" BASIS,
13  *  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  # See the License for the specific language governing permissions and
15  *  # limitations under the License.
16  *  # ============LICENSE_END=========================================================
17  */
18
19 import { Component, OnInit, ViewChild, ElementRef, Input, EventEmitter, Output, ChangeDetectorRef } from '@angular/core';
20 import { Table } from 'primeng/table';
21 import { MessageService } from 'primeng/api';
22 import { trigger, state, style, transition, animate } from '@angular/animations';
23 import * as saveAs from 'file-saver';
24 import * as JSZip from 'jszip';
25 import { AuthService } from '../services/auth.service';
26 import { DatePipe } from '@angular/common';
27 import { DeploymentArtifactService } from '../services/deployment-artifact.service';
28 import { Ng4LoadingSpinnerService } from 'ng4-loading-spinner';
29 import { Toast } from 'primeng/toast'
30 import { ActivatedRoute } from '@angular/router';
31 import { DownloadService } from '../services/download.service';
32
33 @Component({
34   selector: 'app-blueprints',
35   templateUrl: './blueprints.component.html',
36   styleUrls: ['./blueprints.component.css'],
37   animations: [
38     trigger('rowExpansionTrigger', [
39       state('void', style({
40         transform: 'translateX(-10%)',
41         opacity: 0
42       })),
43       state('active', style({
44         transform: 'translateX(0)',
45         opacity: 1
46       })),
47       transition('* <=> *', animate('400ms cubic-bezier(0.86, 0, 0.07, 1)'))
48     ])
49   ],
50   providers: [DatePipe, MessageService]
51 })
52 export class BlueprintsComponent implements OnInit {
53   @ViewChild(Table, { static: false }) dt: Table;
54   @ViewChild(Toast, { static: false }) toast: Toast;
55
56   /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. **/
57   bpElements: BlueprintElement[] = [];
58   cols: any[] = [
59     { field: 'instanceName', header: 'Instance Name' },
60     { field: 'instanceRelease', header: 'Instance Release', width: '7%' },
61     { field: 'tag', header: 'Tag' },
62     { field: 'type', header: 'Type', width: '7%' },
63     { field: 'version', header: 'Version', width: '6%' },
64     { field: 'status', header: 'Status', width: '125px' }];
65   states: {field: string, label: string}[] = [];
66   columns: any[];
67   filteredRows: any;
68   downloadItems: { label: string; command: () => void; }[];
69   username: string;
70   showBpContentDialog: boolean = false;
71   selectedBPs: BlueprintElement[] = [];
72   //  Hides the BP list until the rows are retrieved and filtered
73   visible = "hidden";
74   // These 2 fields are passed from MS Instance to filter the BP list
75   tag: string;
76   release: string;
77
78   filteredName:    string;
79   filteredRelease: string;
80   filteredTag:     string;
81   filteredType:    string;
82   filteredVersion: string;
83   filteredStatus:  string;
84
85   constructor(private change: ChangeDetectorRef, private messageService: MessageService, private authService: AuthService,
86               private datePipe: DatePipe, private bpApis: DeploymentArtifactService, private spinnerService: Ng4LoadingSpinnerService,
87               private route: ActivatedRoute, private downloadService: DownloadService) { }
88
89   ngOnInit() {
90     
91     this.username = this.authService.getUser().username;
92     
93     this.getStates();
94     this.getAllBPs();
95
96     this.change.markForCheck();
97
98     this.route.queryParams.subscribe((params) => {
99       this.filteredTag     = params['tag'];
100       this.filteredRelease = params['release']});
101   }
102
103   //gets statuses for status updates
104   getStates(){
105     this.states = []
106     this.bpApis.getStatuses().subscribe((response) => {this.setMenuStates(response)})
107   }
108
109   //fills actions menu with states
110   setMenuStates(states){
111     for(let item of states){
112       this.states.push({
113         field: item,
114         label: 'To  ' + item
115       })
116     }
117   }
118
119   canDelete: boolean = false;
120   canDownload: boolean = false;
121   canUpdate: boolean = false;
122   deleteTooltip: string;
123   enableButtonCheck(){
124     if(this.selectedBPs.length > 0){
125       this.canDownload = true;
126       this.canUpdate = true;
127       
128       for(let item of this.selectedBPs){
129         if (item.status !== 'IN_DEV' && item.status !== 'NOT_NEEDED' && item.status !== 'DEV_COMPLETE'){
130           this.canDelete = false;
131           this.deleteTooltip = 'Only blueprints that are in a status of "In Dev", "Not Needed" or "Dev Complete" can be deleted'
132           break
133         } else {
134           this.canDelete = true;
135         }
136       }
137
138     } else {
139       this.canDownload = false;
140       this.canUpdate = false;
141       this.canDelete = false;
142       this.deleteTooltip = 'No Blueprints Selected'
143     }
144   }
145
146   updateStateTo: string = ''; //selected state to update blueprint to
147   //checks if there are different releases/statuses selected
148   updateSelectedStatusesCheck(state){
149     this.updateStateTo = state.field
150     let multipleStates: boolean = false
151     let multipleReleases: boolean = false
152     let firstStatus = this.selectedBPs[0]['status']
153     let firstRelease = this.selectedBPs[0]['instanceRelease']
154
155     for(let bp of this.selectedBPs){
156       if(bp.instanceRelease !== firstRelease){
157         multipleReleases = true
158       }
159       if (bp.status !== firstStatus) {
160         multipleStates = true
161       }
162     }
163
164     if(multipleReleases && multipleStates){
165       this.messageService.add({ key: 'confirmToast', sticky: true, severity: 'warn', summary: 'Are you sure?', detail: 'You are about to update blueprints for different releases and statuses. Confirm to proceed.' });
166     } else if (multipleReleases && !multipleStates) {
167       this.messageService.add({ key: 'confirmToast', sticky: true, severity: 'warn', summary: 'Are you sure?', detail: 'You are about to update blueprints for different releases. Confirm to proceed.' });
168     } else if (!multipleReleases && multipleStates) {
169       this.messageService.add({ key: 'confirmToast', sticky: true, severity: 'warn', summary: 'Are you sure?', detail: 'You are about to update blueprints for different statuses. Confirm to proceed.' });
170     } else if (!multipleReleases && !multipleStates){
171       this.updateSelectedStatuses()
172     }
173   }
174   onConfirm() {
175     this.messageService.clear('confirmToast')
176     this.updateSelectedStatuses()
177   }
178   onReject() {
179     this.messageService.clear('confirmToast')
180   }
181
182   /* * * * Update status for multiple blueprints * * * */
183   successfulStatusUpdates: number = 0 //keeps track of how many status updates were successful
184   selectionLength: number = 0 //length of array of blueprints with different statuses than update choice
185   statusUpdateCount: number = 0 //keeps track of how many api calls have been made throughout a loop
186   statusUpdateErrors: string[] = [] //keeps list of errors
187   updateSelectedStatuses(){
188     this.successfulStatusUpdates = 0      
189     this.statusUpdateErrors = []          
190     this.statusUpdateCount = 0
191
192     let bpsToUpdate = this.selectedBPs.filter(bp => bp.status !== this.updateStateTo) //array of blueprints with different statuses than update choice
193     this.selectionLength = bpsToUpdate.length;
194
195     if (this.selectionLength === 0) { this.selectedBPs = [] } else {
196       this.spinnerService.show();
197       this.updateState(this.updateStateTo, bpsToUpdate, true)
198     }
199   }
200
201   /* * * * Update Statuses * * * */
202   //state is the state to update to
203   //data is the bp data from selection
204   //multiple is whether updates were called for single blueprint or multiple selected blueprints
205   updateState(state, data, multiple){
206     //single status update
207     if(!multiple){
208       this.bpApis.patchBlueprintStatus(state.field, data['id']).subscribe(
209         (response: string) => {
210           data.status = state.field
211           this.messageService.add({ key: 'statusUpdate', severity: 'success', summary: 'Status Updated' });
212         }, errResponse => {
213           this.statusUpdatesResponseHandler(errResponse, false)
214         }
215       )
216     } 
217     
218     //multiple status updates
219     if(multiple){
220       (async () => {
221         for (let bp of data) {
222           this.bpApis.patchBlueprintStatus(this.updateStateTo, bp.id).subscribe(
223             (response: string) => {
224               bp.status = this.updateStateTo
225               this.statusUpdatesResponseHandler(null, true)
226             }, errResponse => {
227               this.statusUpdatesResponseHandler(errResponse, true)
228             }
229           )
230           await timeout(1500);
231         }
232       })();
233
234       function timeout(ms) {
235         return new Promise(resolve => setTimeout(resolve, ms));
236       }
237     }
238   }
239
240   /* * * * Handles errors and messages for status updates * * * */
241   statusUpdatesResponseHandler(response, multiple){
242     if(!multiple){
243       if(response !== null){
244         if (response.error.message.includes('Only 1 blueprint can be in the DEV_COMPLETE state.')) {
245           let message = response.error.message.replace('Only 1 blueprint can be in the DEV_COMPLETE state.  ', '\n\nOnly 1 blueprint can be in the DEV_COMPLETE state.\n')
246           this.messageService.add({ key: 'statusUpdate', severity: 'error', summary: 'Status Not Updated', detail: message, sticky: true });
247         } else {
248           this.messageService.add({ key: 'statusUpdate', severity: 'error', summary: 'Error Message', detail: response.error.message, sticky: true });
249         }
250       }
251     }
252
253     if(multiple){
254       this.statusUpdateCount++
255       if (response === null) {
256         this.successfulStatusUpdates++
257       } else {
258         if (response.error.message.includes('Only 1 blueprint can be in the DEV_COMPLETE state.')) {
259           let error = response.error.message.split('Only 1 blueprint can be in the DEV_COMPLETE state.')[0]
260           this.statusUpdateErrors.push(error)
261         } else { 
262           this.messageService.add({ key: 'statusUpdate', severity: 'error', summary: 'Error Message', detail: response.error.message, sticky: true });
263         }
264       }
265
266       if (this.statusUpdateCount === this.selectionLength) {
267         if (this.successfulStatusUpdates > 0) {
268           this.messageService.add({ key: 'statusUpdate', severity: 'success', summary: `(${this.successfulStatusUpdates} of ${this.selectionLength}) Statuses Updated`, life: 5000 });
269         }
270         if (this.statusUpdateErrors.length > 0) {
271           let message: string = ''
272           for (let elem of this.statusUpdateErrors) {
273             message += '- ' + elem + '\n'
274           }
275           message += '\nOnly 1 blueprint can be in the DEV_COMPLETE state.\nChange the current DEV_COMPLETE blueprint to NOT_NEEDED or IN_DEV before changing another to DEV_COMPLETE.'
276           this.messageService.add({ key: 'statusUpdate', severity: 'error', summary: 'Statuses Not Updated', detail: message, sticky: true });
277         }
278         this.spinnerService.hide()
279         this.selectedBPs = []
280       }
281     }
282   }
283
284   bpToDelete: any;
285   deleteSingle: boolean = false;
286   rowIndexToDelete;
287   rowIndexToDeleteFiltered;
288   warnDeleteBlueprint(data){
289     if(data !== null){
290       this.deleteSingle = true;
291       this.rowIndexToDeleteFiltered = this.filteredRows.map(function (x) { return x.id; }).indexOf(data['id']);
292       this.rowIndexToDelete = this.bpElements.map(function (x) { return x.id; }).indexOf(data['id']);
293       this.bpToDelete = data;
294       this.messageService.add({ key: 'confirmDeleteToast', sticky: true, severity: 'warn', summary: 'Are you sure?', detail: `- ${data.instanceName} (v${data.version}) for ${data.instanceRelease}` });
295     } else {
296       this.deleteSingle = false;
297       this.selectionLength = this.selectedBPs.length;
298       let warnMessage: string = ''
299       for(let item of this.selectedBPs){
300         warnMessage += `- ${item.instanceName} (v${item.version}) for ${item.instanceRelease}\n`
301       }
302       this.messageService.add({ key: 'confirmDeleteToast', sticky: true, severity: 'warn', summary: 'Are you sure?', detail: warnMessage });
303     }    
304   }
305
306   resetFilter = false;
307   onConfirmDelete() {
308     this.messageService.clear('confirmDeleteToast')
309
310     if (this.filteredName !== '' || this.filteredRelease !== '' || this.filteredTag !== '' || this.filteredType !== '' || this.filteredVersion !== '' || this.filteredStatus !== ''){
311       this.resetFilter = true;
312     } else {this.resetFilter = false}
313     
314     if(this.deleteSingle){
315       this.bpApis.deleteBlueprint(this.bpToDelete['id']).subscribe(response => {
316         this.checkBpWasSelected(this.bpToDelete['id'])
317         this.bpElements.splice(this.rowIndexToDelete, 1)
318         if (this.resetFilter) {
319           this.resetFilters()
320         }
321         this.messageService.add({ key: 'bpDeleteResponse', severity: 'success', summary: 'Success Message', detail: 'Deployment Artifact Deleted' });
322       }, error => {
323         this.messageService.add({ key: 'bpDeleteResponse', severity: 'error', summary: 'Error Message', detail: error.error.message });
324       })
325     } else {
326       for(let item of this.selectedBPs){
327         this.bpApis.deleteBlueprint(item.id).subscribe(response => {
328           this.deleteResponseHandler(true, item.id)
329         }, error => {
330           this.messageService.add({ key: 'bpDeleteResponse', severity: 'error', summary: 'Error Message', detail: error.error.message });
331         })
332       }
333     }
334   }
335   onRejectDelete() {
336     this.messageService.clear('confirmDeleteToast')
337   }
338
339   checkBpWasSelected(id){
340     if(this.selectedBPs.length > 0){
341       for(let item of this.selectedBPs){
342         if(item.id === id){
343           let indexToDelete = this.selectedBPs.map(function (x) { return x.id; }).indexOf(item['id']);
344           this.selectedBPs.splice(indexToDelete, 1)
345         }
346       }
347     }
348   }
349
350   bpsToDelete: string[] = [];
351   deleteBpCount = 0;
352   deleteResponseHandler(success, bpToDeleteId){
353     this.deleteBpCount++
354     if(success){
355       this.bpsToDelete.push(bpToDeleteId)
356     }
357     if(this.deleteBpCount === this.selectionLength){
358       for(let item of this.bpsToDelete){
359         
360         let indexToDelete = this.bpElements.map(function (x) { return x.id; }).indexOf(item);
361         this.bpElements.splice(indexToDelete, 1)
362       }
363
364       if(this.resetFilter){
365         this.resetFilters()
366       }
367
368       this.selectedBPs = [];
369       this.bpsToDelete = [];
370       this.deleteBpCount = 0;
371       this.messageService.add({ key: 'bpDeleteResponse', severity: 'success', summary: 'Success Message', detail: 'Deployment Artifacts Deleted' });
372     }
373   }
374
375   resetFilters(){
376     let filters: {field: string, value: string}[] = [];
377       filters.push({field: 'instanceName', value: this.filteredName})
378       filters.push({ field: 'instanceRelease', value: this.filteredRelease })
379       filters.push({ field: 'tag', value: this.filteredTag })
380       filters.push({ field: 'type', value: this.filteredType })
381       filters.push({ field: 'version', value: this.filteredVersion })
382       filters.push({ field: 'status', value: this.filteredStatus })
383     
384     for(let item of filters){
385       this.dt.filter(item.value, item.field, 'contains')
386     }
387   }
388
389   /* * * * Gets all blueprints * * * */
390   getAllBPs() {
391     this.spinnerService.show();
392     this.bpElements = [];
393     this.columns = this.cols.map(col => ({ title: col.header, dataKey: col.field }));
394
395     this.visible = "hidden";
396
397     this.bpApis.getAllBlueprints()
398       .subscribe((data: any[]) => {
399         this.fillTable(data)
400       })
401
402   }
403   
404   /* * * *  Checks when table is filtered and stores filtered data in new object to be downloaded when download button is clicked * * * */
405   onTableFiltered(values) {
406     if (values) {
407       this.filteredRows = values;
408     } else {
409       this.filteredRows = this.bpElements
410     }
411   }
412
413   /* * * * Download table as excel file * * * */
414   exportTable(exportTo) {
415     let downloadElements: any[] = []
416
417     for (let row of this.filteredRows) {
418       let labels;
419       let notes;
420       if (exportTo === "excel") {
421         if (row.metadata.labels !== undefined && row.metadata.labels !== null ) {
422           labels = row.metadata.labels.join(",")
423         }
424       } else {
425         labels = row.metadata.labels
426       }
427
428       if (row.metadata.notes !== null && row.metadata.notes !== undefined && row.metadata.notes !== '') {
429         notes = encodeURI(row.metadata.notes).replace(/%20/g, " ").replace(/%0A/g, "\\n")
430       }
431     
432       downloadElements.push({ 
433         Instance_Name: row.instanceName, 
434         Instance_Release: row.instanceRelease, 
435         Tag: row.tag, 
436         Type: row.type, 
437         Version: row.version, 
438         Status: row.status, 
439         Created_By: row.metadata.createdBy,
440         Created_On: row.metadata.createdOn,
441         Updated_By: row.metadata.updatedBy,
442         Updated_On: row.metadata.updatedOn,
443         Failure_Reason: row.metadata.failureReason,
444         Notes: notes,
445         Labels: labels
446       })
447     }
448     
449     let csvHeaders = []
450
451     if (exportTo === "csv") {
452       csvHeaders = [
453         "Instance_Name",
454         "Instance_Release",
455         "Tag",
456         "Type",
457         "Version",
458         "Status",
459         "Created_By",
460         "Created_On",
461         "Updated_By",
462         "Updated_On",
463         "Failure_Reason",
464         "Notes",
465         "Labels"];
466
467     }
468     
469     this.downloadService.exportTableData(exportTo, downloadElements, csvHeaders)
470   }
471
472   /* * * * Fills object with blueprint data to be used to fill table * * * */
473   fillTable(data) {
474     let fileName: string;
475     let tag: string;
476     let type: string;
477
478     for (let elem of data) {
479       fileName = elem.fileName;
480       if(fileName.includes('docker')){
481         type = 'docker'
482         if(fileName.includes('-docker')){
483           tag = fileName.split('-docker')[0]
484         } else if (fileName.includes('_docker')){
485           tag = fileName.split('_docker')[0]
486         }
487       } else if (fileName.includes('k8s')){
488         type = 'k8s'
489         if (fileName.includes('-k8s')) {
490           tag = fileName.split('-k8s')[0]
491         } else if (fileName.includes('_k8s')) {
492           tag = fileName.split('_k8s')[0]
493         }
494       }
495       
496       //create temporary bp element to push to array of blueprints
497       var tempBpElement: BlueprintElement = {
498         instanceId:      elem.msInstanceInfo.id,
499         instanceName:    elem.msInstanceInfo.name,
500         instanceRelease: elem.msInstanceInfo.release,
501         id:              elem.id,
502         version:         elem.version,
503         content:         elem.content,
504         status:          elem.status,
505         fileName:        fileName,
506         tag:             tag, 
507         type:            type,
508         metadata: {
509           failureReason: elem.metadata.failureReason,
510           notes:         elem.metadata.notes,
511           labels:        elem.metadata.labels,
512           createdBy:     elem.metadata.createdBy,
513           createdOn:     this.datePipe.transform(elem.metadata.createdOn, 'MM-dd-yyyy HH:mm'),
514           updatedBy:     elem.metadata.updatedBy,
515           updatedOn:     this.datePipe.transform(elem.metadata.updatedOn, 'MM-dd-yyyy HH:mm')
516         },
517         specification: {
518           id:            elem.specificationInfo.id
519         }
520       }
521
522       this.bpElements.push(tempBpElement)
523     }
524     this.bpElements.reverse();
525     this.filteredRows = this.bpElements;
526
527     this.resetFilters();
528
529     this.visible = "visible";
530     this.spinnerService.hide();
531   }
532
533   /* * * * Define content to show in bp view dialog pop up * * * */
534   BpContentToView: string;
535   viewBpContent(data){
536     this.BpFileNameForDownload = `${data['tag']}_${data['type']}_${data['instanceRelease']}_${data['version']}`
537     this.BpContentToView = data['content']
538     this.showBpContentDialog = true
539   }
540
541   /* * * * Download single blueprint * * * */
542   BpFileNameForDownload: string;
543   download() {
544     let file = new Blob([this.BpContentToView], { type: 'text;charset=utf-8' });
545     let name: string = this.BpFileNameForDownload + '.yaml'
546     saveAs(file, name)
547   }
548
549 /* * * * Download selected blueprints * * * */
550   downloadSelectedBps() {
551     let canDownloadBps: boolean = true;
552     
553     //checks if blueprints for multiple releases are selected
554     let selectedBpRelease: string = this.selectedBPs[0]['instanceRelease'];
555     for (let bp in this.selectedBPs) {
556       if (this.selectedBPs[bp]['instanceRelease'] !== selectedBpRelease) {
557         canDownloadBps = false
558         break
559       }
560     }
561
562     //downloads blueprints to zip file if all selected blueprints are for one release
563     if (canDownloadBps) {
564       var zip = new JSZip();
565       for (var i in this.selectedBPs) {
566         zip.file(`${this.selectedBPs[i]['tag']}_${this.selectedBPs[i]['type']}_${this.selectedBPs[i]['instanceRelease']}_${this.selectedBPs[i]['version']}.yaml`, this.selectedBPs[i]['content'])
567       }
568       zip.generateAsync({ type: "blob" }).then(function (content) {
569         saveAs(content, 'Blueprints.zip');
570       });
571     } else {
572       this.messageService.add({ key: 'multipleBpReleasesSelected', severity: 'error', summary: 'Error Message', detail: "Cannot download blueprints for different releases" });
573     }    
574
575     this.selectedBPs = []
576   }
577 }
578
579 export interface BlueprintElement{
580   instanceId: string
581   instanceName: string
582   instanceRelease: string
583   id: string
584   version: string
585   content: string
586   status: string
587   fileName: string
588   tag: string
589   type: string
590   metadata: {
591     failureReason: string
592     notes: string
593     labels: string[]
594     createdBy: string
595     createdOn: string
596     updatedBy: string
597     updatedOn: string
598   },
599   specification: {
600     id: string
601   }
602 }