Fix mod ui build issues
[dcaegen2/platform.git] / mod2 / ui / src / app / blueprints / blueprints.component.ts
diff --git a/mod2/ui/src/app/blueprints/blueprints.component.ts b/mod2/ui/src/app/blueprints/blueprints.component.ts
new file mode 100644 (file)
index 0000000..b4a7e73
--- /dev/null
@@ -0,0 +1,602 @@
+/* 
+ *  # ============LICENSE_START=======================================================
+ *  # Copyright (c) 2020 AT&T Intellectual Property. All rights reserved.
+ *  # ================================================================================
+ *  # Licensed under the Apache License, Version 2.0 (the "License");
+ *  # you may not use this file except in compliance with the License.
+ *  # You may obtain a copy of the License at
+ *  #
+ *  #      http://www.apache.org/licenses/LICENSE-2.0
+ *  #
+ *  # Unless required by applicable law or agreed to in writing, software
+ *  # distributed under the License is distributed on an "AS IS" BASIS,
+ *  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  # See the License for the specific language governing permissions and
+ *  # limitations under the License.
+ *  # ============LICENSE_END=========================================================
+ */
+
+import { Component, OnInit, ViewChild, ElementRef, Input, EventEmitter, Output, ChangeDetectorRef } from '@angular/core';
+import { Table } from 'primeng/table';
+import { MessageService } from 'primeng/api';
+import { trigger, state, style, transition, animate } from '@angular/animations';
+import * as saveAs from 'file-saver';
+import * as JSZip from 'jszip';
+import { AuthService } from '../services/auth.service';
+import { DatePipe } from '@angular/common';
+import { DeploymentArtifactService } from '../services/deployment-artifact.service';
+import { Ng4LoadingSpinnerService } from 'ng4-loading-spinner';
+import { Toast } from 'primeng/toast'
+import { ActivatedRoute } from '@angular/router';
+import { DownloadService } from '../services/download.service';
+
+@Component({
+  selector: 'app-blueprints',
+  templateUrl: './blueprints.component.html',
+  styleUrls: ['./blueprints.component.css'],
+  animations: [
+    trigger('rowExpansionTrigger', [
+      state('void', style({
+        transform: 'translateX(-10%)',
+        opacity: 0
+      })),
+      state('active', style({
+        transform: 'translateX(0)',
+        opacity: 1
+      })),
+      transition('* <=> *', animate('400ms cubic-bezier(0.86, 0, 0.07, 1)'))
+    ])
+  ],
+  providers: [DatePipe, MessageService]
+})
+export class BlueprintsComponent implements OnInit {
+  @ViewChild(Table, { static: false }) dt: Table;
+  @ViewChild(Toast, { static: false }) toast: Toast;
+
+  /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. **/
+  bpElements: BlueprintElement[] = [];
+  cols: any[] = [
+    { field: 'instanceName', header: 'Instance Name' },
+    { field: 'instanceRelease', header: 'Instance Release', width: '7%' },
+    { field: 'tag', header: 'Tag' },
+    { field: 'type', header: 'Type', width: '7%' },
+    { field: 'version', header: 'Version', width: '6%' },
+    { field: 'status', header: 'Status', width: '125px' }];
+  states: {field: string, label: string}[] = [];
+  columns: any[];
+  filteredRows: any;
+  downloadItems: { label: string; command: () => void; }[];
+  username: string;
+  showBpContentDialog: boolean = false;
+  selectedBPs: BlueprintElement[] = [];
+  //  Hides the BP list until the rows are retrieved and filtered
+  visible = "hidden";
+  // These 2 fields are passed from MS Instance to filter the BP list
+  tag: string;
+  release: string;
+
+  filteredName:    string;
+  filteredRelease: string;
+  filteredTag:     string;
+  filteredType:    string;
+  filteredVersion: string;
+  filteredStatus:  string;
+
+  constructor(private change: ChangeDetectorRef, private messageService: MessageService, private authService: AuthService,
+              private datePipe: DatePipe, private bpApis: DeploymentArtifactService, private spinnerService: Ng4LoadingSpinnerService,
+              private route: ActivatedRoute, private downloadService: DownloadService) { }
+
+  ngOnInit() {
+    
+    this.username = this.authService.getUser().username;
+    
+    this.getStates();
+    this.getAllBPs();
+
+    this.change.markForCheck();
+
+    this.route.queryParams.subscribe((params) => {
+      this.filteredTag     = params['tag'];
+      this.filteredRelease = params['release']});
+  }
+
+  //gets statuses for status updates
+  getStates(){
+    this.states = []
+    this.bpApis.getStatuses().subscribe((response) => {this.setMenuStates(response)})
+  }
+
+  //fills actions menu with states
+  setMenuStates(states){
+    for(let item of states){
+      this.states.push({
+        field: item,
+        label: 'To  ' + item
+      })
+    }
+  }
+
+  canDelete: boolean = false;
+  canDownload: boolean = false;
+  canUpdate: boolean = false;
+  deleteTooltip: string;
+  enableButtonCheck(){
+    if(this.selectedBPs.length > 0){
+      this.canDownload = true;
+      this.canUpdate = true;
+      
+      for(let item of this.selectedBPs){
+        if (item.status !== 'IN_DEV' && item.status !== 'NOT_NEEDED' && item.status !== 'DEV_COMPLETE'){
+          this.canDelete = false;
+          this.deleteTooltip = 'Only blueprints that are in a status of "In Dev", "Not Needed" or "Dev Complete" can be deleted'
+          break
+        } else {
+          this.canDelete = true;
+        }
+      }
+
+    } else {
+      this.canDownload = false;
+      this.canUpdate = false;
+      this.canDelete = false;
+      this.deleteTooltip = 'No Blueprints Selected'
+    }
+  }
+
+  updateStateTo: string = ''; //selected state to update blueprint to
+  //checks if there are different releases/statuses selected
+  updateSelectedStatusesCheck(state){
+    this.updateStateTo = state.field
+    let multipleStates: boolean = false
+    let multipleReleases: boolean = false
+    let firstStatus = this.selectedBPs[0]['status']
+    let firstRelease = this.selectedBPs[0]['instanceRelease']
+
+    for(let bp of this.selectedBPs){
+      if(bp.instanceRelease !== firstRelease){
+        multipleReleases = true
+      }
+      if (bp.status !== firstStatus) {
+        multipleStates = true
+      }
+    }
+
+    if(multipleReleases && multipleStates){
+      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.' });
+    } else if (multipleReleases && !multipleStates) {
+      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.' });
+    } else if (!multipleReleases && multipleStates) {
+      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.' });
+    } else if (!multipleReleases && !multipleStates){
+      this.updateSelectedStatuses()
+    }
+  }
+  onConfirm() {
+    this.messageService.clear('confirmToast')
+    this.updateSelectedStatuses()
+  }
+  onReject() {
+    this.messageService.clear('confirmToast')
+  }
+
+  /* * * * Update status for multiple blueprints * * * */
+  successfulStatusUpdates: number = 0 //keeps track of how many status updates were successful
+  selectionLength: number = 0 //length of array of blueprints with different statuses than update choice
+  statusUpdateCount: number = 0 //keeps track of how many api calls have been made throughout a loop
+  statusUpdateErrors: string[] = [] //keeps list of errors
+  updateSelectedStatuses(){
+    this.successfulStatusUpdates = 0      
+    this.statusUpdateErrors = []          
+    this.statusUpdateCount = 0
+
+    let bpsToUpdate = this.selectedBPs.filter(bp => bp.status !== this.updateStateTo) //array of blueprints with different statuses than update choice
+    this.selectionLength = bpsToUpdate.length;
+
+    if (this.selectionLength === 0) { this.selectedBPs = [] } else {
+      this.spinnerService.show();
+      this.updateState(this.updateStateTo, bpsToUpdate, true)
+    }
+  }
+
+  /* * * * Update Statuses * * * */
+  //state is the state to update to
+  //data is the bp data from selection
+  //multiple is whether updates were called for single blueprint or multiple selected blueprints
+  updateState(state, data, multiple){
+    //single status update
+    if(!multiple){
+      this.bpApis.patchBlueprintStatus(state.field, data['id']).subscribe(
+        (response: string) => {
+          data.status = state.field
+          this.messageService.add({ key: 'statusUpdate', severity: 'success', summary: 'Status Updated' });
+        }, errResponse => {
+          this.statusUpdatesResponseHandler(errResponse, false)
+        }
+      )
+    } 
+    
+    //multiple status updates
+    if(multiple){
+      (async () => {
+        for (let bp of data) {
+          this.bpApis.patchBlueprintStatus(this.updateStateTo, bp.id).subscribe(
+            (response: string) => {
+              bp.status = this.updateStateTo
+              this.statusUpdatesResponseHandler(null, true)
+            }, errResponse => {
+              this.statusUpdatesResponseHandler(errResponse, true)
+            }
+          )
+          await timeout(1500);
+        }
+      })();
+
+      function timeout(ms) {
+        return new Promise(resolve => setTimeout(resolve, ms));
+      }
+    }
+  }
+
+  /* * * * Handles errors and messages for status updates * * * */
+  statusUpdatesResponseHandler(response, multiple){
+    if(!multiple){
+      if(response !== null){
+        if (response.error.message.includes('Only 1 blueprint can be in the DEV_COMPLETE state.')) {
+          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')
+          this.messageService.add({ key: 'statusUpdate', severity: 'error', summary: 'Status Not Updated', detail: message, sticky: true });
+        } else {
+          this.messageService.add({ key: 'statusUpdate', severity: 'error', summary: 'Error Message', detail: response.error.message, sticky: true });
+        }
+      }
+    }
+
+    if(multiple){
+      this.statusUpdateCount++
+      if (response === null) {
+        this.successfulStatusUpdates++
+      } else {
+        if (response.error.message.includes('Only 1 blueprint can be in the DEV_COMPLETE state.')) {
+          let error = response.error.message.split('Only 1 blueprint can be in the DEV_COMPLETE state.')[0]
+          this.statusUpdateErrors.push(error)
+        } else { 
+          this.messageService.add({ key: 'statusUpdate', severity: 'error', summary: 'Error Message', detail: response.error.message, sticky: true });
+        }
+      }
+
+      if (this.statusUpdateCount === this.selectionLength) {
+        if (this.successfulStatusUpdates > 0) {
+          this.messageService.add({ key: 'statusUpdate', severity: 'success', summary: `(${this.successfulStatusUpdates} of ${this.selectionLength}) Statuses Updated`, life: 5000 });
+        }
+        if (this.statusUpdateErrors.length > 0) {
+          let message: string = ''
+          for (let elem of this.statusUpdateErrors) {
+            message += '- ' + elem + '\n'
+          }
+          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.'
+          this.messageService.add({ key: 'statusUpdate', severity: 'error', summary: 'Statuses Not Updated', detail: message, sticky: true });
+        }
+        this.spinnerService.hide()
+        this.selectedBPs = []
+      }
+    }
+  }
+
+  bpToDelete: any;
+  deleteSingle: boolean = false;
+  rowIndexToDelete;
+  rowIndexToDeleteFiltered;
+  warnDeleteBlueprint(data){
+    if(data !== null){
+      this.deleteSingle = true;
+      this.rowIndexToDeleteFiltered = this.filteredRows.map(function (x) { return x.id; }).indexOf(data['id']);
+      this.rowIndexToDelete = this.bpElements.map(function (x) { return x.id; }).indexOf(data['id']);
+      this.bpToDelete = data;
+      this.messageService.add({ key: 'confirmDeleteToast', sticky: true, severity: 'warn', summary: 'Are you sure?', detail: `- ${data.instanceName} (v${data.version}) for ${data.instanceRelease}` });
+    } else {
+      this.deleteSingle = false;
+      this.selectionLength = this.selectedBPs.length;
+      let warnMessage: string = ''
+      for(let item of this.selectedBPs){
+        warnMessage += `- ${item.instanceName} (v${item.version}) for ${item.instanceRelease}\n`
+      }
+      this.messageService.add({ key: 'confirmDeleteToast', sticky: true, severity: 'warn', summary: 'Are you sure?', detail: warnMessage });
+    }    
+  }
+
+  resetFilter = false;
+  onConfirmDelete() {
+    this.messageService.clear('confirmDeleteToast')
+
+    if (this.filteredName !== '' || this.filteredRelease !== '' || this.filteredTag !== '' || this.filteredType !== '' || this.filteredVersion !== '' || this.filteredStatus !== ''){
+      this.resetFilter = true;
+    } else {this.resetFilter = false}
+    
+    if(this.deleteSingle){
+      this.bpApis.deleteBlueprint(this.bpToDelete['id']).subscribe(response => {
+        this.checkBpWasSelected(this.bpToDelete['id'])
+        this.bpElements.splice(this.rowIndexToDelete, 1)
+        if (this.resetFilter) {
+          this.resetFilters()
+        }
+        this.messageService.add({ key: 'bpDeleteResponse', severity: 'success', summary: 'Success Message', detail: 'Deployment Artifact Deleted' });
+      }, error => {
+        this.messageService.add({ key: 'bpDeleteResponse', severity: 'error', summary: 'Error Message', detail: error.error.message });
+      })
+    } else {
+      for(let item of this.selectedBPs){
+        this.bpApis.deleteBlueprint(item.id).subscribe(response => {
+          this.deleteResponseHandler(true, item.id)
+        }, error => {
+          this.messageService.add({ key: 'bpDeleteResponse', severity: 'error', summary: 'Error Message', detail: error.error.message });
+        })
+      }
+    }
+  }
+  onRejectDelete() {
+    this.messageService.clear('confirmDeleteToast')
+  }
+
+  checkBpWasSelected(id){
+    if(this.selectedBPs.length > 0){
+      for(let item of this.selectedBPs){
+        if(item.id === id){
+          let indexToDelete = this.selectedBPs.map(function (x) { return x.id; }).indexOf(item['id']);
+          this.selectedBPs.splice(indexToDelete, 1)
+        }
+      }
+    }
+  }
+
+  bpsToDelete: string[] = [];
+  deleteBpCount = 0;
+  deleteResponseHandler(success, bpToDeleteId){
+    this.deleteBpCount++
+    if(success){
+      this.bpsToDelete.push(bpToDeleteId)
+    }
+    if(this.deleteBpCount === this.selectionLength){
+      for(let item of this.bpsToDelete){
+        
+        let indexToDelete = this.bpElements.map(function (x) { return x.id; }).indexOf(item);
+        this.bpElements.splice(indexToDelete, 1)
+      }
+
+      if(this.resetFilter){
+        this.resetFilters()
+      }
+
+      this.selectedBPs = [];
+      this.bpsToDelete = [];
+      this.deleteBpCount = 0;
+      this.messageService.add({ key: 'bpDeleteResponse', severity: 'success', summary: 'Success Message', detail: 'Deployment Artifacts Deleted' });
+    }
+  }
+
+  resetFilters(){
+    let filters: {field: string, value: string}[] = [];
+      filters.push({field: 'instanceName', value: this.filteredName})
+      filters.push({ field: 'instanceRelease', value: this.filteredRelease })
+      filters.push({ field: 'tag', value: this.filteredTag })
+      filters.push({ field: 'type', value: this.filteredType })
+      filters.push({ field: 'version', value: this.filteredVersion })
+      filters.push({ field: 'status', value: this.filteredStatus })
+    
+    for(let item of filters){
+      this.dt.filter(item.value, item.field, 'contains')
+    }
+  }
+
+  /* * * * Gets all blueprints * * * */
+  getAllBPs() {
+    this.spinnerService.show();
+    this.bpElements = [];
+    this.columns = this.cols.map(col => ({ title: col.header, dataKey: col.field }));
+
+    this.visible = "hidden";
+
+    this.bpApis.getAllBlueprints()
+      .subscribe((data: any[]) => {
+        this.fillTable(data)
+      })
+
+  }
+  
+  /* * * *  Checks when table is filtered and stores filtered data in new object to be downloaded when download button is clicked * * * */
+  onTableFiltered(values) {
+    if (values) {
+      this.filteredRows = values;
+    } else {
+      this.filteredRows = this.bpElements
+    }
+  }
+
+  /* * * * Download table as excel file * * * */
+  exportTable(exportTo) {
+    let downloadElements: any[] = []
+
+    for (let row of this.filteredRows) {
+      let labels;
+      let notes;
+      if (exportTo === "excel") {
+        if (row.metadata.labels !== undefined && row.metadata.labels !== null ) {
+          labels = row.metadata.labels.join(",")
+        }
+      } else {
+        labels = row.metadata.labels
+      }
+
+      if (row.metadata.notes !== null && row.metadata.notes !== undefined && row.metadata.notes !== '') {
+        notes = encodeURI(row.metadata.notes).replace(/%20/g, " ").replace(/%0A/g, "\\n")
+      }
+    
+      downloadElements.push({ 
+        Instance_Name: row.instanceName, 
+        Instance_Release: row.instanceRelease, 
+        Tag: row.tag, 
+        Type: row.type, 
+        Version: row.version, 
+        Status: row.status, 
+        Created_By: row.metadata.createdBy,
+        Created_On: row.metadata.createdOn,
+        Updated_By: row.metadata.updatedBy,
+        Updated_On: row.metadata.updatedOn,
+        Failure_Reason: row.metadata.failureReason,
+        Notes: notes,
+        Labels: labels
+      })
+    }
+    
+    let csvHeaders = []
+
+    if (exportTo === "csv") {
+      csvHeaders = [
+        "Instance_Name",
+        "Instance_Release",
+        "Tag",
+        "Type",
+        "Version",
+        "Status",
+        "Created_By",
+        "Created_On",
+        "Updated_By",
+        "Updated_On",
+        "Failure_Reason",
+        "Notes",
+        "Labels"];
+
+    }
+    
+    this.downloadService.exportTableData(exportTo, downloadElements, csvHeaders)
+  }
+
+  /* * * * Fills object with blueprint data to be used to fill table * * * */
+  fillTable(data) {
+    let fileName: string;
+    let tag: string;
+    let type: string;
+
+    for (let elem of data) {
+      fileName = elem.fileName;
+      if(fileName.includes('docker')){
+        type = 'docker'
+        if(fileName.includes('-docker')){
+          tag = fileName.split('-docker')[0]
+        } else if (fileName.includes('_docker')){
+          tag = fileName.split('_docker')[0]
+        }
+      } else if (fileName.includes('k8s')){
+        type = 'k8s'
+        if (fileName.includes('-k8s')) {
+          tag = fileName.split('-k8s')[0]
+        } else if (fileName.includes('_k8s')) {
+          tag = fileName.split('_k8s')[0]
+        }
+      }
+      
+      //create temporary bp element to push to array of blueprints
+      var tempBpElement: BlueprintElement = {
+        instanceId:      elem.msInstanceInfo.id,
+        instanceName:    elem.msInstanceInfo.name,
+        instanceRelease: elem.msInstanceInfo.release,
+        id:              elem.id,
+        version:         elem.version,
+        content:         elem.content,
+        status:          elem.status,
+        fileName:        fileName,
+        tag:             tag, 
+        type:            type,
+        metadata: {
+          failureReason: elem.metadata.failureReason,
+          notes:         elem.metadata.notes,
+          labels:        elem.metadata.labels,
+          createdBy:     elem.metadata.createdBy,
+          createdOn:     this.datePipe.transform(elem.metadata.createdOn, 'MM-dd-yyyy HH:mm'),
+          updatedBy:     elem.metadata.updatedBy,
+          updatedOn:     this.datePipe.transform(elem.metadata.updatedOn, 'MM-dd-yyyy HH:mm')
+        },
+        specification: {
+          id:            elem.specificationInfo.id
+        }
+      }
+
+      this.bpElements.push(tempBpElement)
+    }
+    this.bpElements.reverse();
+    this.filteredRows = this.bpElements;
+
+    this.resetFilters();
+
+    this.visible = "visible";
+    this.spinnerService.hide();
+  }
+
+  /* * * * Define content to show in bp view dialog pop up * * * */
+  BpContentToView: string;
+  viewBpContent(data){
+    this.BpFileNameForDownload = `${data['tag']}_${data['type']}_${data['instanceRelease']}_${data['version']}`
+    this.BpContentToView = data['content']
+    this.showBpContentDialog = true
+  }
+
+  /* * * * Download single blueprint * * * */
+  BpFileNameForDownload: string;
+  download() {
+    let file = new Blob([this.BpContentToView], { type: 'text;charset=utf-8' });
+    let name: string = this.BpFileNameForDownload + '.yaml'
+    saveAs(file, name)
+  }
+
+/* * * * Download selected blueprints * * * */
+  downloadSelectedBps() {
+    let canDownloadBps: boolean = true;
+    
+    //checks if blueprints for multiple releases are selected
+    let selectedBpRelease: string = this.selectedBPs[0]['instanceRelease'];
+    for (let bp in this.selectedBPs) {
+      if (this.selectedBPs[bp]['instanceRelease'] !== selectedBpRelease) {
+        canDownloadBps = false
+        break
+      }
+    }
+
+    //downloads blueprints to zip file if all selected blueprints are for one release
+    if (canDownloadBps) {
+      var zip = new JSZip();
+      for (var i in this.selectedBPs) {
+        zip.file(`${this.selectedBPs[i]['tag']}_${this.selectedBPs[i]['type']}_${this.selectedBPs[i]['instanceRelease']}_${this.selectedBPs[i]['version']}.yaml`, this.selectedBPs[i]['content'])
+      }
+      zip.generateAsync({ type: "blob" }).then(function (content) {
+        saveAs(content, 'Blueprints.zip');
+      });
+    } else {
+      this.messageService.add({ key: 'multipleBpReleasesSelected', severity: 'error', summary: 'Error Message', detail: "Cannot download blueprints for different releases" });
+    }    
+
+    this.selectedBPs = []
+  }
+}
+
+export interface BlueprintElement{
+  instanceId: string
+  instanceName: string
+  instanceRelease: string
+  id: string
+  version: string
+  content: string
+  status: string
+  fileName: string
+  tag: string
+  type: string
+  metadata: {
+    failureReason: string
+    notes: string
+    labels: string[]
+    createdBy: string
+    createdOn: string
+    updatedBy: string
+    updatedOn: string
+  },
+  specification: {
+    id: string
+  }
+}
\ No newline at end of file