summaryrefslogtreecommitdiffstats
path: root/mod2/ui/src/app/blueprints
diff options
context:
space:
mode:
Diffstat (limited to 'mod2/ui/src/app/blueprints')
-rw-r--r--mod2/ui/src/app/blueprints/blueprints.component.css142
-rw-r--r--mod2/ui/src/app/blueprints/blueprints.component.html283
-rw-r--r--mod2/ui/src/app/blueprints/blueprints.component.spec.ts153
-rw-r--r--mod2/ui/src/app/blueprints/blueprints.component.ts602
4 files changed, 1180 insertions, 0 deletions
diff --git a/mod2/ui/src/app/blueprints/blueprints.component.css b/mod2/ui/src/app/blueprints/blueprints.component.css
new file mode 100644
index 0000000..ccc03d9
--- /dev/null
+++ b/mod2/ui/src/app/blueprints/blueprints.component.css
@@ -0,0 +1,142 @@
+/*
+ * # ============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=========================================================
+ */
+
+td{
+ word-break:break-all;
+ font-size: 12px;
+}
+
+th{
+ font-size: 12px;
+}
+
+.table_column_filter{
+ width: 100%;
+ height: 20px;
+ font-size: 10px;
+}
+
+.table_div{
+ margin: 0px 50px 10px 20px;
+ min-width: 900px;
+ width: 98%;
+ border: 1px solid darkslategray;
+}
+
+.fa-refresh{
+ cursor: pointer;
+}
+
+textarea
+{
+ font-size: 12px;
+}
+
+.row-expand-layout{
+ display: grid;
+ width: 100%;
+ grid-template-columns: 20% 30% auto 25%;
+ grid-gap: 10px;
+ grid-auto-rows: minmax(100px, auto);
+}
+
+.row-expand-card{
+ font-size: 12px;
+ border-radius: 5px;
+ border: 1px solid slategray;
+ padding: 10px 5px 5px 10px;
+ /* This height prevents vertical scroll bars in Notes and Failure Reason */
+ height: 92px;
+ overflow: hidden;
+}
+
+.table_export_buttons_alignment{
+ margin-left: 5px;
+ margin-top: -32px;
+ float: left;
+}
+
+.table_export_button{
+ border-radius: 5px;
+ height: 22px;
+ font-size: 14px;
+ border: none;
+ margin-top: 4px;
+ margin-right: 7px;
+ display: inline-flex;
+}
+
+.table_caption_header{
+ margin-left: -18%;
+ width: 82%;
+ max-height: 25px;
+ display: inline-flex;
+}
+
+.table_global_filter{
+ width: 250px;
+ height:25px;
+ margin-bottom: -5px;
+ font-size: 12px;
+ margin-left: 15px;
+}
+
+.table_title{
+ width: 40%;
+ margin-left: 10%;
+}
+
+.table_action_item{
+ outline: none;
+ font-size: 12px;
+}
+
+::ng-deep .mat-menu-content {
+padding-top: 0px !important;
+padding-bottom: 0px !important;
+}
+.mat-menu-item{
+line-height:30px;
+height:30px;
+}
+
+.greenStatus{
+ background-color: rgba(80, 233, 105, 0.616)
+}
+
+.redStatus{
+ background-color: rgba(255, 29, 29, 0.527)
+}
+
+.blueStatus{
+ background-color: rgba(0, 183, 255, 0.384)
+}
+
+.greyStatus{
+ background-color: rgba(150, 150, 150, 0.432)
+}
+
+.ui-toast-detail{
+ white-space: pre-wrap;
+ font-size: 12px;
+}
+
+.ui-state-highlight {
+ background-color: #878C94 !important;
+ color: black !important;
+}
diff --git a/mod2/ui/src/app/blueprints/blueprints.component.html b/mod2/ui/src/app/blueprints/blueprints.component.html
new file mode 100644
index 0000000..53fed09
--- /dev/null
+++ b/mod2/ui/src/app/blueprints/blueprints.component.html
@@ -0,0 +1,283 @@
+<!--
+ # ============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=========================================================
+ -->
+
+<ng4-loading-spinner [timeout]="1000000"></ng4-loading-spinner>
+<div class="table_div" [style.visibility]="visible">
+ <!-- * * * * Table of Blueprints * * * * -->
+ <p-table #dt [columns]="cols" [(selection)]="selectedBPs" [value]="bpElements" sortMode="multiple" [paginator]="true"
+ [rows]="18" [rowsPerPageOptions]="[10,12,14,16,18,20,25,50]" (onFilter)="onTableFiltered(dt.filteredValue, $event)" dataKey="id" editMode="row">
+
+ <!-- * * * * Top caption row * * * * -->
+ <ng-template pTemplate="caption">
+
+ <div class="table_caption_header">
+ <!--Blueprints Table Header-->
+ <div>
+ <!-- * * * * Refresh * * * * -->
+ <i class="fa fa-refresh" (click)="getAllBPs()"></i>
+ <!-- * * * * Global filter * * * * -->
+ <input class="table_global_filter" type="text" pInputText size="50" placeholder="Global Filter"
+ (input)="dt.filterGlobal($event.target.value, 'contains')">
+ <i class="fa fa-search" style="margin:4px 0 0 8px"></i>
+ </div>
+
+ <h4 class="table_title"><b>Deployment Artifacts</b></h4>
+
+ </div>
+ </ng-template>
+
+ <!-- * * * * Header row with dynamic column names. Columns include microservice Name, Release, Tag, Type, Version and Status * * * * -->
+ <ng-template pTemplate="header" let-columns>
+ <tr style="text-align: center; vertical-align: bottom;">
+ <th style="width: 3em"></th>
+ <th class="ui-state-highlight" *ngFor="let col of columns" style="outline: none;" [pSortableColumn]="col.field" style="font-size: 12px; outline: none;"
+ [ngStyle]="{'width': col.width}">
+ {{col.header}}<br>
+ <p-sortIcon [field]="col.field"></p-sortIcon>
+ </th>
+ <th style="width: 7%; vertical-align: middle;">
+ Actions
+ </th>
+ </tr>
+
+ <!-- * * * * Second header row for individual column filters * * * * -->
+ <tr>
+ <th style="width: 3em"></th>
+ <!-- * * * * column filters * * * * -->
+ <th *ngFor="let col of columns" style="text-align: center;" [ngSwitch]="col.field">
+ <input *ngSwitchCase="'instanceName'" [(ngModel)]="filteredName" pInputText type="text" (input)="dt.filter($event.target.value, col.field, 'contains')" class="table_column_filter" placeholder="Filter">
+ <input *ngSwitchCase="'instanceRelease'" [(ngModel)]="filteredRelease" pInputText type="text" (input)="dt.filter($event.target.value, col.field, 'contains')" class="table_column_filter" placeholder="Filter">
+ <input *ngSwitchCase="'tag'" [(ngModel)]="filteredTag" pInputText type="text" (input)="dt.filter($event.target.value, col.field, 'contains')" class="table_column_filter" placeholder="Filter">
+ <input *ngSwitchCase="'type'" [(ngModel)]="filteredType" pInputText type="text" (input)="dt.filter($event.target.value, col.field, 'contains')" class="table_column_filter" placeholder="Filter">
+ <input *ngSwitchCase="'version'" [(ngModel)]="filteredVersion" pInputText type="text" (input)="dt.filter($event.target.value, col.field, 'contains')" class="table_column_filter" placeholder="Filter">
+ <input *ngSwitchCase="'status'" [(ngModel)]="filteredStatus" pInputText type="text" (input)="dt.filter($event.target.value, col.field, 'contains')" class="table_column_filter" placeholder="Filter">
+ </th>
+ <th>
+ <div style="text-align: center;">
+ <p-tableHeaderCheckbox style="padding-right: 5px;"></p-tableHeaderCheckbox>
+ <button pButton type="button" class="ui-button-secondary" (click)="enableButtonCheck()" [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger"
+ style="background-color: transparent; border: none; width: 20px; height: 20px; vertical-align: middle;">
+ <i class="pi pi-ellipsis-h" style="color: grey;"></i>
+ </button>
+ <mat-menu #menu="matMenu" xPosition="before">
+ <!--<div (mouseleave)="menuTrigger.closeMenu()">-->
+
+ <div style="background-color: rgba(128, 128, 128, 0.25);">
+ <span style="font-size: 12px; margin-left: 10px; font-weight: 500;"><i class="pi pi-download"></i> Download</span>
+ </div>
+
+ <!-- * * * * Download Blueprints * * * * -->
+ <div matTooltip="No Blueprints Selected" [matTooltipDisabled]="canDownload" matTooltipPosition="left">
+ <button mat-menu-item class="table_action_item" (click)="downloadSelectedBps()" [disabled]="!canDownload">Download Selected Blueprints</button>
+ </div>
+
+ <div style="background-color: rgba(128, 128, 128, 0.25);">
+ <span style="font-size: 12px; margin-left: 10px; font-weight: 500;"><i class="pi pi-times"></i> Delete</span>
+ </div>
+
+ <!-- * * * * Delete Selected Blueprints * * * -->
+ <div [matTooltip]="deleteTooltip" [matTooltipDisabled]="canDelete" matTooltipPosition="left">
+ <button mat-menu-item (click)="warnDeleteBlueprint(null)" class="table_action_item" [disabled]="!canDelete">Delete Selected Blueprints</button>
+ </div>
+
+ <div style="background-color: rgba(128, 128, 128, 0.25);">
+ <span style="font-size: 12px; margin-left: 10px; font-weight: 500;"><i class="pi pi-pencil"></i> Update</span>
+ </div>
+
+ <!-- * * * * State Changes * * * * -->
+ <div matTooltip="No Blueprints Selected" [matTooltipDisabled]="canUpdate" matTooltipPosition="left">
+ <button *ngFor="let state of states" mat-menu-item class="table_action_item" (click)="updateSelectedStatusesCheck(state)" [disabled]="!canUpdate">{{state.label}}</button>
+ </div>
+
+ <!--</div>-->
+ </mat-menu>
+ </div>
+ </th>
+ </tr>
+ </ng-template>
+
+ <!-- * * * * dynamic rows generated from columns object and msElems object * * * * -->
+ <ng-template pTemplate="body" let-rowData let-expanded="expanded" let-bpElem>
+ <tr>
+ <!-- * * * * Column for row expand buttons * * * * -->
+ <td>
+ <a href="#" [pRowToggler]="rowData">
+ <i [ngClass]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i>
+ </a>
+ </td>
+
+ <td *ngFor="let col of cols">
+ <div *ngIf="col.field==='status'" style="width: -moz-max-content; width: fit-content; padding: 0px 5px 0px 5px; border-radius: 3px; font-weight: 600;"
+ [ngClass]="{
+ 'greenStatus' : bpElem[col.field] === 'DEV_COMPLETE' || bpElem[col.field] === 'PST_CERTIFIED' || bpElem[col.field] === 'ETE_CERTIFIED' || bpElem[col.field] === 'IN_PROD',
+ 'redStatus' : bpElem[col.field] === 'PST_FAILED' || bpElem[col.field] === 'ETE_FAILED' || bpElem[col.field] === 'PROD_FAILED',
+ 'blueStatus' : bpElem[col.field] === 'IN_DEV' || bpElem[col.field] === 'IN_PST' || bpElem[col.field] === 'IN_ETE',
+ 'greyStatus' : bpElem[col.field] === 'NOT_NEEDED'}">
+ {{bpElem[col.field]}}
+ </div>
+ <div *ngIf="col.field!=='status'">{{bpElem[col.field]}}</div>
+ </td>
+
+ <!-- * * * * Actions Column * * * * -->
+ <td>
+ <div style="text-align: center;">
+ <p-tableCheckbox [value]="rowData" style="padding-right: 5px;"></p-tableCheckbox>
+ <!-- * * * * Actions Button * * * * -->
+ <button #actionButton pButton type="button" #menuTrigger="matMenuTrigger" class="ui-button-secondary" style="background-color: transparent; border: none; width: 20px; height: 20px; vertical-align: middle;" [matMenuTriggerFor]="menu">
+ <i class="pi pi-ellipsis-h" style="color: grey;"></i>
+ </button>
+ <!-- * * * * Actions Menu Items * * * * -->
+ <mat-menu #menu="matMenu">
+ <div style="background-color: rgba(128, 128, 128, 0.25);">
+ <span style="font-size: 12px; margin-left: 10px; font-weight: 500;"><i style="font-size: 12px;" class="pi pi-search"></i> View</span>
+ </div>
+
+ <button mat-menu-item class="table_action_item" (click)="viewBpContent(rowData)">View BP Content</button>
+
+ <div style="background-color: rgba(128, 128, 128, 0.25);">
+ <span style="font-size: 12px; margin-left: 10px; font-weight: 500;"><i class="pi pi-times"></i> Delete</span>
+ </div>
+
+ <div matTooltip='Only blueprints that are in a status of "In Dev", "Not Needed" or "Dev Complete" can be deleted' [matTooltipDisabled]="rowData.status === 'IN_DEV' || rowData.status === 'NOT_NEEDED' || rowData.status === 'DEV_COMPLETE'" matTooltipPosition="left">
+ <button mat-menu-item class="table_action_item" (click)="warnDeleteBlueprint(rowData)" [disabled]="rowData.status !== 'IN_DEV' && rowData.status !== 'NOT_NEEDED' && rowData.status !== 'DEV_COMPLETE'">Delete Blueprint</button>
+ </div>
+
+ <div style="background-color: rgba(128, 128, 128, 0.25);">
+ <span style="font-size: 12px; margin-left: 10px; font-weight: 500;"><i class="pi pi-pencil"></i> Update</span>
+ </div>
+
+ <div>
+ <div *ngFor="let state of states">
+ <button *ngIf="rowData.status !== state.field" mat-menu-item class="table_action_item" (click)="updateState(state, rowData, false)">{{state.label}}</button>
+ </div>
+ </div>
+ </mat-menu>
+ </div>
+ </td>
+ </tr>
+ </ng-template>
+
+
+ <!-- * * * * Row expand content * * * * -->
+ <ng-template pTemplate="rowexpansion" let-rowData let-columns="columns">
+ <tr>
+ <td [attr.colspan]="columns.length + 2">
+ <div class="row-expand-layout" [@rowExpansionTrigger]="'active'">
+ <!-- * * * * Audit Fields * * * * -->
+ <div class="row-expand-card" style="background-color: rgba(95, 158, 160, 0.295);">
+ <b>Created By:</b> {{rowData.metadata.createdBy}}<br>
+ <b>Created On:</b> {{rowData.metadata.createdOn}}<br>
+ <b>Updated By:</b> {{rowData.metadata.updatedBy}}<br>
+ <b>Updated On:</b> {{rowData.metadata.updatedOn}}<br>
+ </div>
+ <!-- * * * * Notes * * * * -->
+ <div class="row-expand-card" style="background-color: rgba(100, 148, 237, 0.295); white-space: pre-line;">
+ <b>Notes:</b><br>
+ <p-scrollPanel [style]="{width: '100%', height: '75px'}">
+ <div style="font-size: 12px; word-break: normal;">{{rowData.metadata.notes}}</div>
+ </p-scrollPanel>
+ </div>
+ <!-- * * * * Labels * * * * -->
+ <div class="row-expand-card" style="background-color: rgba(76, 65, 225, 0.295)">
+ <b style="padding-bottom: 5px;">Labels:</b><br>
+ <div *ngFor="let label of rowData['metadata']['labels']"
+ style="display: inline-flex; margin-top: 5px;">
+ <div style="padding: 2px 7px 3px 0px;">
+ <span style="background-color: rgba(80, 80, 80, 0.185); padding: 3px; border-radius: 3px;">{{label}}</span>
+ </div>
+ </div>
+ </div>
+ <!-- * * * * Failure Reason * * * * -->
+ <div class="row-expand-card" style="background-color: rgba(225, 65, 65, 0.295)">
+ <b>Failure Reason:</b><br>
+ <p-scrollPanel [style]="{width: '100%', height: '75px'}">
+ <div style="font-size: 12px; word-break: normal;">{{rowData.metadata.failureReason}}</div>
+ </p-scrollPanel>
+ </div>
+
+ </div>
+ </td>
+ </tr>
+ </ng-template>
+ </p-table>
+
+ <!-- * * * * download buttons for exporting table to either csv or excel file * * * * -->
+ <div class="table_export_buttons_alignment">
+ <button pButton type="button" (click)="exportTable('csv')" matTooltip="Export Table to CSV" matTooltipPosition="above" class="table_export_button" style="width: 55px;">
+ <i class="pi pi-file" style="margin-top: 3px; margin-left: 4px;"></i>
+ <label style="font-weight: 800; margin-top: 1px;">CSV</label>
+ </button>
+ <button pButton type="button" (click)="exportTable('excel')" matTooltip="Export Table to XLSX" class="table_export_button" matTooltipPosition="above" style="width: 65px; background-color: green;">
+ <i class="pi pi-file-excel" style="margin-top: 3px; margin-left: 4px;"></i>
+ <label style="font-weight: 800; margin-top: 1px">Excel</label>
+ </button>
+ </div>
+</div>
+
+<p-toast key="statusUpdate" class="ui-toast-detail" [style]="{width: '500px'}">
+ <ng-template let-message pTemplate="message">
+ <p><b>{{message.summary}}</b></p>
+ <p style="font-size: 12px;">{{message.detail}}</p>
+ </ng-template>
+</p-toast>
+<p-toast key="multipleBpReleasesSelected"></p-toast>
+
+<p-toast key="bpDeleteResponse"></p-toast>
+
+
+<!-- * * * * Confirm multiple statuses/releases update toast * * * * -->
+<p-toast position="center" key="confirmToast" (onClose)="onReject()" [baseZIndex]="5000" [style]="{width: '300px'}">
+ <ng-template let-message pTemplate="message">
+ <div style="text-align: center">
+ <i class="pi pi-exclamation-triangle" style="font-size: 3em"></i>
+ <h3>{{message.summary}}</h3>
+ <p>{{message.detail}}</p>
+ </div>
+ <div style="width: 100%; text-align: center;">
+ <button type="button" pButton (click)="onConfirm()" label="Confirm" class="ui-button-success"></button>
+ <button type="button" pButton (click)="onReject()" label="Cancel" class="ui-button-secondary" style="margin-left: 20px;"></button>
+ </div>
+ </ng-template>
+</p-toast>
+
+<!-- * * * * Confirm delete blueprint * * * * -->
+<p-toast position="center" key="confirmDeleteToast" class="ui-toast-detail" (onClose)="onReject()" [baseZIndex]="5000" [style]="{width: '300px'}">
+ <ng-template let-message pTemplate="message">
+ <div style="text-align: center">
+ <i class="pi pi-exclamation-triangle" style="font-size: 3em"></i>
+ <h3>{{message.summary}}</h3>
+ Confirm to delete blueprint(s):<br><br>
+ <p style="text-align: left; margin-left: 10%;">{{message.detail}}</p><br>
+ </div>
+ <div style="width: 100%; text-align: center;">
+ <button type="button" pButton (click)="onConfirmDelete()" label="Confirm" class="ui-button-success"></button>
+ <button type="button" pButton (click)="onRejectDelete()" label="Cancel" class="ui-button-secondary"
+ style="margin-left: 20px;"></button>
+ </div>
+ </ng-template>
+</p-toast>
+
+<!-- * * * * View BP Content Pop Up * * * * -->
+<p-dialog [(visible)]="showBpContentDialog" header="Blueprint Content" appendTo="body" [maximizable]="true" [modal]="true" [style]="{width: '80vw'}" [baseZIndex]="10000"
+ [closable]="false">
+ <pre>{{BpContentToView}}</pre>
+ <p-footer>
+ <button pButton label="Close" (click)="showBpContentDialog=false" type="button"></button>
+ <button pButton label="Download" (click)="download()" type="button"></button>
+ </p-footer>
+</p-dialog> \ No newline at end of file
diff --git a/mod2/ui/src/app/blueprints/blueprints.component.spec.ts b/mod2/ui/src/app/blueprints/blueprints.component.spec.ts
new file mode 100644
index 0000000..caa5c38
--- /dev/null
+++ b/mod2/ui/src/app/blueprints/blueprints.component.spec.ts
@@ -0,0 +1,153 @@
+/*
+ * # ============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 { HttpClientTestingModule } from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatMenuModule, MatTooltipModule } from '@angular/material';
+import { RouterTestingModule } from '@angular/router/testing';
+import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt';
+import { Ng4LoadingSpinnerModule } from 'ng4-loading-spinner';
+import { MessageService } from 'primeng/api';
+import { ButtonModule } from 'primeng/button';
+import { DialogModule } from 'primeng/dialog';
+import { DropdownModule } from 'primeng/dropdown';
+import { ScrollPanelModule } from 'primeng/scrollpanel';
+import { TableModule } from 'primeng/table';
+import { ToastModule } from 'primeng/toast';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { BlueprintsComponent } from './blueprints.component';
+
+describe('BlueprintsComponent', () => {
+ let component: BlueprintsComponent;
+ let fixture: ComponentFixture<BlueprintsComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [BlueprintsComponent],
+ imports: [
+ Ng4LoadingSpinnerModule,
+ TableModule,
+ MatMenuModule,
+ ScrollPanelModule,
+ ToastModule,
+ DialogModule,
+ DropdownModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ButtonModule,
+ HttpClientTestingModule,
+ ToastModule,
+ RouterTestingModule,
+ MatTooltipModule,
+ BrowserAnimationsModule
+ ],
+ providers: [
+ MessageService,
+ { provide: JWT_OPTIONS, useValue: JWT_OPTIONS },
+ JwtHelperService
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BlueprintsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it(`should set states`, async(() => {
+ const fixture = TestBed.createComponent(BlueprintsComponent);
+ const app = fixture.debugElement.componentInstance;
+ let mockStates = [
+ 'state1',
+ 'state2'
+ ]
+ app.setMenuStates(mockStates)
+ fixture.detectChanges();
+ expect(app.states).toEqual([ ]);
+ }));
+
+ it(`should not enable action buttons`, async(() => {
+ const fixture = TestBed.createComponent(BlueprintsComponent);
+ const app = fixture.debugElement.componentInstance;
+
+ app.selectedBPs = []
+ app.enableButtonCheck()
+ fixture.detectChanges();
+
+ expect(app.canDownload).toEqual(false);
+ expect(app.canUpdate).toEqual(false);
+ expect(app.canDelete).toEqual(false);
+ }));
+
+ it(`should enable download/update buttons but not delete`, async(() => {
+ const fixture = TestBed.createComponent(BlueprintsComponent);
+ const app = fixture.debugElement.componentInstance;
+
+ app.selectedBPs = [{status: 'TEST'}]
+ app.enableButtonCheck()
+ fixture.detectChanges();
+
+ expect(app.canDownload).toEqual(true);
+ expect(app.canUpdate).toEqual(true);
+ expect(app.canDelete).toEqual(false);
+ }));
+
+ it(`should enable download/update buttons but not delete`, async(() => {
+ const fixture = TestBed.createComponent(BlueprintsComponent);
+ const app = fixture.debugElement.componentInstance;
+
+ app.selectedBPs = [{ status: 'IN_DEV' }]
+ app.enableButtonCheck()
+ fixture.detectChanges();
+
+ expect(app.canDownload).toEqual(true);
+ expect(app.canUpdate).toEqual(true);
+ expect(app.canDelete).toEqual(true);
+ }));
+
+ it(`should enable download/update buttons but not delete`, async(() => {
+ const fixture = TestBed.createComponent(BlueprintsComponent);
+ const app = fixture.debugElement.componentInstance;
+
+ let mockBpToView = {
+ tag: 'test-tag',
+ type: 'k8s',
+ instanceRelease: '2008',
+ version: '1',
+ content: 'test'
+ }
+
+ app.viewBpContent(mockBpToView)
+ fixture.detectChanges();
+
+ expect(app.BpFileNameForDownload).toEqual('test-tag_k8s_2008_1');
+ expect(app.BpContentToView).toEqual('test');
+ expect(app.showBpContentDialog).toEqual(true);
+ }));
+
+});
+
+
diff --git a/mod2/ui/src/app/blueprints/blueprints.component.ts b/mod2/ui/src/app/blueprints/blueprints.component.ts
new file mode 100644
index 0000000..b4a7e73
--- /dev/null
+++ b/mod2/ui/src/app/blueprints/blueprints.component.ts
@@ -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