diff options
Diffstat (limited to 'mod2/ui/src/app')
97 files changed, 9181 insertions, 0 deletions
diff --git a/mod2/ui/src/app/app-routing.module.ts b/mod2/ui/src/app/app-routing.module.ts new file mode 100644 index 0000000..0ffe503 --- /dev/null +++ b/mod2/ui/src/app/app-routing.module.ts @@ -0,0 +1,55 @@ +/* + * # ============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 { NgModule } from '@angular/core'; +import { Routes, RouterModule, NavigationStart, NavigationEnd, Route} from '@angular/router'; +import { HomeComponent } from './home/home.component'; +import { OnboardingToolsComponent } from './onboarding-tools/onboarding-tools.component'; +import { CompSpecsComponent } from './comp-specs/comp-specs.component'; +import { MsInstancesComponent } from './msInstances/msInstances.component'; +import { LoginComponent } from './login/login.component'; +import { RegisterComponent } from './register/register.component'; +import { ResetPasswordComponent } from './reset-password/reset-password.component'; +import { AuthGuard } from './guards/auth.guard'; +import { RoleGuard } from './guards/role.guard'; +import { UserManagementComponent } from './user-management/user-management.component'; +import { LoginGuard } from './guards/login.guard'; +import { MicroservicesComponent } from './microservices/microservices.component'; +import { BlueprintsComponent } from './blueprints/blueprints.component'; +import { CompSpecValidationComponent } from './comp-spec-validation/comp-spec-validation.component'; + +const routes: Routes = [ + { path: '', component: HomeComponent, canActivate:[LoginGuard]}, + { path: 'home', component: HomeComponent, canActivate:[AuthGuard] }, + { path: 'login', component: LoginComponent, canActivate:[LoginGuard]}, + { path: 'register', component: RegisterComponent, canActivate:[AuthGuard, RoleGuard] }, + { path: 'reset-password', component: ResetPasswordComponent, canActivate:[AuthGuard]}, + { path: 'users', component: UserManagementComponent, canActivate:[AuthGuard, RoleGuard]}, + { path: 'OnboardingTools', component: OnboardingToolsComponent, canActivate:[AuthGuard] }, + { path: 'CompSpecs', component: CompSpecsComponent, canActivate:[AuthGuard]}, + { path: 'ms-instances', component: MsInstancesComponent, canActivate:[AuthGuard] }, + { path: 'base-microservices', component: MicroservicesComponent, canActivate: [AuthGuard] }, + { path: 'blueprints', component: BlueprintsComponent, canActivate: [AuthGuard] }, + { path: 'spec-validator', component: CompSpecValidationComponent, canActivate: [AuthGuard] } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git a/mod2/ui/src/app/app.component.css b/mod2/ui/src/app/app.component.css new file mode 100644 index 0000000..0b9779a --- /dev/null +++ b/mod2/ui/src/app/app.component.css @@ -0,0 +1,173 @@ +/* + * # ============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========================================================= + */ + +.sidenav-container { + height: 100%; +} + +.sidenav { + width: 190px; + background: linear-gradient(to bottom, #1D2329 0%, #454B52 8%) +} + +.sidenav .mat-toolbar { + background: inherit; + color:rgb(225, 241, 247); +} + +.subMenuItem{ + color: white; + font-size: 14px; + font-weight: 400; + height: 6vh !important; + min-height: 35px !important; +} + +.mat-toolbar.mat-primary { + display: block; + position: sticky; + top: 0; + z-index: 1; + height: 33px; + position: fixed; + border-radius: 0%; + background: linear-gradient(to bottom, #1D2329 0%, #454B52 135%) +} + +.nav { + font-size: 16px; + font-weight: 500; + background-color: #454B52; + color:white; + height: 7vh !important; + min-height: 40px !important; +} + +*:focus { + outline: none !important; + border: 0 !important; +} + +.is-active{ + background-color: #EA712F !important; +} + +label { + cursor: pointer; +} + +.input{ + padding-top: 20px; +} + +.inputLabel { + font-weight: 600; + margin-left: 25px; + width: 160px; +} + +.inputFieldSm { + width: 200px; + height: 35px; + padding-left: 8px; +} +.inputFieldMed { + width: 300px; + height: 35px; + padding-left: 6px; +} +.inputFieldLg { + width: 400px; + height: 35px; + padding-left: 6px; +} + +/* B R E A D C R U M B S . . . . . . . . */ +.breadcrumb { + list-style: none; + overflow: hidden; + font: 12px Helvetica, Arial, Sans-Serif; + background: transparent; + margin-left: 5px; + margin-top: -3px; + padding: 0; +} +.breadcrumb li { + float: left; +} +.breadcrumb li a { + color: #1D2329; + font-weight: 700; + text-decoration: none; + padding: 6px 0 6px 45px; + background: brown; /* fallback color */ + background: hsla(21,82%,55%,1); + position: relative; + display: block; + float: left; +} +.breadcrumb li a:after { + content: " "; + display: block; + width: 0; + height: 0; + border-top: 50px solid transparent; /* Go big on the size, and let overflow hide */ + border-bottom: 50px solid transparent; + border-left: 30px solid hsla(21,82%,55%,1); + position: absolute; + top: 50%; + margin-top: -50px; + left: 100%; + z-index: 2; +} +.breadcrumb li a:before { + content: " "; + display: block; + width: 0; + height: 0; + border-top: 50px solid transparent; /* Go big on the size, and let overflow hide */ + border-bottom: 50px solid transparent; + border-left: 30px solid #b9bec4; + position: absolute; + top: 50%; + margin-top: -50px; + margin-left: 3px; + left: 100%; + z-index: 1; +} +.breadcrumb li:first-child a { + padding-left: 14px; +} +.breadcrumb li:nth-child(2) a { background: hsla(30,100%,50%,.95); } +.breadcrumb li:nth-child(2) a:after { border-left-color: hsla(30,100%,50%,.95); } +.breadcrumb li:nth-child(3) a { background: hsla(38,100%,50%,.90); } +.breadcrumb li:nth-child(3) a:after { border-left-color: hsla(38,100%,50%,.90); } +.breadcrumb li:nth-child(4) a { background: hsla(45,100%,50%,.85); } +.breadcrumb li:nth-child(4) a:after { border-left-color: hsla(45,100%,50%,.85); } +.breadcrumb li:nth-child(5) a { background: hsla(52,100%,50%,.80); } +.breadcrumb li:nth-child(5) a:after { border-left-color: hsla(52,100%,50%,.80); } +.breadcrumb li:last-child a { + background: transparent !important; + color: black; + pointer-events: none; + cursor: default; + margin-left: -10px; +} +.breadcrumb li:last-child a:after { border: 0; } +.breadcrumb li a:hover { background: rgba(102, 102, 102, 0.993); color: #e9fffd; cursor: pointer} +.breadcrumb li a:hover:after { border-left-color: hsla(0,0%,40%,1); color: #bbc9d8 !important; cursor: pointer} diff --git a/mod2/ui/src/app/app.component.html b/mod2/ui/src/app/app.component.html new file mode 100644 index 0000000..e214dc1 --- /dev/null +++ b/mod2/ui/src/app/app.component.html @@ -0,0 +1,136 @@ +<!-- + # ============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></ng4-loading-spinner> +<mat-sidenav-container style="background: linear-gradient(to top, #878C94 0%, #DCDFE3 10%, #F2F2F2 40%, #DCDFE3 80%, #878C94 110%);" class="sidenav-container" *ngIf="authService.authPass"> + <mat-sidenav [class.mat-elevation-z2]="!isActive" [class.mat-elevation-z8]="isActive" #sideMenu class="sidenav" + fixedInViewport="true" [attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'" + [mode]="(isHandset$ | async) ? 'over' : 'side'" [opened]="(isHandset$ | async) === true"> + <mat-toolbar></mat-toolbar> + <hr width=90%> + <mat-action-list style="margin-top: -10px;"> + <button class="nav" mat-list-item (click)="setCrumbs('', 'reset')" [routerLink]="'/home'" [routerLinkActive]="'is-active'">Home</button> + <button class="nav" mat-list-item (click)="setCrumbs('User Management', 'reset')" *ngIf="authService.isAdmin" [routerLink]="'/users'" [routerLinkActive]="'is-active'">User Management</button> + <button class="nav" mat-list-item (click)="setCrumbs('Onboarding Tools', 'reset')" [routerLink]="'/OnboardingTools'" [skipLocationChange]="false" [routerLinkActive]="'is-active'">Onboarding Tools</button> + <button class="nav" mat-list-item (click)="msMenu()"> + <i *ngIf="msMenuIconRight" class="pi pi-chevron-right"></i> + <i *ngIf="!msMenuIconRight" class="pi pi-chevron-down"></i> + Microservices</button> + <div *ngIf="showMsMenu" style="margin-left: 20px;"> + <button class="subMenuItem" mat-list-item (click)="setCrumbs('Microservices', 'reset')" [routerLink]="'/base-microservices'" [routerLinkActive]="'is-active'">Microservices</button> + <button class="subMenuItem" mat-list-item (click)="setCrumbs('MS Instances', 'reset')" [routerLink]="'/ms-instances'" [routerLinkActive]="'is-active'">MS Instances</button> + <button class="subMenuItem" mat-list-item (click)="setCrumbs('Blueprints', 'reset')" [routerLink]="'/blueprints'" [routerLinkActive]="'is-active'">Blueprints</button> + + <button class="nav" mat-list-item (click)="utilitiesMenu()"> + <i *ngIf="utilitiesMenuIconRight" class="pi pi-chevron-right"></i> + <i *ngIf="!utilitiesMenuIconRight" class="pi pi-chevron-down"></i> + Utilites</button> + + <div *ngIf="showUtilitiesMenu" style="margin-left: 20px;"> + <button class="subMenuItem" mat-list-item (click)="setCrumbs('Spec Validator', 'reset')" + [routerLink]="'/spec-validator'" [routerLinkActive]="'is-active'">Spec Validator</button> + <button class="subMenuItem" mat-list-item (click)="redirectToAPIs()">MOD APIs</button> + </div> + </div> + <button class="nav" mat-list-item (click)='redirectTo("https://wiki.web.att.com/pages/viewpage.action?spaceKey=DPD&title=DCAE+MOD+User+Guide")'>User Guide</button> + + </mat-action-list> + </mat-sidenav> + <mat-sidenav-content style="margin-top: 70px; max-height: 90%"> + <!-- Top bar when Logged in --> + <mat-toolbar [class.mat-elevation-z2]="!isActive" [class.mat-elevation-z8]="isActive" color="primary" + fixedInViewport="true" style="background-color: #1D2329; color:#F2F2F2"> + <button type="button" aria-label="Toggle sidenav" mat-icon-button (click)="sideMenu.toggle()" style="color:#F2F2F2"> + <i *ngIf="!sideMenu.opened" class="pi pi-angle-double-right" style="font-size: 25px; margin-left: -30px; margin-bottom: 8px;"></i> + <i *ngIf="sideMenu.opened" class="pi pi-angle-double-left" style="font-size: 25px; margin-left: -30px; margin-bottom: 8px;"></i> + </button> + <span style="font-size: 22px; font-weight: 500; margin-left: 35px">MOD</span> + <button type="button" *ngIf="authService.authPass" (click)="handleLogout()" + style="float:right; margin-right: 18%; height: 32px; border: none; color:#F2F2F2; background: linear-gradient(to bottom, #1D2329 0%, #454B52 135%); + font-size: 16px; font-weight: 400"> + <i class="pi pi-sign-out" style="vertical-align:text-top"></i> + Logout + </button> + <button type="button" *ngIf="authService.authPass" (click)="handleProfile()" + style="float:right; margin-right: 4%; height: 32px; border: none; color:#F2F2F2; background: linear-gradient(to bottom, #1D2329 0%, #454B52 135%); + font-size: 16px; font-weight: 400"> + <i class="pi pi-user-edit" style="vertical-align:text-top"></i> + {{authService.getUser().username}} + </button> + <!-- BREADCRUMBS . . . . . --> + <ul class="breadcrumb"> + <li *ngFor="let crumb of breadcrumbs" [routerLink]="crumb.link" (click)="setCrumbs(crumb.page, 'crumbClicked')"><a>{{crumb.page}}</a></li> + </ul> + </mat-toolbar> + <main [@routeAnimations]="prepareRoute(outlet)" class="content"> + <router-outlet #outlet="outlet"></router-outlet> + </main> + </mat-sidenav-content> +</mat-sidenav-container> + +<!-- Top bar when Logging in --> +<mat-toolbar *ngIf="!authService.authPass" [class.mat-elevation-z2]="!isActive" [class.mat-elevation-z8]="isActive" color="primary" + fixedInViewport="true" style="background-color: #1D2329"> + <span style="font-size: 22px; font-weight: 500">MOD</span> + <app-login></app-login> +</mat-toolbar> + +<!-- Login Expired dialog --> +<p-dialog header="Login Expired" [(visible)]="authService.reLoginMsg" [modal]="true" [style]="{width:'25vw'}" [baseZIndex]="10000" + [draggable]="false" [resizable]="false"> + <p>Your login has expired. Please log in again.</p> + <p-footer> + <button type="button" pButton icon="pi pi-check" (click)="onConfirm()" label="OK"></button> + </p-footer> +</p-dialog> + +<!--reset User Password dialog--> +<p-dialog [(visible)]="resetPasswordFlag" appendTo="body" [modal]="true" [transitionOptions]="'300ms'" [style]="{width: '630px'}" [baseZIndex]="10000" +[closable]="true" (onHide)="closeResetDialog()"> +<p-header style="display: inline-flex;"> + Reset User Password +</p-header> +<form [formGroup]="resetPasswordForm"> + <!-- * * * New Password * * * --> + <div class="input"> + <div class="ui-inputgroup" > + <label class="inputLabel">New Password</label> + <span class="ui-inputgroup-addon" (click)="hidePassword=!hidePassword"> + <i [ngClass]="hidePassword? 'pi pi-eye-slash':'pi pi-eye'"></i></span> + <input class="inputFieldSm" [type]="hidePassword? 'password':'text'" pInputText formControlName="password" /> + </div> + </div> + <i *ngIf="resetPasswordForm.get('password').errors && resetPasswordForm.get('password').errors.minlength" style="width: 140px;margin-left: 20px;font-size: small;color: red;">password should be more than 5 characters</i> + <!-- * * * Confirm New Password * * * --> + <div class="input"> + <div class="ui-inputgroup" > + <label class="inputLabel">Confirm Password</label> + <span class="ui-inputgroup-addon" (click)="hideConfirmPassword=!hideConfirmPassword"> + <i [ngClass]="hideConfirmPassword? 'pi pi-eye-slash':'pi pi-eye'"></i></span> + <input class="inputFieldSm" [type]="hideConfirmPassword? 'password':'text'" pInputText formControlName="confirm_password" /> + </div> + </div> + <i *ngIf="resetPasswordForm.errors && resetPasswordForm.errors.errMsg" style="width: 140px;margin-left: 20px;font-size: small;color: red;">{{resetPasswordForm.errors.errMsg}}</i> + <!-- * * * Submit and Cancel buttons * * * --> + <div style="margin-top: 2em; margin-left: 1.3em; margin-bottom: 2em;"> + <button pButton type="button" (click)="closeResetDialog()" label="Cancel"></button> + <button pButton type="submit" (click)="resetPasswordForm.valid && submitReset()" class="ui-button-success" label="Submit" style="width: 70px"></button> + </div> +</form> + +</p-dialog>
\ No newline at end of file diff --git a/mod2/ui/src/app/app.component.spec.ts b/mod2/ui/src/app/app.component.spec.ts new file mode 100644 index 0000000..d6eb43c --- /dev/null +++ b/mod2/ui/src/app/app.component.spec.ts @@ -0,0 +1,90 @@ +/* + * # ============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 { TestBed, async } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; +import { Ng4LoadingSpinnerModule } from 'ng4-loading-spinner'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatCardModule } from '@angular/material/card'; +import { MatTreeModule } from '@angular/material/tree'; +import { DialogModule } from 'primeng/dialog'; +import { MatListModule } from '@angular/material/list'; +import { LoginComponent } from './login/login.component'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { CardModule } from 'primeng/card'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { JwtHelperService, JwtModule, JWT_OPTIONS } from '@auth0/angular-jwt'; + +describe('AppComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + Ng4LoadingSpinnerModule, + MatSidenavModule, + MatToolbarModule, + MatCardModule, + MatTreeModule, + DialogModule, + MatListModule, + FormsModule, + ReactiveFormsModule, + CardModule, + HttpClientTestingModule, + JwtModule, + ], + declarations: [ + AppComponent, + LoginComponent + ], providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }).compileComponents(); + })); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'mod-fe'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('mod-fe'); + }); + + it(`should have menu item variables as 'false'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.showUtilitiesMenu).toEqual(false); + expect(app.utilitiesMenuIconRight).toEqual(true); + }); + + it(`should change utilites menu arrow`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + + app.utilitiesMenu() + expect(app.showUtilitiesMenu).toEqual(true); + expect(app.utilitiesMenuIconRight).toEqual(false); + }); +});
\ No newline at end of file diff --git a/mod2/ui/src/app/app.component.ts b/mod2/ui/src/app/app.component.ts new file mode 100644 index 0000000..87d29b4 --- /dev/null +++ b/mod2/ui/src/app/app.component.ts @@ -0,0 +1,218 @@ +/* + * # ============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, HostBinding, OnInit } from '@angular/core'; +import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; +import { Observable } from 'rxjs'; +import { map, shareReplay } from 'rxjs/operators'; +import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; +import { FlatTreeControl } from '@angular/cdk/tree'; +import { Event, Router, RouterOutlet, NavigationStart, NavigationEnd, RouterEvent } from '@angular/router'; +import { AuthService } from './services/auth.service'; +import { BreadcrumbService } from './services/breadcrumb.service'; +import { environment } from '../environments/environment'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { UserService } from './services/user.service'; +import { User } from './models/User'; +import { Authority } from './models/Authority.enum'; + +interface TreeNode { + name: string; + children?: TreeNode[]; +} + +const TREE_DATA: TreeNode[] = [ + { + name: 'Microservices', + children: [ + { name: 'Microservices' }, + { name: 'MS Instances' }, + { name: 'Blueprints' }, + { name: 'MOD APIs' } + ] + } +]; + +interface ExampleFlatNode { + expandable: boolean; + name: string; + level: number; +} + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent implements OnInit{ + + @HostBinding('@.disabled') + public animationsDisabled = true; + activeNode: any; + resetPasswordFlag: boolean = false; + resetPasswordForm: FormGroup; + hidePassword: boolean = true; + hideConfirmPassword: boolean = true; + breadcrumbs: any[] = []; + + prepareRoute(outlet: RouterOutlet) { + return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation']; + } + + title = 'mod-fe'; + right: boolean = true; + down: boolean = false; + + menu_tree(){ + this.right = !this.right; + this.down = !this.down; + } + + isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset) + .pipe( + map(result => result.matches), + shareReplay() + ); + + redirectTo(link){ + window.open(link, "_blank"); + } + + redirectToAPIs() { + window.open(`http://${environment.api_baseURL}:31001/swagger-ui.html#/`, "_blank"); + } + + constructor(private breakpointObserver: BreakpointObserver, private router: Router, public authService: AuthService, + private bread: BreadcrumbService, private fb: FormBuilder, private userService: UserService) { + this.dataSource.data = TREE_DATA; + } + + ngOnInit() { + this.resetPasswordForm = this.fb.group( + { + password: ['', [Validators.minLength(6)]], + confirm_password: '' + }, + {validators: [this.passwordValidator]} + ); + + // Subscribe to breadcrumb changes + this.bread.breadcrumbs$.subscribe( (crumbArray) => {this.breadcrumbs = crumbArray}); + } + + setCrumbs(component, action) { + this.bread.setBreadcrumbs(component, action) + } + + passwordValidator(group: FormGroup) { + if(group.value.password === group.value.confirm_password){ + return null; + } else { + return {errMsg: 'Passwords do not match!'}; + } + } + + private _transformer = (node: TreeNode, level: number) => { + return { + expandable: !!node.children && node.children.length > 0, + name: node.name, + level: level, + }; + } + + tree_handler(name, treenode) { + if (name == "MOD APIs") { + window.open(`http://${environment.api_baseURL}:31001/swagger-ui.html#/`, "_blank"); + } else if (name == "MS Instances") { + this.router.navigate(["ms-instances"]); + this.activeNode = treenode; + } else if(name == "Microservices") { + this.router.navigate(["base-microservices"]); + this.activeNode = treenode; + } else if(name == "Blueprints") { + this.router.navigate(["blueprints"]); + this.activeNode = treenode; + } + + } + + treeControl = new FlatTreeControl<ExampleFlatNode>( + node => node.level, node => node.expandable); + + treeFlattener = new MatTreeFlattener( + this._transformer, node => node.level, node => node.expandable, node => node.children); + + dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + + hasChild = (_: number, node: ExampleFlatNode) => node.expandable; + + showMsMenu = false; + msMenuIconRight = true; + msMenu() { + this.showMsMenu = !this.showMsMenu + this.msMenuIconRight = !this.msMenuIconRight + } + + showUtilitiesMenu = false; + utilitiesMenuIconRight = true; + utilitiesMenu(){ + this.showUtilitiesMenu = !this.showUtilitiesMenu + this.utilitiesMenuIconRight = !this.utilitiesMenuIconRight + } + + onConfirm() { + this.authService.reLoginMsg = false; + } + + handleLogout() { + this.showMsMenu = false + this.authService.logout() + } + + handleProfile() { + console.log(this.authService.getUser().roles); + this.resetPasswordFlag = true; + } + + closeResetDialog() { + this.resetPasswordForm.reset(); + this.resetPasswordFlag = false; + } + + submitReset() { + if(this.authService.getUser().roles.includes(Authority.ADMIN)){ + this.userService.editUser(this.authService.getUser().username, this.resetPasswordForm.value as User).subscribe(res=>{ + alert("Password reset successful. Please login in using the new credentials."); + this.authService.logout(); + }, (err)=>{ + alert(err.error.message); + + }); + } else { + this.userService.editProfile(this.authService.getUser().username, this.resetPasswordForm.value as User).subscribe(res=>{ + alert("Password reset successful. Please login in using the new credentials."); + this.authService.logout(); + }, (err)=>{ + alert(err.error.message); + }); + } + this.resetPasswordForm.reset(); + this.resetPasswordFlag = false; + } + +} diff --git a/mod2/ui/src/app/app.module.ts b/mod2/ui/src/app/app.module.ts new file mode 100644 index 0000000..1e1da59 --- /dev/null +++ b/mod2/ui/src/app/app.module.ts @@ -0,0 +1,172 @@ +/* + * # ============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 { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; + +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { PathLocationStrategy, LocationStrategy} from '@angular/common'; + +import { SharedModule } from './shared/shared-module'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { HomeComponent } from './home/home.component'; +import { LayoutModule } from '@angular/cdk/layout'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatButtonModule } from '@angular/material/button'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatIconModule } from '@angular/material/icon'; +import { MatListModule } from '@angular/material/list'; +import { MatTableModule } from '@angular/material/table'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatSortModule } from '@angular/material/sort'; +import { MatTreeModule } from '@angular/material/tree'; +import { MatCardModule } from '@angular/material/card'; +import { MaterialElevationDirective } from './material-elevation.directive'; +import { MatProgressSpinnerModule, MatTooltipModule, MatMenuModule } from '@angular/material'; +import { OnboardingToolsComponent, SafePipe } from './onboarding-tools/onboarding-tools.component'; +import { CompSpecsComponent } from './comp-specs/comp-specs.component'; +import { MatTableExporterModule } from 'mat-table-exporter'; +import { TableModule } from 'primeng/table'; +import { MsInstancesComponent } from './msInstances/msInstances.component'; +import { ButtonModule } from 'primeng/button'; +import { SidebarModule } from 'primeng/sidebar'; +import { MenuModule } from 'primeng/menu'; +import { ToolbarModule } from 'primeng/toolbar'; +import { PanelMenuModule } from 'primeng/panelmenu'; +import { CardModule } from 'primeng/card'; +import { LoginComponent } from './login/login.component'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RegisterComponent } from './register/register.component'; +import { ResetPasswordComponent } from './reset-password/reset-password.component'; +import { AuthGuard } from './guards/auth.guard'; +import { RoleGuard } from './guards/role.guard'; +import { JwtInterceptorService } from './services/jwt-interceptor.service'; +import { JwtModule } from '@auth0/angular-jwt'; +import { UserManagementComponent } from './user-management/user-management.component'; +import { DialogModule } from 'primeng/dialog'; +import { ToastModule } from 'primeng/toast'; +import { Ng4LoadingSpinnerModule } from 'ng4-loading-spinner'; +import { PasswordModule } from 'primeng/password'; +import { TooltipModule } from 'primeng/tooltip'; +import { AccordionModule } from 'primeng/accordion'; +import { SplitButtonModule } from 'primeng/splitbutton'; +import { DropdownModule } from 'primeng/dropdown'; +import { FileUploadModule } from 'primeng/fileupload'; +import { InputTextareaModule } from 'primeng/inputtextarea'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { RadioButtonModule } from 'primeng/radiobutton'; +import { SelectButtonModule } from 'primeng/selectbutton'; +import { MessageService } from 'primeng/api'; +import { MsAddChangeComponent } from './ms-add-change/ms-add-change.component'; +import { MicroservicesComponent } from './microservices/microservices.component'; +import { PaginatorModule } from 'primeng/paginator'; +import { ScrollPanelModule } from 'primeng/scrollpanel'; +import { CalendarModule } from 'primeng/calendar'; +import { BlueprintsComponent } from './blueprints/blueprints.component'; +import { CompSpecAddComponent } from './comp-spec-add/comp-spec-add.component'; +import { MsInstanceAddComponent } from './ms-instance-add/ms-instance-add.component'; +import {MultiSelectModule} from 'primeng/multiselect'; +import {CheckboxModule} from 'primeng/checkbox'; +import { InputSwitchModule } from 'primeng/inputswitch'; +import { CompSpecValidationComponent } from './comp-spec-validation/comp-spec-validation.component'; + +@NgModule({ + declarations: [ + AppComponent, + HomeComponent, + MaterialElevationDirective, + SafePipe, + OnboardingToolsComponent, + CompSpecsComponent, + MsInstancesComponent, + LoginComponent, + RegisterComponent, + ResetPasswordComponent, + UserManagementComponent, + MsAddChangeComponent, + MicroservicesComponent, + BlueprintsComponent, + CompSpecAddComponent, + MsInstanceAddComponent, + CompSpecValidationComponent + ], + imports: [ + BrowserModule, + AppRoutingModule, + BrowserAnimationsModule, + SharedModule, + HttpClientModule, + LayoutModule, + MatToolbarModule, + MatButtonModule, + MatSidenavModule, + MatIconModule, + MatListModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatTreeModule, + MatCardModule, + MatProgressSpinnerModule, + MatTableExporterModule, + TableModule, + ButtonModule, + SidebarModule, + MenuModule, + ToolbarModule, + PanelMenuModule, + FormsModule, + ReactiveFormsModule, + CardModule, + JwtModule.forRoot({ + config: { + tokenGetter: ()=>localStorage.getItem('jwt') + } + }), + DialogModule, + ToastModule, + Ng4LoadingSpinnerModule, + TooltipModule, + AccordionModule, + SplitButtonModule, + DropdownModule, + FileUploadModule, + InputTextareaModule, + MatExpansionModule, + PasswordModule, + RadioButtonModule, + SelectButtonModule, + MatTooltipModule, + PaginatorModule, + ScrollPanelModule, + MatMenuModule, + CalendarModule, + MultiSelectModule, + CheckboxModule, + InputSwitchModule + ], + providers: [AuthGuard, RoleGuard, { + provide: HTTP_INTERCEPTORS, + useClass: JwtInterceptorService, + multi: true + }, Location, { provide: LocationStrategy, useClass: PathLocationStrategy }, MessageService], + bootstrap: [AppComponent] +}) +export class AppModule { } 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 diff --git a/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.css b/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.css new file mode 100644 index 0000000..54806ab --- /dev/null +++ b/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.css @@ -0,0 +1,43 @@ +/* + * # ============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========================================================= + */ + +.input { + padding-top: 10px; +} + +.inputLabel { + font-weight: 600; + margin-left: 20px; + width: 135px; +} + +.inputFieldSm { + width: 200px; + height: 35px; + padding-left: 6px; +} +.inputFieldMed { + width: 300px; + height: 35px; + padding-left: 6px; +} +.inputFieldLg { + width: 400px; + height: 35px; + padding-left: 6px; +} diff --git a/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.html b/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.html new file mode 100644 index 0000000..f15f19b --- /dev/null +++ b/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.html @@ -0,0 +1,64 @@ +<!-- + # ============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========================================================= + --> + +<p-dialog *ngIf="visible" header="Component Spec ADD" [(visible)]="visible" appendTo="body" [modal]="true" [transitionOptions]="'300ms'" + [closeOnEscape]="false" [closable]="false" [style]="{width: '650px'}" (onHide)="closeDialog()"> + + <!-- * * * * * Input fields * * * * * --> + <form [formGroup]="csAddForm" (ngSubmit)="saveCs()" class="bg-faded"> + <!-- * * * Type * * * --> + <div class="input"> + <label class="inputLabel">Type<span style="color:red">*</span></label> + <p-dropdown [options]="types" placeholder="Select Type" optionLabel="type" formControlName="type"></p-dropdown> + </div> + <!-- * * * Labels * * * --> + <div class="input"> + <label class="inputLabel">Labels</label> + <input class="inputFieldLg" type="text" pInputText formControlName="labels" /> + </div> + <span style="padding: 9px 0px 0px 158px; font-size: 13px;">(Separate labels with a space)</span> + <!-- * * * Notes * * * --> + <div class="input"> + <label class="inputLabel" style="vertical-align: top">Notes</label> + <textarea class="inputFieldLg" [rows]="1" [cols]="30" pInputTextarea autoResize="autoResize" formControlName="notes"></textarea> + </div> + <!-- * * * Comp Spec File Select * * * --> + <div class="input"> + <label class="inputLabel">Component Spec<span style="color:red">*</span></label> + + <input type="file" style="width: 460px; color:blue; font-style: italic;" (input)="onCompSpecUpload($event)" name="myfile" id="myfile" accept=".json"> + </div> + + <!-- * * * Policy File Select * * * --> + <div class="input"> + <label class="inputLabel">Policy</label> + + <input type="file" style="width: 460px; color:blue; font-style: italic;" (input)="onPolicyUpload($event)" + name="myPolicyFile" id="myPolicyFile" accept=".json"> + </div> + + <!-- * * * ADD and Cancel buttons * * * --> + <div style="float: right; padding: 20px 45px"> + <button pButton type="button" (click)="closeDialog()" label="Cancel"></button> + <button pButton type="submit" class="ui-button-success" label="Add" [disabled]="!csAddForm.valid || !compSpecSelected" style="width: 70px"></button> + </div> + </form> + + <p-toast key="jsonError" [style]="{width: '430px'}"></p-toast> + +</p-dialog> diff --git a/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.spec.ts b/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.spec.ts new file mode 100644 index 0000000..e29a110 --- /dev/null +++ b/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.spec.ts @@ -0,0 +1,77 @@ +/* + * # ============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 { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; +import { MessageService } from 'primeng/api'; +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; +import { DropdownModule } from 'primeng/dropdown'; +import { ToastModule } from 'primeng/toast'; + + +import { CompSpecAddComponent } from './comp-spec-add.component'; + +describe('CompSpecAddComponent', () => { + let component: CompSpecAddComponent; + let fixture: ComponentFixture<CompSpecAddComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CompSpecAddComponent], + imports: [ + DialogModule, + DropdownModule, + ToastModule, + FormsModule, + ReactiveFormsModule, + ButtonModule, + HttpClientTestingModule, + RouterTestingModule + ], + providers: [ + MessageService, + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CompSpecAddComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it(`should invalidate spec JSON structure`, async(() => { + const fixture = TestBed.createComponent(CompSpecAddComponent); + const app = fixture.debugElement.componentInstance; + let mockErrorJson = "test: 'test}" + app.compSpecContent = mockErrorJson + expect(() => app.validateJsonStructure()).toThrowError('JSON Structure error, quit!') + })); + +}); diff --git a/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.ts b/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.ts new file mode 100644 index 0000000..7564852 --- /dev/null +++ b/mod2/ui/src/app/comp-spec-add/comp-spec-add.component.ts @@ -0,0 +1,180 @@ +/* + * # ============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, Input, Output, EventEmitter } from '@angular/core'; +import { InputTextModule } from 'primeng/inputtext'; +import { DropdownModule } from 'primeng/dropdown'; +import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms'; +import { AuthService } from '../services/auth.service'; +import { MessageService } from 'primeng/api'; + +interface Type { + type: string; +} + +@Component({ + selector: 'app-comp-spec-add', + templateUrl: './comp-spec-add.component.html', + styleUrls: ['./comp-spec-add.component.css'] +}) + +export class CompSpecAddComponent implements OnInit { + + compSpecSelected: any; + compSpecContent: any; + + policySelected: any; + policyContent: any; + + csAddForm: FormGroup; + // The loggged in user + username: string; + + // Input form fields + type: string; + labels: string; + notes: string; + specFile: any; + policyJson: any; + + // Dropdowns + types: Type[]; + + // Build JSON as a string + csAddString: any; + // Return JSON to parent component + csAddJson: any; + + constructor(private fb: FormBuilder, private authService: AuthService, private messageService: MessageService) { + } + + @Input() visible: boolean; + @Output() handler: EventEmitter<any> = new EventEmitter(); + + ngOnInit() { + // The logged in user + this.username = this.authService.getUser().username; + + this.csAddForm = new FormGroup({ + type: new FormControl(), + labels: new FormControl(), + notes: new FormControl(), + specFile: new FormControl(), + policyJson: new FormControl() + }); + + // FORM fields and validations + this.csAddForm = this.fb.group({ + type: ['', [Validators.required]], + labels: ['', []], + notes: ['', []], + specFile: ['', []], + policyJson: ['', []] + }); + + // TYPE Dropdown + this.types = [ + { type: 'DOCKER' }, + { type: 'K8S' } + ]; + } + + saveCs() { + this.createOutputJson(); + this.csAddJson = JSON.stringify(this.csAddString); + this.handler.emit(this.csAddJson); + this.closeDialog(); + } + + // Create the JSON to be sent to the parent component + // The "labels" functions below take into account leading/trailing spaces, multiple spaces between labels, and conversion into an array + createOutputJson() { + this.validateJsonStructure(); + + let policy; + if(this.policyContent !== undefined){ + policy = JSON.parse(this.policyContent) + } else { + policy = null + } + + this.csAddString = { + specContent: JSON.parse(this.compSpecContent), + policyJson: policy, + type: this.csAddForm.value['type'].type, + metadata: { + labels: this.csAddForm.value['labels'].trim().replace(/\s{2,}/g, ' ').split(" "), + notes: this.csAddForm.value['notes'] + }, + user: this.username + }; + } + + // Validate, catch, display JSON structure error, and quit! + validateJsonStructure() { + try { + JSON.parse(this.compSpecContent); + } catch (error) { + this.messageService.add({ key: 'jsonError', severity: 'error', summary: 'Invalid Component Spec JSON', detail: error, sticky: true }); + throw new Error('JSON Structure error, quit!'); + } + + if(this.policyContent !== undefined){ + try { + JSON.parse(this.policyContent); + } catch (error) { + this.messageService.add({ key: 'jsonError', severity: 'error', summary: 'Invalid Policy JSON', detail: error, sticky: true }); + throw new Error('JSON Structure error, quit!'); + } + } + } + + // Read the selected Component Spec JSON file + onCompSpecUpload(event) { + this.compSpecSelected = event.target.files[0]; + this.readCsFileContent(this.compSpecSelected); + } + //Read the selected Component Spec JSON file + onPolicyUpload(event) { + this.policySelected = event.target.files[0]; + this.readPolicyFileContent(this.policySelected); + } + + readCsFileContent(file) { + if (file) { + let fileReader = new FileReader(); + fileReader.onload = (e) => { this.compSpecContent = fileReader.result; }; + fileReader.readAsText(file); + } + } + + readPolicyFileContent(file) { + if (file) { + let fileReader = new FileReader(); + fileReader.onload = (e) => { this.policyContent = fileReader.result; }; + fileReader.readAsText(file); + } + } + + // The handler emits 'null' back to parent to close dialog and make it available again when clicked + closeDialog() { + this.visible = false; + this.handler.emit(null); + } + +} diff --git a/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.css b/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.css new file mode 100644 index 0000000..d76e0fa --- /dev/null +++ b/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.css @@ -0,0 +1,73 @@ +/* + * # ============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========================================================= + */ + +.validator-input-card{ + width: 40%; + min-width: 430px; + min-height: 100%; + border: 2px solid slategray; + border-radius: 5px; + background-color: white; + padding: 1%; +} +.validator-output-card{ + width: 58%; + border: 2px solid slategray; + margin-left: 2%; + min-height: 100%; + border-radius: 5px; + background-color: white; + padding: 1%; +} + +.input-section-card{ + font-size: 14px; + border: solid slategray; + border-style: double; + margin-top: 7px +} + +.validateSpec{ + margin-top: 10px; +} +.downloadSchema{ + margin-top: 80px; +} + +.type-selection{ + position: absolute; + background-color: rgba(128, 128, 128, 0.15); + width: 190px; + height: 40px; + display: inline-flex; + align-items: center; + margin-left: 150px; +} + +.greenOutput{ + background-color: rgba(0, 255, 0, 0.205); + height: 40px; + padding: 3px; + border: 2px solid rgba(0, 66, 0, 0.747); +} +.redOutput{ + background-color: rgba(255, 0, 0, 0.267); + height: 87%; + padding: 10px; + border: 2px solid rgba(65, 0, 0, 0.712); +} diff --git a/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.html b/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.html new file mode 100644 index 0000000..9867364 --- /dev/null +++ b/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.html @@ -0,0 +1,108 @@ +<!-- + # ============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========================================================= + --> + +<div style="margin-left: 2%; margin-right: 2%;"> + <p style="font-size: 26px;">Component Specification Validation</p> + <div style="display: inline-flex; width: 100%; height: 100%; min-height: 450px;"> + <div class="validator-input-card"> + <div> + <mat-card class="input-section-card"> + <span><b>Action</b></span><span style="color:red">*</span><br> + <div style="display: inline-flex;"> + <div style="padding-right: 10%; width: 200px;"> + <p-radioButton name="spec-validator-actions" value="validateSpec" [(ngModel)]="spec_validator_action" (click)="validateRadioButton()"></p-radioButton> Validate Spec File + </div> + <div> + <p-radioButton name="spec-validator-actions" value="downloadSchema" [(ngModel)]="spec_validator_action" (click)="downloadRadioButton()"></p-radioButton> Download Schema + </div> + </div> + </mat-card> + + <mat-card class="input-section-card"> + <span><b>Release</b></span><span style="color:red">*</span><br> + <div> + <div style="display: inline-flex; align-items: center;"> + <p-radioButton name="spec-validator-release" value="2007" [(ngModel)]="release"></p-radioButton> 2007+ + </div> + </div> + </mat-card> + + <mat-card class="input-section-card"> + <span><b>Type</b></span><span style="color:red">*</span><br> + <div> + <div style="display: inline-flex; align-items: center;"> + <p-radioButton name="spec-validator-type" value="k8s" [(ngModel)]="type"></p-radioButton> K8s + </div> + <br> + <div style="display: inline-flex; align-items: center;"> + <p-radioButton name="spec-validator-type" value="docker" [(ngModel)]="type"></p-radioButton> Docker + </div> + </div> + </mat-card> + + <mat-card *ngIf="spec_validator_action === 'validateSpec'" class="input-section-card"> + <!-- * * * Comp Spec File Select * * * --> + <div> + <b>Component Spec File</b><span style="color:red">*</span><br> + + <div style="display: inline-flex;"> + <input #myFile type="file" style="color:blue; font-style: italic; width: fit-content" + (input)="onCompSpecUpload($event)" name="myfile" accept=".json"> + + <button pButton type="button" (click)="resetFile()" + style="background-color: transparent; border: none; height: 20px;"><i class="pi pi-times" + style="color: black;"></i></button> + </div> + </div> + </mat-card> + + <div> + <div *ngIf="shouldValidate" style="background-color: rgba(128, 128, 128, 0.315); height: 2px; width: 100%; margin-top: 2%;"></div> + <div *ngIf="shouldDownload" style="background-color: rgba(128, 128, 128, 0.315); height: 2px; width: 100%; margin-top: 22%;"></div> + + <div style="float: right; margin-top: 8px"> + <div *ngIf="shouldValidate" matTooltip="Fill In Required Fields" [matTooltipDisabled]="!(release === '' || type === '' || compSpecContent === null)" matTooltipPosition="above"> + <button [disabled]="release === '' || type === '' || compSpecContent === null" pButton label="Validate Spec" type="button" (click)="validateSpec()"></button> + </div> + <div *ngIf="shouldDownload" matTooltip="Fill In Required Fields" [matTooltipDisabled]="!(release === '' || type === '')" matTooltipPosition="above"> + <button [disabled]="release === '' || type === ''" pButton label="Download Schema" type="button" (click)="downloadSchema()"></button> + </div> + </div> + </div> + </div> + </div> + + <div *ngIf="shouldValidate" class="validator-output-card"> + <span><b>Output:</b></span><br><br> + <div *ngIf="specValidated" style="width: 100%; padding: 1%; border-radius: 3px; overflow: hidden;" [ngClass]="{'greenOutput' : validCompSpec === true, 'redOutput' : validCompSpec === false}"> + <div> + <span style="font-weight: 500;">{{specValidationOutputHeader}}</span> + <div *ngIf="specValidationOutputMessage !== ''" style="margin-top: 10px; width: 100%; height: 2px; background-color: rgba(128, 128, 128, 0.315); "></div> + + <div *ngIf="specValidationOutputMessage !== ''" style="margin-top: 10px"> + <p-scrollPanel [style]="{width: '100%', height: '50vh'}"> + <pre style="white-space: pre-wrap;"><b>Summary:</b><br>{{specValidationOutputSummary}}</pre> + <pre style="white-space: pre-wrap;"><b>Message(s):</b><br>{{specValidationOutputMessage}}</pre> + </p-scrollPanel> + </div> + + </div> + </div> + </div> + </div> +</div>
\ No newline at end of file diff --git a/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.spec.ts b/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.spec.ts new file mode 100644 index 0000000..a9efe28 --- /dev/null +++ b/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.spec.ts @@ -0,0 +1,132 @@ +/* + * # ============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 { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { RadioButtonModule } from 'primeng/radiobutton'; + +import { CompSpecValidationComponent } from './comp-spec-validation.component'; +import { FormsModule } from '@angular/forms'; +import { MatCardModule, MatTooltipModule } from '@angular/material'; +import { ScrollPanelModule } from 'primeng/scrollpanel'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { MessageService } from 'primeng/api'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; +import { Ng4LoadingSpinnerModule } from 'ng4-loading-spinner'; + +describe('CompSpecValidationComponent', () => { + let component: CompSpecValidationComponent; + let fixture: ComponentFixture<CompSpecValidationComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CompSpecValidationComponent ], + imports: [ + RadioButtonModule, + FormsModule, + MatCardModule, + MatTooltipModule, + ScrollPanelModule, + HttpClientTestingModule, + Ng4LoadingSpinnerModule + ], + providers: [ + MessageService, + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CompSpecValidationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it(`should have as shouldValidate 'false'`, () => { + const fixture = TestBed.createComponent(CompSpecValidationComponent); + const app = fixture.debugElement.componentInstance; + expect(app.shouldValidate).toEqual(false); + }); + + it(`should change shouldValidate to 'true'`, async(() => { + const fixture = TestBed.createComponent(CompSpecValidationComponent); + const app = fixture.debugElement.componentInstance; + app.validateRadioButton() + fixture.detectChanges(); + expect(app.shouldValidate).toEqual(true); + })); + + it(`should have as shouldDownload 'false'`, () => { + const fixture = TestBed.createComponent(CompSpecValidationComponent); + const app = fixture.debugElement.componentInstance; + expect(app.shouldDownload).toEqual(false); + }); + + it(`should change shouldDownload to 'true'`, async(() => { + const fixture = TestBed.createComponent(CompSpecValidationComponent); + const app = fixture.debugElement.componentInstance; + app.downloadRadioButton() + fixture.detectChanges(); + expect(app.shouldDownload).toEqual(true); + })); + + it(`should set validation error message`, async(() => { + const fixture = TestBed.createComponent(CompSpecValidationComponent); + const app = fixture.debugElement.componentInstance; + let mockSuccess = { + status: 200 + } + app.setSpecValidationMessage(mockSuccess) + fixture.detectChanges(); + expect(app.validCompSpec).toEqual(true) + })); + + it(`should set validation error message`, async(() => { + const fixture = TestBed.createComponent(CompSpecValidationComponent); + const app = fixture.debugElement.componentInstance; + + let mockError = { + status: 400, + error: { + summary: 'Test', + errors: [ + 'error1', + 'error2' + ] + } + } + app.setSpecValidationMessage(mockError) + fixture.detectChanges(); + expect(app.validCompSpec).toEqual(false) + })); + + it(`should invalidate JSON structure`, async(() => { + const fixture = TestBed.createComponent(CompSpecValidationComponent); + const app = fixture.debugElement.componentInstance; + let mockErrorJson = "test: 'test}" + app.compSpecContent = mockErrorJson + expect(() => app.validateJsonStructure()).toThrowError('JSON Structure error, quit!') + })); + +}); diff --git a/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.ts b/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.ts new file mode 100644 index 0000000..2ce8fef --- /dev/null +++ b/mod2/ui/src/app/comp-spec-validation/comp-spec-validation.component.ts @@ -0,0 +1,145 @@ +/* + * # ============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 } from '@angular/core'; +import { SpecValidationService } from '../services/spec-validation.service'; +import { DownloadService } from '../services/download.service'; +import { Ng4LoadingSpinnerService } from 'ng4-loading-spinner'; + +@Component({ + selector: 'app-comp-spec-validation', + templateUrl: './comp-spec-validation.component.html', + styleUrls: ['./comp-spec-validation.component.css'] +}) +export class CompSpecValidationComponent implements OnInit { + + @ViewChild('myFile', {static: false}) + myInputVariable: ElementRef; + + spec_validator_action; + release = ''; + type = ''; + shouldValidate = false; + shouldDownload = false; + validCompSpec = false; + specValidated = false; + specValidationOutputSummary: any; + + constructor(private specValidator: SpecValidationService, private downloadService: DownloadService, private spinnerService: Ng4LoadingSpinnerService ) { } + + ngOnInit() { + + } + + compSpecSelected: any; + onCompSpecUpload(event){ + this.compSpecSelected = event.target.files[0]; + this.readCsFileContent(this.compSpecSelected); + } + + compSpecContent: any = null; + readCsFileContent(file) { + if (file) { + let fileReader = new FileReader(); + fileReader.onload = (e) => { this.compSpecContent = fileReader.result; }; + fileReader.readAsText(file); + } + } + + validateRadioButton(){ + this.shouldValidate = true; + this.shouldDownload = false; + } + + downloadRadioButton(){ + this.shouldValidate = false; + this.shouldDownload = true; + this.compSpecContent = null; + this.specValidated = false + } + + resetFile(){ + this.myInputVariable.nativeElement.value = ""; + this.compSpecContent = null + this.compSpecSelected = null + this.specValidated = false + } + + specValidationOutputHeader = '' + specValidationOutputMessage = ''; + validateSpec(){ + this.specValidationOutputHeader = "" + this.specValidationOutputMessage = "" + this.specValidationOutputSummary = "" + + this.spinnerService.show() + this.validateJsonStructure() + this.specValidator.sendSpecFile(this.compSpecContent, this.type, this.release).subscribe( + res => {}, err => { + this.setSpecValidationMessage(err) + } + ) + } + + setSpecValidationMessage(res){ + if(res.status === 200){ + this.specValidationOutputHeader = "Success: Valid Component Spec" + this.specValidationOutputMessage = "" + this.validCompSpec = true + } else { + this.specValidationOutputHeader = `${res.status} Error: Invalid Component Spec` + this.specValidationOutputSummary = res.error.summary + + for(let item of res.error.errors){ + this.specValidationOutputMessage += `- ${item}\n\n` + } + + this.validCompSpec = false + } + + this.specValidated = true; + this.spinnerService.hide() + } + + validateJsonStructure() { + try { + JSON.parse(this.compSpecContent); + } catch (error) { + this.specValidationOutputHeader = "Error: Invalid Component Spec" + this.specValidationOutputSummary = "JSON Structure Error" + this.specValidationOutputMessage = error + this.validCompSpec = false + this.specValidated = true + this.spinnerService.hide() + throw new Error('JSON Structure error, quit!'); + } + } + + downloadSchema(){ + this.spinnerService.show() + this.specValidator.getSchema(this.type).subscribe( + res => { + this.downloadService.downloadJSON(res, `${this.release}+_${this.type}_Schema`) + this.spinnerService.hide() + }, err => { + console.log(err) + this.spinnerService.hide() + } + ) + } +} diff --git a/mod2/ui/src/app/comp-specs/comp-specs.component.css b/mod2/ui/src/app/comp-specs/comp-specs.component.css new file mode 100644 index 0000000..5f86e73 --- /dev/null +++ b/mod2/ui/src/app/comp-specs/comp-specs.component.css @@ -0,0 +1,121 @@ +/* + * # ============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 +} + +textarea +{ + font-size: 12px; +} + +.table_actions_button{ + background-color: transparent; + border: none; + width: 20px; + height: 20px; + vertical-align: middle; +} + +.row-expand-layout{ + display: grid; + grid-template-columns: 30% 40% auto; + grid-gap: 10px; + grid-auto-rows: minmax(100px, auto); +} + +.row-expand-card{ + font-size: 12px; + grid-row: 1; + border-radius: 5px; + border: 1px solid slategray; + padding: 10px; + /* This height prevents vertical scroll bar in Notes */ + height: 92px; + overflow: hidden; +} + +label { + cursor: pointer; +} + +.fa-refresh{ + cursor: pointer; +} + +.input{ + padding-top: 10px; +} + +.inputLabel { + font-weight: 600; + margin-left: 20px; + width: 140px; +} + +.inputFieldSm { + width: 200px; + height: 35px; + padding-left: 6px; +} +.inputFieldMed { + width: 300px; + height: 35px; + padding-left: 6px; +} +.inputFieldLg { + width: 400px; + height: 35px; + padding-left: 6px; +} + +.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.87) +} + +.redStatus{ + background-color: rgba(255, 29, 29, 0.733) +} + +.blueStatus{ + background-color: rgba(0, 183, 255, 0.432) +} + +.greyStatus{ + background-color: rgba(150, 150, 150, 0.432) +} + +.ui-state-highlight { + background-color: #878C94 !important; + color: black !important; +} diff --git a/mod2/ui/src/app/comp-specs/comp-specs.component.html b/mod2/ui/src/app/comp-specs/comp-specs.component.html new file mode 100644 index 0000000..3061cdf --- /dev/null +++ b/mod2/ui/src/app/comp-specs/comp-specs.component.html @@ -0,0 +1,189 @@ +<!-- + # ============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========================================================= + --> + +<div style="margin: 0px 50px 10px 20px; border: 1px solid darkslategray; min-width: 980px"> + <p-table #dt *ngIf="loadTable" [columns]="cols" [value]="csElements" sortMode="multiple" [paginator]="true" + [rows]="18" [rowsPerPageOptions]="[10,12,14,16,18,20,25,50]" (onFilter)="onTableFiltered(dt.filteredValue)" dataKey="id"> + <ng-template pTemplate="caption"> + + <div style="margin-left: -18%; width: 82%; max-height: 25px; display: inline-flex;"> + <!--CS Table Header--> + <div style="float: left;"> + <!--Refresh--> + <i class="fa fa-refresh" (click)="getAllCs()"></i> + <!--Global Filter--> + <input type="text" pInputText size="50" placeholder="Global Filter" + (input)="dt.filterGlobal($event.target.value, 'contains')" + style="width: 250px; height:25px; font-size: 12px; margin-left: 15px"> + <i class="fa fa-search" style="margin:4px 0px 0 8px"></i> + </div> + + <h4 style="margin-left: 15%"><b>Component Specs</b></h4> + + </div> + </ng-template> + + <ng-template pTemplate="header" let-columns> + <tr> + <th style="width: 3em"></th> + <th class="ui-state-highlight" *ngFor="let col of columns" [pSortableColumn]="col.field" + style="font-size: 12px; outline: none; vertical-align: bottom; text-align: center;" [ngStyle]="{'width': col.width}"> + {{col.header}}<br> + <p-sortIcon [field]="col.field"></p-sortIcon> + </th> + <th style="font-size: 13px; width: 8%; vertical-align: top; text-align: center;"> + Actions + </th> + </tr> + + <!--Second header row for individual column filters--> + <tr style="text-align: center;"> + <th style="width: 3em"></th> + <th *ngFor="let col of columns" [ngSwitch]="col.field"> + <input pInputText type="text" (input)="dt.filter($event.target.value, col.field, 'contains')" + style="width: 100%; height: 20px; font-size: 10px;" placeholder="Filter"> + </th> + <th> + + </th> + </tr> + </ng-template> + + <ng-template pTemplate="body" let-rowData let-csElem> + <tr style="font-size: 12px;"> + <!--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: fit-content; width: -moz-max-content; padding: 0px 5px 0px 5px; border-radius: 3px; font-weight: 600;" + [ngClass]="{'greenStatus' : csElem[col.field] === 'ACTIVE', 'greyStatus' : csElem[col.field] === 'INACTIVE'}"> + {{csElem[col.field]}} + </div> + <div *ngIf="col.field!=='status'">{{csElem[col.field]}}</div> + </td> + + <!--Actions Column--> + <td> + <div style="text-align: center;"> + <button pButton type="button" + style="background-color: transparent; border: none; width: 20px; height: 20px; vertical-align: middle;" + class="ui-button-secondary" [matMenuTriggerFor]="menu"> + <i class="pi pi-ellipsis-h" style="color: grey;"></i> + </button> + <mat-menu #menu="matMenu" xPosition="before"> + + <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-search" style="font-size: 10px;"></i> View</span> + </div> + + <button mat-menu-item class="table_action_item" (click)="showViewCsDialog(rowData)">Component Spec</button> + <div matTooltip="Policy not included" [matTooltipDisabled]="rowData.policyJson" matTooltipPosition="left"> + <button mat-menu-item [disabled]="!rowData.policyJson" class="table_action_item" (click)="showViewPolicyDialog(rowData)">Policy</button> + </div> + <!-- + <div *ngIf="rowData.status !== 'ACTIVE'"> + <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" + style="font-size: 10px;"></i> Update</span> + </div> + + <button mat-menu-item class="table_action_item" (click)="toActive(rowData)">To Active</button> + </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}} + </div> + <!-- Notes --> + <div class="row-expand-card" style="background-color: rgba(100, 148, 237, 0.219)"> + <b>Notes:</b><br> + <p-scrollPanel [style]="{width: '100%', height: '62px'}"> + <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.199)"> + <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> + </div> + </td> + </tr> + </ng-template> + </p-table> + + <!--download buttons for exporting table to either csv or excel file--> + <div *ngIf="loadTable" style="margin-left: 10px; margin-top: -32px; float: left;"> + <button pButton type="button" (click)="exportTable('csv')" matTooltip="Export Table to CSV" + matTooltipPosition="above" + style="border-radius: 5px; width: 65px; height: 22px; font-size: 14px; border: none; margin-top: 4px; display: inline-flex;"> + <i class="pi pi-file" style="margin-top: 2px; margin-left: 8px;"></i> + <label style="font-weight: 800; vertical-align: middle;">CSV</label> + </button> + <button pButton type="button" (click)="exportTable('excel')" matTooltip="Export Table to XLSX" + matTooltipPosition="above" + style="border-radius: 5px; width: 65px; height: 22px; margin-left: 7px; font-size: 14px; background-color: green; border: none; display: inline-flex;"> + <i class="pi pi-file-excel" style="margin-top: 2px; margin-left: 4px;"></i> + <label style="font-weight: 800; vertical-align: middle;">Excel</label> + </button> + </div> +</div> + +<!-- * * * * View Spec Content Pop Up * * * * --> +<p-dialog [(visible)]="showViewCs" header="Spec Content" appendTo="body" [maximizable]="true" + [modal]="true" [style]="{width: '80vw'}" [baseZIndex]="10000" [closable]="false"> + <pre>{{specContentToView | json}}</pre> + <p-footer> + <button pButton label="Close" (click)="showViewCs=false" type="button"></button> + <button pButton label="Download" (click)="download(specContentToView, 'Component_Spec')" type="button"></button> + </p-footer> +</p-dialog> + +<!-- * * * * View Policy JSON Pop Up * * * * --> +<p-dialog [(visible)]="showViewPolicy" header="Policy Json" appendTo="body" [maximizable]="true" [modal]="true" + [style]="{width: '80vw'}" [baseZIndex]="10000" [closable]="false"> + <pre>{{policyJsonToView | json}}</pre> + <p-footer> + <button pButton label="Close" (click)="showViewPolicy=false" type="button"></button> + <button pButton label="Download" (click)="download(policyJsonToView, 'Policy_Json')" type="button"></button> + </p-footer> +</p-dialog> diff --git a/mod2/ui/src/app/comp-specs/comp-specs.component.spec.ts b/mod2/ui/src/app/comp-specs/comp-specs.component.spec.ts new file mode 100644 index 0000000..64405e3 --- /dev/null +++ b/mod2/ui/src/app/comp-specs/comp-specs.component.spec.ts @@ -0,0 +1,133 @@ +/* + * # ============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 { HttpClientModule } from '@angular/common/http'; +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 { CompSpecsComponent } from './comp-specs.component'; + +describe('CompSpecsComponent', () => { + let component: CompSpecsComponent; + let fixture: ComponentFixture<CompSpecsComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + CompSpecsComponent + ], + imports: [ + Ng4LoadingSpinnerModule, + TableModule, + MatMenuModule, + ScrollPanelModule, + ToastModule, + DialogModule, + DropdownModule, + FormsModule, + ReactiveFormsModule, + ButtonModule, + HttpClientModule, + ToastModule, + RouterTestingModule, + MatTooltipModule + ], + providers: [ + MessageService, + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CompSpecsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it(`should fill csElements Object`, () => { + const fixture = TestBed.createComponent(CompSpecsComponent); + const app = fixture.debugElement.componentInstance; + + let mockCsElement = [{ + id: 'testId1234', + name: 'test-MS', + type: 'k8s', + specContent: '', + policyJson: 'test', + status: 'New', + msInstanceInfo: { + release: '2008', + name: 'test Ms', + }, + metadata: { + createdBy: 'test', + createdOn: '01-01-2020 12:00', + updatedBy: 'test', + updatedOn: '01-01-2020 12:00', + notes: 'test', + labels: ['test'], + } + }] + + app.fillTable(mockCsElement) + + expect(app.loadTable).toEqual(true); + expect(app.csElements.length).toEqual(1); + }); + + it(`should set spec content to view`, () => { + const fixture = TestBed.createComponent(CompSpecsComponent); + const app = fixture.debugElement.componentInstance; + let mockData = { + specContent: 'test' + } + app.showViewCsDialog(mockData) + expect(app.showViewCs).toEqual(true); + expect(app.specContentToView).toEqual('test'); + }); + + it(`should set policy json content to view`, () => { + const fixture = TestBed.createComponent(CompSpecsComponent); + const app = fixture.debugElement.componentInstance; + let mockData = { + policyJson: 'test' + } + app.showViewPolicyDialog(mockData) + expect(app.showViewPolicy).toEqual(true); + expect(app.policyJsonToView).toEqual('test'); + }); +}); + diff --git a/mod2/ui/src/app/comp-specs/comp-specs.component.ts b/mod2/ui/src/app/comp-specs/comp-specs.component.ts new file mode 100644 index 0000000..4327c0b --- /dev/null +++ b/mod2/ui/src/app/comp-specs/comp-specs.component.ts @@ -0,0 +1,211 @@ +/* + * # ============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 } from '@angular/core'; +import { compSpecsService } from '../services/comp-specs-service.service'; +import { Table } from 'primeng/table'; +import { MessageService } from 'primeng/api'; +import { Ng4LoadingSpinnerService } from 'ng4-loading-spinner'; +import { DatePipe } from '@angular/common'; +import { trigger, state, transition, style, animate } from '@angular/animations'; +import { ActivatedRoute } from '@angular/router'; +import { DownloadService } from '../services/download.service'; + +@Component({ + selector: 'app-comp-specs', + templateUrl: './comp-specs.component.html', + styleUrls: ['./comp-specs.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] +}) +export class CompSpecsComponent implements OnInit { + @ViewChild(Table, { static: false }) dt: Table; + + csElements: any[] = []; + + cols: any[] = [ + { field: 'instanceName', header: 'Instance Name' }, + { field: 'release', header: 'Release', width: '15%' }, + { field: 'type', header: 'Type', width: '15%' }, + { field: 'policy', header: 'Policy', width: '15%' }, + { field: 'status', header: 'Status', width: '15%' } + ]; + + columns: any[]; + loadTable: boolean; + filteredRows: any; + summaryRows: any; + downloadItems: { label: string; command: () => void; }[]; + + msInstanceId: string; + msInstanceName: any; + msInstanceRelease: any; + + constructor(private csApis: compSpecsService, private messageService: MessageService, + private spinnerService: Ng4LoadingSpinnerService, private datePipe: DatePipe, + private route: ActivatedRoute, private downloadService: DownloadService) { } + + //create table of comp specs + ngOnInit() { + this.loadTable = false; + + this.route.queryParams.subscribe((params) => { + this.msInstanceId = params['instanceId']; + }); + + this.getAllCs() + } + + getAllCs() { + + this.csElements = []; + + this.csApis.getAllCompSpecs(this.msInstanceId) + .subscribe((data: any[]) => { + this.fillTable(data) + }) + + this.columns = this.cols.map(col => ({ title: col.header, dataKey: col.field })); + } + + //filter table + onTableFiltered(values) { + if (values) { this.filteredRows = values; } + else { this.filteredRows = this.summaryRows; } + } + + /* * * * Export ms instance table to excel or csv * * * */ + exportTable(exportTo) { + let downloadElements: any[] = [] + + //labels array not handled well by excel download so converted them to a single string + for (let row of this.filteredRows) { + let labels; + let notes; + if (exportTo === "excel") { + if (row.metadata.labels !== undefined) { + 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, + Release: row.release, + Type: row.type, + Status: row.status, + Created_By: row.metadata.createdBy, + Created_On: row.metadata.createdOn, + Updated_By: row.metadata.updatedBy, + Updated_On: row.metadata.updatedOn, + Notes: notes, + Labels: labels + }) + } + + let arrHeader = [] + + if (exportTo === "csv") { + arrHeader = [ + "Instance_Name", + "Release", + "Type", + "Status", + "Created_By", + "Created_On", + "Updated_By", + "Updated_On", + "Notes", + "Labels" + ]; + } + + this.downloadService.exportTableData(exportTo, downloadElements, arrHeader) + } + + //fill object with microservice data, to be used to fill table. + //checks if fields are empty and if they are, store 'N/A' as the values + fillTable(data) { + for (let elem of data) { + let policy = ''; + if(elem.policyJson){policy = "Included"} + + let tempCsElement: any = { + id: elem.id, + instanceName: elem.msInstanceInfo.name, + release: elem.msInstanceInfo.release, + type: elem.type, + policy: policy, + status: elem.status, + specContent: elem.specContent, + policyJson: elem.policyJson, + metadata: { + 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'), + notes: elem.metadata.notes, + labels: elem.metadata.labels + } + } + this.csElements.unshift(tempCsElement) + } + this.msInstanceName = this.csElements[0]['instanceName'] + this.msInstanceRelease = this.csElements[0]['release'] + this.filteredRows = this.csElements + this.loadTable = true; + this.spinnerService.hide(); + } + + showViewCs: boolean = false; + specContentToView: string; + showViewCsDialog(data) { + this.showViewCs = true; + this.specContentToView = data.specContent; + } + + showViewPolicy: boolean = false; + policyJsonToView: string; + showViewPolicyDialog(data) { + this.showViewPolicy = true; + this.policyJsonToView = data.policyJson; + } + + /* * * * Download single spec file or policy * * * */ + download(content, contentType) { + let fileName = `${this.msInstanceName}_${this.msInstanceRelease}_${contentType}` + this.downloadService.downloadJSON(content, fileName) + } +}
\ No newline at end of file diff --git a/mod2/ui/src/app/guards/auth.guard.spec.ts b/mod2/ui/src/app/guards/auth.guard.spec.ts new file mode 100644 index 0000000..c82f8d6 --- /dev/null +++ b/mod2/ui/src/app/guards/auth.guard.spec.ts @@ -0,0 +1,44 @@ +/* + * # ============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 { TestBed, async, inject } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { AuthGuard } from './auth.guard'; + +describe('AuthGuard', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + AuthGuard, + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ], + imports: [ + HttpClientTestingModule, + RouterTestingModule + ] + }); + }); + + it('should ...', inject([AuthGuard], (guard: AuthGuard) => { + expect(guard).toBeTruthy(); + })); +}); diff --git a/mod2/ui/src/app/guards/auth.guard.ts b/mod2/ui/src/app/guards/auth.guard.ts new file mode 100644 index 0000000..cce544b --- /dev/null +++ b/mod2/ui/src/app/guards/auth.guard.ts @@ -0,0 +1,62 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; +import { Observable, of } from 'rxjs'; +import { AuthService } from '../services/auth.service'; +import { catchError, map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate { + + constructor( + private authService: AuthService, + private router: Router + ) {} + + canActivate( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { + + return this.authService.checkLogin().pipe(map((e:boolean)=>{ + if(e){ + this.authService.setUser(); + if(this.authService.getUser().roles.includes("ROLE_ADMIN")){ + this.authService.isAdmin = true; + }else{ + this.authService.isAdmin = false; + } + this.authService.authPass=true; + return true; + } + }), catchError(err=>{ + this.authService.reLoginMsg = true; + // window.alert("Your login has expired. Please log in again."); + this.authService.authPass=false; + this.router.navigate(['/login']); + return of(false); + })) + } + + + + +} diff --git a/mod2/ui/src/app/guards/login.guard.spec.ts b/mod2/ui/src/app/guards/login.guard.spec.ts new file mode 100644 index 0000000..d58450c --- /dev/null +++ b/mod2/ui/src/app/guards/login.guard.spec.ts @@ -0,0 +1,41 @@ +/* + * # ============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 { TestBed, async, inject } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { LoginGuard } from './login.guard'; + +describe('LoginGuard', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + LoginGuard, + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ], + imports: [HttpClientTestingModule, RouterTestingModule] + }); + }); + + it('should ...', inject([LoginGuard], (guard: LoginGuard) => { + expect(guard).toBeTruthy(); + })); +}); diff --git a/mod2/ui/src/app/guards/login.guard.ts b/mod2/ui/src/app/guards/login.guard.ts new file mode 100644 index 0000000..851ef61 --- /dev/null +++ b/mod2/ui/src/app/guards/login.guard.ts @@ -0,0 +1,52 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; +import { Observable, of } from 'rxjs'; +import { AuthService } from '../services/auth.service'; +import { catchError, map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class LoginGuard implements CanActivate { + constructor( + private authService: AuthService, + private router: Router + ) {} + + canActivate( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { + + return this.authService.checkLogin().pipe(map((e:boolean)=>{ + if(e){ + console.log("Hi this is loginguard"); + this.authService.authPass=true; + this.router.navigate(['/home']); + return true; + } + }), catchError(err=>{ + console.log("Login guard out"); + this.authService.authPass=false; + this.router.navigate(['/login']); + return of(false); + })) +} +} diff --git a/mod2/ui/src/app/guards/role.guard.spec.ts b/mod2/ui/src/app/guards/role.guard.spec.ts new file mode 100644 index 0000000..c57dfe6 --- /dev/null +++ b/mod2/ui/src/app/guards/role.guard.spec.ts @@ -0,0 +1,44 @@ +/* + * # ============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 { TestBed, async, inject } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { RoleGuard } from './role.guard'; + +describe('RoleGuard', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + RoleGuard, + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ], + imports: [ + HttpClientTestingModule, + RouterTestingModule + ] + }); + }); + + it('should ...', inject([RoleGuard], (guard: RoleGuard) => { + expect(guard).toBeTruthy(); + })); +}); diff --git a/mod2/ui/src/app/guards/role.guard.ts b/mod2/ui/src/app/guards/role.guard.ts new file mode 100644 index 0000000..7e5b7ae --- /dev/null +++ b/mod2/ui/src/app/guards/role.guard.ts @@ -0,0 +1,61 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; +import { Observable, of } from 'rxjs'; +import { AuthService } from '../services/auth.service'; +import { catchError, map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class RoleGuard implements CanActivate { + + + constructor( + private authService: AuthService, + private router: Router + ) {} + + canActivate( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { + + return this.authService.checkLogin().pipe(map((e:boolean)=>{ + if(e){ + console.log("role guard in"); + this.authService.setUser(); + if(this.authService.getUser().roles.includes("ROLE_ADMIN")){ + this.authService.isAdmin = true; + }else{ + this.authService.isAdmin = false; + this.router.navigate(['/home']); + } + return this.authService.getUser().roles.includes("ROLE_ADMIN"); + } + }), catchError(err=>{ + this.authService.reLoginMsg = true; + // window.alert("Your login has expired. Please log in again."); + this.authService.authPass=false; + this.router.navigate(['/login']); + return of(false); + })) + +} +} diff --git a/mod2/ui/src/app/home/home.component.css b/mod2/ui/src/app/home/home.component.css new file mode 100644 index 0000000..f345db2 --- /dev/null +++ b/mod2/ui/src/app/home/home.component.css @@ -0,0 +1,50 @@ +/* + * # ============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========================================================= + */ + + +.card{ + display: inline-block; + margin-top: 56px; + margin-left: 75px; + color: rgb(34, 32, 32); + font-family: Arial, Helvetica, sans-serif; + font-weight: 800; + text-align: center; + padding-top: 42px; + font-size: 20px; + border-width: 2px; + border-color: gray; + border-radius: 8px; + height: 112px; + width: 261px; + cursor: pointer +} + +#subMenu{ + height: 75px; + width: 229px; + padding-top: 24px; + margin-left: 61px; + font-size: 18px; + border-radius: 5px +} + + +.mat-form-field + .mat-form-field { + margin-left: 8px; +}
\ No newline at end of file diff --git a/mod2/ui/src/app/home/home.component.html b/mod2/ui/src/app/home/home.component.html new file mode 100644 index 0000000..31fe720 --- /dev/null +++ b/mod2/ui/src/app/home/home.component.html @@ -0,0 +1,93 @@ +<!-- + # ============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========================================================= + --> + +<div style="margin: -50px 0px 0px -30px"> + <mat-card + (click)="toggleMsMenu()" + appMaterialElevation + [defaultElevation]="defaultElevation" + raisedElevation="16" + class="card" + style="background-color: #60a4a5"> + Microservices... + </mat-card> + <mat-card + (click)="navSelect('Onboarding Tools')" + appMaterialElevation + [defaultElevation]="defaultElevation" + raisedElevation="16" + class="card" + style="background-color: rgba(154, 135, 167, 0.904);" + [routerLink]="'/OnboardingTools'"> + Onboarding Tools + </mat-card> + <mat-card *ngIf="authService.isAdmin" + (click)="navSelect('User Management')" + appMaterialElevation + [defaultElevation]="defaultElevation" + raisedElevation="16" + class="card" + style="background-color: rgba(160, 120, 83, 0.904);" + [routerLink]="'/users'"> + User Management + </mat-card> +</div> +<br> +<div *ngIf="displayMsMenu" style="margin-top: -60px"> + <mat-card id="subMenu" (click)="navSelect('Microservices')" + appMaterialElevation + [defaultElevation]="defaultElevation" + raisedElevation="16" + class="card" + style="background-color: #70a7a9; position: absolute;"> + Microservices + </mat-card> +</div> +<br> +<div *ngIf="displayMsMenu" style="margin-top: 70px"> + <mat-card id="subMenu" (click)="navSelect('MS Instances')" + appMaterialElevation + [defaultElevation]="defaultElevation" + raisedElevation="16" + class="card" + style="background-color: #8fbbbc; position: absolute;"> + MS Instances + </mat-card> +</div> +<br> +<div *ngIf="displayMsMenu" style="margin-top: 70px"> + <mat-card id="subMenu" (click)="navSelect('Blueprints')" + appMaterialElevation + [defaultElevation]="defaultElevation" + raisedElevation="16" + class="card" + style="background-color: #afcecf; position: absolute;"> + Blueprints + </mat-card> +</div> +<br> +<div *ngIf="displayMsMenu" style="margin-top: 70px"> + <mat-card id="subMenu" (click)="navSelect('MOD APIs')" + appMaterialElevation + [defaultElevation]="defaultElevation" + raisedElevation="16" + class="card" + style="background-color: #cfe2e2; position: absolute;"> + MOD APIs + </mat-card> +</div>
\ No newline at end of file diff --git a/mod2/ui/src/app/home/home.component.ts b/mod2/ui/src/app/home/home.component.ts new file mode 100644 index 0000000..36f9ee1 --- /dev/null +++ b/mod2/ui/src/app/home/home.component.ts @@ -0,0 +1,64 @@ +/* + * # ============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, HostBinding } from '@angular/core'; +import { AppComponent } from '../app.component'; +import { AuthService } from '../services/auth.service'; +import { BreadcrumbService } from '../services/breadcrumb.service'; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.css'] +}) +export class HomeComponent implements OnInit { + + defaultElevation = 2; + raisedElevation = 8; + panelOpenState = false; + + name = 'Angular'; + + displayMsMenu = false; + + constructor(private appComp: AppComponent, public authService: AuthService, private bread: BreadcrumbService) { } + + ngOnInit() { + } + + disableAnimation = true; + ngAfterViewInit(): void { + // timeout required to avoid the dreaded 'ExpressionChangedAfterItHasBeenCheckedError' + setTimeout(() => this.disableAnimation = false); + } + + toggleMsMenu() { + if (this.displayMsMenu == false) { + this.displayMsMenu = true + } else { + this.displayMsMenu = false + } + } + + navSelect(menuItem: any) { + this.appComp.tree_handler(menuItem, null); + // Set the breadcrumbs for the selected menu item (card) + this.bread.setBreadcrumbs(menuItem, "reset"); + } + +} diff --git a/mod2/ui/src/app/login/login.component.css b/mod2/ui/src/app/login/login.component.css new file mode 100644 index 0000000..55e48a1 --- /dev/null +++ b/mod2/ui/src/app/login/login.component.css @@ -0,0 +1,25 @@ +/* + * # ============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========================================================= + */ + +.field-icon { + float:right; + left: -15px; + margin-top: 1px; + position: relative; + z-index: 2; +}
\ No newline at end of file diff --git a/mod2/ui/src/app/login/login.component.html b/mod2/ui/src/app/login/login.component.html new file mode 100644 index 0000000..b98baff --- /dev/null +++ b/mod2/ui/src/app/login/login.component.html @@ -0,0 +1,58 @@ +<!-- + # ============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========================================================= + --> + +<p-card header="Log in to MOD" [style]="{margin:'100px 300px 300px 300px'}"> +<hr class="line-break"> +<form [formGroup]="form" (ngSubmit)="submit()" class="p-5 bg-faded" style="margin-left: 220px;margin-bottom: 100px;"> + <div class="ui-g ui-fluid" > + <div class="ui-g-12 ui-md-4"> + <div class="ui-inputgroup" > + <span class="ui-inputgroup-addon"><i class="pi pi-user" style="line-height: 1.25;"></i></span> + <input type="text" placeholder="ATT UID" formControlName="username"> + </div> + </div> + </div> + <!-- <div class="ui-g ui-fluid" > + <div class="ui-g-12 ui-md-4"> + <div class="ui-inputgroup" > + <span class="ui-inputgroup-addon"><i class="pi pi-user" style="line-height: 1.25;"></i></span> + <input type="text" placeholder="ATT UID" formControlName="uid"> + </div> + </div> + </div> --> + <div class="ui-g ui-fluid"> + <div class="ui-g-12 ui-md-4"> + <div class="ui-inputgroup" > + <span class="ui-inputgroup-addon" (click)="hide=!hide"> + <i [ngClass]="hide? 'pi pi-eye-slash':'pi pi-eye'"></i></span> + <input [type]="hide? 'password':'text'" placeholder="password" formControlName="password" > + <!-- <a routerLink="/reset-password">Forget password?</a> --> + </div> + </div> + </div> + + <p-footer class="text-left ui-g-12"> + <!-- <button pButton type="button" class="ui-button-info" label="Cancel" (click)="cancel()" style="margin-right: .25em"></button> --> + <button pButton type="submit" class="ui-button-success" label="Login" [disabled]="!form.valid"></button> + </p-footer> + <div class="text-left ui-g-12"> + <p>Not a registered user? Contact the DCAE-MOD team at <i>dcae-mod-team@att.com</i></p> + <a href="mailto:dcae-mod-team@att.com?subject=account application&body=Please help open a dcae account with username:">Cick here to contact now</a> + </div> + </form> +</p-card> diff --git a/mod2/ui/src/app/login/login.component.spec.ts b/mod2/ui/src/app/login/login.component.spec.ts new file mode 100644 index 0000000..5656be4 --- /dev/null +++ b/mod2/ui/src/app/login/login.component.spec.ts @@ -0,0 +1,59 @@ +/* + * # ============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 { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; +import { CardModule } from 'primeng/card'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture<LoginComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [LoginComponent], + imports: [ + FormsModule, + ReactiveFormsModule, + CardModule, + HttpClientTestingModule, + RouterTestingModule + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/login/login.component.ts b/mod2/ui/src/app/login/login.component.ts new file mode 100644 index 0000000..16cafee --- /dev/null +++ b/mod2/ui/src/app/login/login.component.ts @@ -0,0 +1,65 @@ +/* + * # ============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 } from '@angular/core'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { AuthService } from '../services/auth.service'; +import { User } from '../models/User'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.css'] +}) + +export class LoginComponent implements OnInit { + + form: FormGroup; + hide: boolean=true; + + constructor(private fb: FormBuilder, private authService: AuthService, private router: Router) { } + + ngOnInit() { + this.form = this.fb.group({ + username: ['', [Validators.required]], + // uid: ['', [Validators.required]], + password: ['', [Validators.required]] + }); + } + + submit() { + this.authService.login(this.form.value as User).subscribe( + res => { + // if (this.authService.getUser().roles && this.authService.getUser().roles.includes("ROLE_USER")) { + // this.authService.isAdmin = false; + // } + if(this.authService.getUser().roles &&this.authService.getUser().roles.includes("ROLE_ADMIN")) { + this.authService.isAdmin = true; + } else { + this.authService.isAdmin = false; + } + this.router.navigate(['/home']); + }, + (err) => { + alert('User or Password is not correct, please re-enter'); + this.form.reset(); + } + ); + } +} diff --git a/mod2/ui/src/app/material-elevation.directive.ts b/mod2/ui/src/app/material-elevation.directive.ts new file mode 100644 index 0000000..8843d1e --- /dev/null +++ b/mod2/ui/src/app/material-elevation.directive.ts @@ -0,0 +1,64 @@ +/* + * # ============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 { Directive, ElementRef, HostListener, Input, Renderer2, OnChanges, SimpleChanges } from '@angular/core'; + +@Directive({ + selector: '[appMaterialElevation]' +}) +export class MaterialElevationDirective implements OnChanges { + + @Input() + defaultElevation = 2; + + @Input() + raisedElevation = 8; + + constructor( + private element: ElementRef, + private renderer: Renderer2 + ) { + this.setElevation(this.defaultElevation); + } + + ngOnChanges(_changes: SimpleChanges) { + this.setElevation(this.defaultElevation); + } + + @HostListener('mouseenter') + onMouseEnter() { + this.setElevation(this.raisedElevation); + } + + @HostListener('mouseleave') + onMouseLeave() { + this.setElevation(this.defaultElevation); + } + + setElevation(amount: number) { + // remove all elevation classes + const classesToRemove = Array.from((<HTMLElement>this.element.nativeElement).classList).filter(c => c.startsWith('mat-elevation-z')); + classesToRemove.forEach((c) => { + this.renderer.removeClass(this.element.nativeElement, c); + }); + + // add the given elevation class + const newClass = `mat-elevation-z${amount}`; + this.renderer.addClass(this.element.nativeElement, newClass); + } +} diff --git a/mod2/ui/src/app/microservices/microservices.component.css b/mod2/ui/src/app/microservices/microservices.component.css new file mode 100644 index 0000000..5f86e73 --- /dev/null +++ b/mod2/ui/src/app/microservices/microservices.component.css @@ -0,0 +1,121 @@ +/* + * # ============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 +} + +textarea +{ + font-size: 12px; +} + +.table_actions_button{ + background-color: transparent; + border: none; + width: 20px; + height: 20px; + vertical-align: middle; +} + +.row-expand-layout{ + display: grid; + grid-template-columns: 30% 40% auto; + grid-gap: 10px; + grid-auto-rows: minmax(100px, auto); +} + +.row-expand-card{ + font-size: 12px; + grid-row: 1; + border-radius: 5px; + border: 1px solid slategray; + padding: 10px; + /* This height prevents vertical scroll bar in Notes */ + height: 92px; + overflow: hidden; +} + +label { + cursor: pointer; +} + +.fa-refresh{ + cursor: pointer; +} + +.input{ + padding-top: 10px; +} + +.inputLabel { + font-weight: 600; + margin-left: 20px; + width: 140px; +} + +.inputFieldSm { + width: 200px; + height: 35px; + padding-left: 6px; +} +.inputFieldMed { + width: 300px; + height: 35px; + padding-left: 6px; +} +.inputFieldLg { + width: 400px; + height: 35px; + padding-left: 6px; +} + +.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.87) +} + +.redStatus{ + background-color: rgba(255, 29, 29, 0.733) +} + +.blueStatus{ + background-color: rgba(0, 183, 255, 0.432) +} + +.greyStatus{ + background-color: rgba(150, 150, 150, 0.432) +} + +.ui-state-highlight { + background-color: #878C94 !important; + color: black !important; +} diff --git a/mod2/ui/src/app/microservices/microservices.component.html b/mod2/ui/src/app/microservices/microservices.component.html new file mode 100644 index 0000000..95ec45c --- /dev/null +++ b/mod2/ui/src/app/microservices/microservices.component.html @@ -0,0 +1,185 @@ +<!-- + # ============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 style="margin: 0px 0px 10px 20px; width: 97%; min-width: 900px; border: 1px solid darkslategray"> + <!--Table of base microservices--> + <p-table #dt *ngIf="loadTable" [columns]="cols" [value]="msElements" sortMode="multiple" [paginator]="true" + [rows]="18" [rowsPerPageOptions]="[10,12,14,16,18,20,25,50]" (onFilter)="onTableFiltered(dt.filteredValue)" dataKey="id"> + + <!--Top caption row--> + <ng-template pTemplate="caption"> + + <div style="margin-left: -5%; width: 90%; max-height: 25px; display: inline-flex;"> + <!--Microservices Table Header--> + <div style="float: left;"> + <!--Refresh--> + <i class="fa fa-refresh" (click)="getAllMs()"></i> + <!--Global Filter--> + <input type="text" pInputText size="50" placeholder="Global Filter" + (input)="dt.filterGlobal($event.target.value, 'contains')" + style="width: 250px; height:25px; font-size: 12px; margin-left: 15px"> + <i class="fa fa-search" style="margin:4px 0px 0 8px"></i> + </div> + + <h4 style="margin-left: 15%"><b>Microservices</b></h4> + + </div> + + <div style="float: right;"> + <button pButton type="button" (click)="showAddChangeDialog()" matTooltip="Add Microservice" matTooltipPosition="above" + style="border-radius: 5px; width: 65px; height: 27px; font-size: 14px; border: none; display: inline-flex;"> + <i class="pi pi-plus" style="margin-top: 5px; margin-left: 10px;"></i> + <label style="font-weight: 800; margin-top: 3px">MS</label> + </button> + </div> + + </ng-template> + + <!--Header row with dynamic column names. Columns include microservice Name, Type, Location and Namespace--> + <ng-template pTemplate="header" let-columns> + <tr style="text-align: center"> + <th style="width: 3em"></th> + <th class="ui-state-highlight" *ngFor="let col of columns" [pSortableColumn]="col.field" style="font-size: 12px; outline: none; vertical-align: bottom;" [ngStyle]="{'width': col.width}"> + {{col.header}}<br> + <p-sortIcon [field]="col.field"></p-sortIcon> + </th> + <th style="font-size: 13px; width: 6.5%; vertical-align: top;"> + Actions + </th> + </tr> + + <!--Second header row for individual column filters--> + <tr style="text-align: center;"> + <th style="width: 3em"></th> + <th *ngFor="let col of columns" [ngSwitch]="col.field"> + <input pInputText type="text" + (input)="dt.filter($event.target.value, col.field, 'contains')" style="width: 100%; height: 20px; font-size: 10px;" + placeholder="Filter"> + </th> + <th></th> + </tr> + </ng-template> + + <!--dynamic rows generated from columns object and msElems object--> + <ng-template pTemplate="body" let-rowData let-expanded="expanded" let-msElem> + <tr style="font-size: 12px;"> + <!--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: fit-content; width: -moz-max-content; padding: 0px 5px 0px 5px; border-radius: 3px; font-weight: 600;" + [ngClass]="{'greenStatus' : msElem[col.field] === 'ACTIVE', + 'greyStatus' : msElem[col.field] === 'INACTIVE'}"> + {{msElem[col.field]}} + </div> + <div *ngIf="col.field!=='status'">{{msElem[col.field]}}</div> + </td> + + <!--Actions Column--> + <td> + <div style="text-align: center;"> + <button pButton type="button" style="background-color: transparent; border: none; width: 20px; height: 20px; vertical-align: middle;" class="ui-button-secondary" [matMenuTriggerFor]="menu"> + <i class="pi pi-ellipsis-h" style="color: grey;"></i> + </button> + <mat-menu #menu="matMenu" xPosition="before"> + + <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-plus" + style="font-size: 10px;"></i> Add</span> + </div> + + <button mat-menu-item class="table_action_item" (click)="showAddChangeMsInstanceDialog(rowData)">Add MS + Instance...</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-pencil"></i> Update</span> + </div> + + <button mat-menu-item class="table_action_item" (click)="showAddChangeDialog(rowData)">Update Microservice...</button> + </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}} + </div> + <!-- Notes --> + <div class="row-expand-card" style="background-color: rgba(100, 148, 237, 0.219)"> + <b>Notes:</b><br> + <p-scrollPanel [style]="{width: '100%', height: '62px'}"> + <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.199)"> + <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> + </div> + </td> + </tr> + </ng-template> + </p-table> + + <!--download buttons for exporting table to either csv or excel file--> + <div *ngIf="loadTable" style="margin-left: 10px; margin-top: -32px; float: left;"> + <button pButton type="button" (click)="exportTable('csv')" + matTooltip="Export Table to CSV" matTooltipPosition="above" + style="border-radius: 5px; width: 65px; height: 22px; font-size: 14px; border: none; margin-top: 4px; display: inline-flex;"> + <i class="pi pi-file" style="margin-top: 2px; margin-left: 8px;"></i> + <label style="font-weight: 800; vertical-align: middle;">CSV</label> + </button> + <button pButton type="button" (click)="exportTable('excel')" + matTooltip="Export Table to XLSX" matTooltipPosition="above" + style="border-radius: 5px; width: 65px; height: 22px; margin-left: 7px; font-size: 14px; background-color: green; border: none; display: inline-flex;"> + <i class="pi pi-file-excel" style="margin-top: 2px; margin-left: 4px;"></i> + <label style="font-weight: 800; vertical-align: middle;">Excel</label> + </button> + </div> + + <!-- Dialog to Add or Change a MS --> + <app-ms-add-change *ngIf="showMsAddChangeDialog" [visible]="showMsAddChangeDialog" [currentRow]="currentRow" (handler)="addOrChangeMs($event)"></app-ms-add-change> + + <!-- Dialog to Add (or Change) a MS Instance --> + <app-ms-instance-add *ngIf="showAddChangeMsInstance" [visible]="showAddChangeMsInstance" [msName]="addInstanceTo" [currentRow]="currentRow" (handler)="addMsInstance($event)"></app-ms-instance-add> + + <!-- Shared success message --> + <p-toast key="addChangeSuccess"></p-toast> + +</div>
\ No newline at end of file diff --git a/mod2/ui/src/app/microservices/microservices.component.spec.ts b/mod2/ui/src/app/microservices/microservices.component.spec.ts new file mode 100644 index 0000000..2d2e5f1 --- /dev/null +++ b/mod2/ui/src/app/microservices/microservices.component.spec.ts @@ -0,0 +1,134 @@ +/* + * # ============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 } 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 { CalendarModule } from 'primeng/calendar'; +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 { MsAddChangeComponent } from '../ms-add-change/ms-add-change.component'; +import { MsInstanceAddComponent } from '../ms-instance-add/ms-instance-add.component'; + +import { MicroservicesComponent } from './microservices.component'; + +describe('MicroservicesComponent', () => { + let component: MicroservicesComponent; + let fixture: ComponentFixture<MicroservicesComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + MicroservicesComponent, + MsAddChangeComponent, + MsInstanceAddComponent, + ], + imports: [ + Ng4LoadingSpinnerModule, + TableModule, + MatMenuModule, + ScrollPanelModule, + ToastModule, + DialogModule, + DropdownModule, + FormsModule, + ReactiveFormsModule, + ButtonModule, + CalendarModule, + HttpClientTestingModule, + ToastModule, + RouterTestingModule + ], + providers: [ + MessageService, + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MicroservicesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it(`should fill msInstances Object`, () => { + const fixture = TestBed.createComponent(MicroservicesComponent); + const app = fixture.debugElement.componentInstance; + + let mockMicroservice = [{ + id: 'testId1234', + name: 'test-MS', + tag: 'test-MS-tag', + serviceName: 'testServiceName', + type: 'testType', + location: 'TestLocation', + namespace: 'testNameSpace', + status: 'testStatus', + metadata: { + createdBy: 'test', + createdOn: '01-01-2020 12:00', + updatedBy: 'test', + updatedOn: '01-01-2020 12:00', + notes: 'test', + labels: ['test'], + }, + msInstances: 'test' + }] + + app.fillTable(mockMicroservice) + + expect(app.loadTable).toEqual(true); + expect(app.msElements.length).toEqual(1); + }); + + it(`should set addOrChange to "Add"`, () => { + const fixture = TestBed.createComponent(MicroservicesComponent); + const app = fixture.debugElement.componentInstance; + + let mockRowData = null + app.showAddChangeDialog(mockRowData) + + expect(app.addOrChange).toEqual('Add'); + }); + + it(`should set addOrChange to "Change"`, () => { + const fixture = TestBed.createComponent(MicroservicesComponent); + const app = fixture.debugElement.componentInstance; + + let mockRowData = {field: 'test'} + app.showAddChangeDialog(mockRowData) + + expect(app.addOrChange).toEqual('Change'); + }); +}); diff --git a/mod2/ui/src/app/microservices/microservices.component.ts b/mod2/ui/src/app/microservices/microservices.component.ts new file mode 100644 index 0000000..640e750 --- /dev/null +++ b/mod2/ui/src/app/microservices/microservices.component.ts @@ -0,0 +1,311 @@ +/* + * # ============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 } from '@angular/core'; +import { MatTableDataSource } from '@angular/material/table'; +import { Table } from 'primeng/table'; +import { MessageService } from 'primeng/api'; +import { trigger, state, style, transition, animate } from '@angular/animations'; +import { BaseMicroserviceService } from '../services/base-microservice.service'; +import { MsAddService } from '../services/ms-add.service'; +import { MicroserviceInstanceService } from '../services/microservice-instance.service'; +import { AuthService } from '../services/auth.service'; +import { DatePipe } from '@angular/common'; +import { Ng4LoadingSpinnerService } from 'ng4-loading-spinner'; +import { DownloadService } from '../services/download.service'; + +@Component({ + selector: 'app-microservices', + templateUrl: './microservices.component.html', + styleUrls: ['./microservices.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] +}) +export class MicroservicesComponent implements OnInit { + @ViewChild(Table, { static: false }) dt: Table; + + /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ + msElements: any[] = []; + dataSource = new MatTableDataSource<any>(this.msElements); + cols: any[] = [ + { field: 'name', header: 'MS Name' }, + { field: 'tag', header: 'MS Tag' }, + { field: 'serviceName', header: 'Service Short Name'}, + { field: 'type', header: 'Type', width: '11%' }, + { field: 'namespace', header: 'Namespace', width: '15%' }, + { field: 'status', header: 'Status', width: '90px' } + ]; + columns: any[]; + loadTable: boolean; + filteredRows: any; + downloadItems: { label: string; command: () => void; }[]; + showAddChangeMsInstance: boolean; + addInstanceTo: string = ""; + msInstanceStates: { label: string, value: string }[] = [ + { label: 'New', value: 'new' }, + { label: 'In Dev', value: 'in-dev' }, + { label: 'Dev Complete', value: 'dev-complete' }, + { label: 'In Test', value: 'in-test' }, + { label: 'Certified', value: 'certified' }, + { label: 'Prod Deployed', value: 'prod-deployed' } + ] + + // Json to add MS to DB, returned from child + msAddChangeJson: any; + + showMsAddChangeDialog: boolean = false; + currentRow: any; + currentMsRow: string = ""; + addOrChange: string; + username: string; + errorMessage: any; + successMessage: string; + + constructor(private spinnerService: Ng4LoadingSpinnerService, private baseMsService: BaseMicroserviceService, + private msInstanceApi: MicroserviceInstanceService, private messageService: MessageService, + private addChangeMsApi: MsAddService, private authService: AuthService, private datePipe: DatePipe, + private downloadService: DownloadService) { } + + ngOnInit() { + this.username = this.authService.getUser().username; + this.getAllMs(); + } + + getAllMs() { + this.spinnerService.show(); + this.msElements = []; + this.loadTable = false; + + this.baseMsService.getAllBaseMs() + .subscribe((data: any[]) => { + this.fillTable(data) + }) + + this.columns = this.cols.map(col => ({ title: col.header, dataKey: col.field })); + } + + /*checks when table is filtered and stores filtered data in new + object to be downloaded when download button is clicked*/ + onTableFiltered(values) { + if (values !== null) { + this.filteredRows = values; + } else { + this.filteredRows = this.msElements; + } + } + + //download table as excel file + exportTable(exportTo: string) { + let downloadElements: any[] = [] + + //labels array not handled well by excel download so converted them to a single string + for (let row of this.filteredRows) { + let labels; + let notes; + if (exportTo === "excel") { + if (row.metadata.labels !== undefined) { + 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({ + MS_Name: row.name, + MS_Tag: row.tag, + Service_Short_Name: row.serviceName, + Type: row.type, + Location: row.location, + Namespace: row.namespace, + Status: row.status, + Created_By: row.metadata.createdBy, + Created_On: row.metadata.createdOn, + Updated_By: row.metadata.updatedBy, + Updated_On: row.metadata.updatedOn, + Notes: notes, + Labels: labels + }) + } + + let csvHeaders = [] + + if (exportTo === "csv") { + csvHeaders = [ + "MS_Name", + "MS_Tag", + "Service_Short_Name", + "Type", + "Location", + "Namespace", + "Status", + "Created_By", + "Created_On", + "Updated_By", + "Updated_On", + "Notes", + "Labels" + ]; + } + + this.downloadService.exportTableData(exportTo, downloadElements, csvHeaders) + } + + // * * * * * Show the Dialog to Add a MS (<app-ms-add-change> tag in the html) * * * * * + showAddChangeDialog(rowData) { + this.showMsAddChangeDialog = true; + this.currentRow = rowData; + if (this.currentRow) { + this.addOrChange = "Change"; + } else { + this.addOrChange = "Add"; + } + } + + // * * * * * Add or Change a MS * * * * * + // The response includes the entire MS record that was Added or Changed, (along with ID and audit fields). + // When Added, the response is added directly to the table. When Changed, the current record is updated field-by-field. + addOrChangeMs(jsonFromChildDialog) { + if (jsonFromChildDialog) { + this.msAddChangeJson = jsonFromChildDialog; + if (this.addOrChange == "Change") { + this.currentMsRow = this.currentRow['id'] + } + this.addChangeMsApi.addChangeMsToCatalog(this.addOrChange, this.currentMsRow, this.msAddChangeJson).subscribe( + (response: any) => { + if (this.addOrChange == "Add") { + this.msElements.unshift(response); + this.successMessage = "Microservice Added"; + } else { + this.updateCurrentRow(jsonFromChildDialog); + this.successMessage = "Microservice Updated"; + } + this.showMsAddChangeDialog = false; + this.messageService.add({ key: 'addChangeSuccess', severity: 'success', summary: 'Success', detail: this.successMessage, life: 5000 }); + }, + errResponse => { + // for testing only - this.updateCurrentRow(jsonFromChildDialog); + this.messageService.add({ key: 'msAddChangeError', severity: 'error', summary: 'Error', detail: errResponse.error.message, sticky: true }); + } + ) + } + else { + this.showMsAddChangeDialog = false; + }; + } + + updateCurrentRow(jsonFromChildDialog) { + const newRow = JSON.parse(jsonFromChildDialog); + this.currentRow['name'] = newRow['name']; + this.currentRow['serviceName'] = newRow['serviceName']; + this.currentRow['type'] = newRow['type']; + this.currentRow['location'] = newRow['location']; + this.currentRow['namespace'] = newRow['namespace']; + this.currentRow['metadata']['labels'] = newRow['metadata']['labels']; + this.currentRow['metadata']['notes'] = newRow['metadata']['notes']; + } + + /* * * * Show pop up for Adding a new MS Instance * * * */ + showAddChangeMsInstanceDialog(data) { + this.addInstanceTo = data['name'] + this.showAddChangeMsInstance = true + } + + /* * * * Call API to Add a new MS Instance * * * */ + addMsInstance(body) { + if (body === null) { + this.showAddChangeMsInstance = false; + } else { + this.msInstanceApi.addChangeMsInstance("ADD", this.addInstanceTo, body).subscribe( + (data) => { + this.messageService.add({ key: 'addChangeSuccess', severity: 'success', summary: 'Success', detail: "MS Instance Added", life: 5000 }); + this.showAddChangeMsInstance = false; + }, + (errResponse) => { + console.log(errResponse) + this.messageService.add({ key: 'instanceAddChangeError', severity: 'error', summary: 'Error', detail: errResponse.error.message, sticky: true }); + } + ) + } + } + + //fill object with microservice data, to be used to fill table. + //checks if fields are empty and if they are, store 'N/A' as the values + fillTable(data) { + for (let elem of data) { + var tempMsElement: any = { + id: elem.id, + name: elem.name, + tag: elem.tag, + serviceName: elem.serviceName, + type: elem.type, + location: elem.location, + namespace: elem.namespace, + status: elem.status, + metadata: { + 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'), + notes: elem.metadata.notes, + labels: elem.metadata.labels + }, + msInstances: elem.msInstances + } + this.msElements.push(tempMsElement) + } + this.filteredRows = this.msElements + this.loadTable = true; + this.spinnerService.hide(); + } +} + +export interface AddMsInstance{ + name: string, + release: string, + metadata: { + scrumLead: string, + scrumLeadId: string, + systemsEngineer: string, + systemsEngineerId: string, + developer: string; + developerId: string; + pstDueDate: string, + pstDueIteration: string, + eteDueDate: string, + eteDueIteration: string, + labels: string[], + notes: string + } + user: string +} diff --git a/mod2/ui/src/app/models/AuthResponse.ts b/mod2/ui/src/app/models/AuthResponse.ts new file mode 100644 index 0000000..f44104d --- /dev/null +++ b/mod2/ui/src/app/models/AuthResponse.ts @@ -0,0 +1,28 @@ +/* + * # ============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 { Authority } from './Authority.enum'; + +export class AuthResponse { + token?: string; + type?: null; + id?: string; + username?:string; + roles?: Authority[]; + message?:string; +}
\ No newline at end of file diff --git a/mod2/ui/src/app/models/Authority.enum.ts b/mod2/ui/src/app/models/Authority.enum.ts new file mode 100644 index 0000000..cbac582 --- /dev/null +++ b/mod2/ui/src/app/models/Authority.enum.ts @@ -0,0 +1,22 @@ +/* + * # ============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========================================================= + */ + +export enum Authority { + ADMIN = 'ROLE_ADMIN', + USER = 'ROLE_USER' +}
\ No newline at end of file diff --git a/mod2/ui/src/app/models/User.ts b/mod2/ui/src/app/models/User.ts new file mode 100644 index 0000000..3c662d9 --- /dev/null +++ b/mod2/ui/src/app/models/User.ts @@ -0,0 +1,25 @@ +/* + * # ============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========================================================= + */ + +export class User{ + username: string; + fullName?: string; + password?: string; + active?: boolean; + roles?: any[]; +}
\ No newline at end of file diff --git a/mod2/ui/src/app/ms-add-change/ms-add-change.component.css b/mod2/ui/src/app/ms-add-change/ms-add-change.component.css new file mode 100644 index 0000000..f256b2f --- /dev/null +++ b/mod2/ui/src/app/ms-add-change/ms-add-change.component.css @@ -0,0 +1,57 @@ +/* + * # ============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========================================================= + */ + +.input { + padding-top: 10px; +} + +.inputLabel { + font-weight: 600; + margin-left: 20px; + width: 150px; +} + +.inputFieldSm { + width: 200px; + height: 35px; + padding-left: 6px; +} +.inputFieldMed { + width: 300px; + height: 35px; + padding-left: 6px; +} +.inputFieldLg { + width: 400px; + height: 35px; + padding-left: 6px; +} + +.validationMsg { + padding-left: 175px; + color: red; + font-size: 11px; + font-weight: 550; +} + +.validationMsgWarning { + padding-left: 175px; + color: rgb(255, 81, 0); + font-size: 11px; + font-weight: 550; +}
\ No newline at end of file diff --git a/mod2/ui/src/app/ms-add-change/ms-add-change.component.html b/mod2/ui/src/app/ms-add-change/ms-add-change.component.html new file mode 100644 index 0000000..2fd19d7 --- /dev/null +++ b/mod2/ui/src/app/ms-add-change/ms-add-change.component.html @@ -0,0 +1,109 @@ +<!-- + # ============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========================================================= + --> + +<p-dialog *ngIf="visible" [header]="guiHeader" [(visible)]="visible" appendTo="body" [modal]="true" [transitionOptions]="'300ms'" + [closeOnEscape]="false" [closable]="false" [style]="{width: '645px'}" (onHide)="closeDialog()"> + + <!-- "Add / Change MS" Error Message --> + <p-toast key="msAddChangeError" [style]="{width: '500px'}"></p-toast> + + <!-- * * * * * Input fields * * * * * --> + <form [formGroup]="msAddForm" (ngSubmit)="saveMs()" class="bg-faded"> + <!-- * * * MS Name * * * --> + <div class="input"> + <label class="inputLabel">MS Name<span style="color:red">*</span></label> + <input class="inputFieldMed" type="text" pInputText formControlName="name"> + </div> + <!-- * * * MS Tag * * * --> + <div class="input"> + <label class="inputLabel">MS Tag<span style="color:red">*</span></label> + <input *ngIf="!currentRow" class="inputFieldMed" type="text" pInputText formControlName="tag"> + <!-- If Updating (vs ADDing) the data, the "Tag" is Read-Only --> + <input *ngIf="currentRow" class="inputFieldMed" type="text" pInputText formControlName="tag" style="border:none" readonly> + <!-- * * * (Validation Rules Display) * * * --> + <div class="validationMsg" *ngIf="msAddForm.controls['tag'].invalid && + !msAddForm.value['tag']=='' && + msAddForm.controls['tag'].value.length < 51"> + Format: lowercase alphanumeric with embedded dashes (5-50 characters) + </div> + <!-- * * * (Validation Rule - length > 50) * * * --> + <div class="validationMsg" *ngIf="msAddForm.controls['tag'].value.length > 50"> + MS Tag cannot exceed 50 chars + </div> + </div> + <!-- * * * Service Short Name * * * --> + <div class="input"> + <label class="inputLabel">Service Short Name</label> + <input class="inputFieldMed" type="text" pInputText formControlName="serviceName"> + <!-- * * * (Validation Rules Display) * * * --> + <div class="validationMsg" *ngIf="msAddForm.controls['serviceName'].invalid && + !msAddForm.value['serviceName']=='' && + msAddForm.controls['serviceName'].value.length < 26"> + Format: lowercase alpha with embedded dashes + </div> + <!-- * * * (Warning! Global vs Central/Edge Service Name length) * * * --> + <div class="validationMsgWarning" *ngIf="msAddForm.controls['serviceName'].valid && + msAddForm.controls['serviceName'].value.length > 12 && + msAddForm.controls['serviceName'].value.length < 26"> + Warning! Only Global Site short names can exceed 12 chars (max 25) + </div> + <!-- * * * (Validation Rule - length > 25) * * * --> + <div class="validationMsg" *ngIf="msAddForm.controls['serviceName'].value.length > 25"> + Global Site short names cannot exceed 25 chars + </div> + </div> + <!-- * * * Type * * * --> + <div class="input"> + <label class="inputLabel">Type<span style="color:red">*</span></label> + <p-dropdown [options]="types" placeholder="Select Type" optionLabel="type" formControlName="type"></p-dropdown> + </div> + <!-- * * * Location + <div class="input"> + <label class="inputLabel">Location<span style="color:red">*</span></label> + <p-dropdown [options]="locations" placeholder="Select Location" optionLabel="location" formControlName="location"></p-dropdown> + </div> + * * * --> + <!-- * * * Namespace * * * --> + <div class="input"> + <label class="inputLabel">Namespace</label> + <input class="inputFieldMed" type="text" pInputText formControlName="namespace"> + <!-- * * * (Validation Rules Display) * * * --> + <div class="validationMsg" *ngIf="msAddForm.controls['namespace'].invalid && !msAddForm.value['namespace']==''"> + Format: lowercase alphanumeric with embedded dashes + </div> + </div> + <!-- * * * Labels * * * --> + <div class="input"> + <label class="inputLabel">Labels</label> + <input class="inputFieldLg" type="text" pInputText formControlName="labels"> + </div> + <span style="padding: 9px 0px 0px 172px; font-size: 13px;">(Separate labels with a space)</span> + <!-- * * * Notes * * * --> + <div class="input"> + <label class="inputLabel" style="vertical-align: top">Notes</label> + <textarea class="inputFieldLg" [rows]="1" [cols]="30" pInputTextarea autoResize="autoResize" formControlName="notes"></textarea> + </div> + <!-- * * * ADD and CANCEL buttons * * * --> + <div style="float: right; padding: 20px 45px"> + <button pButton type="button" (click)="closeDialog()" style="margin-right: 10px" label="Cancel"></button> + <button pButton type="submit" class="ui-button-success" style="width: 77px; text-align:center" [label]="addOrUpdate" + [disabled]="!msAddForm.valid || !msAddForm.value['name'].trim()"></button> + </div> + </form> + +</p-dialog> diff --git a/mod2/ui/src/app/ms-add-change/ms-add-change.component.spec.ts b/mod2/ui/src/app/ms-add-change/ms-add-change.component.spec.ts new file mode 100644 index 0000000..4ba7ae9 --- /dev/null +++ b/mod2/ui/src/app/ms-add-change/ms-add-change.component.spec.ts @@ -0,0 +1,67 @@ +/* + * # ============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 { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; +import { MessageService } from 'primeng/api'; +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; +import { DropdownModule } from 'primeng/dropdown'; +import { ToastModule } from 'primeng/toast'; + +import { MsAddChangeComponent } from './ms-add-change.component'; + +describe('MsAddChangeComponent', () => { + let component: MsAddChangeComponent; + let fixture: ComponentFixture<MsAddChangeComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [MsAddChangeComponent], + imports: [ + DialogModule, + DropdownModule, + ToastModule, + FormsModule, + ReactiveFormsModule, + ButtonModule, + HttpClientTestingModule, + RouterTestingModule + ], + providers: [ + MessageService, + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MsAddChangeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/ms-add-change/ms-add-change.component.ts b/mod2/ui/src/app/ms-add-change/ms-add-change.component.ts new file mode 100644 index 0000000..6991104 --- /dev/null +++ b/mod2/ui/src/app/ms-add-change/ms-add-change.component.ts @@ -0,0 +1,180 @@ +/* + * # ============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, Input, Output, EventEmitter } from '@angular/core'; +import { InputTextModule } from 'primeng/inputtext'; +import { DropdownModule } from 'primeng/dropdown'; +import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms'; +import { AuthService } from '../services/auth.service'; + +interface Type { + type: string; +} +interface Location { + location: string; +} + +@Component({ + selector: 'app-ms-add-change', + templateUrl: './ms-add-change.component.html', + styleUrls: ['./ms-add-change.component.css'] +}) + +export class MsAddChangeComponent implements OnInit { + + guiHeader: string = "Microservice ADD" + // Used for the Add/Update button label + addOrUpdate: string = "Add"; + + msAddForm: FormGroup; + // The loggged in user + username: string; + + // Input form fields + name: string; + tag: string; + serviceName: string = ""; + type: string; + location: string; + namespace: string; + labels: string; + notes: string; + + // Dropdowns + types: Type[]; + locations: Location[]; + + // Return JSON to parent component + msAddChangeString: any; + msAddChangeJson: any; + + constructor(private fb: FormBuilder, private authService: AuthService) { + } + + @Input() visible: boolean; + @Input() currentRow: any; + @Output() handler: EventEmitter<any> = new EventEmitter(); + + ngOnInit() { + // The logged in user + this.username = this.authService.getUser().username; + + this.msAddForm = new FormGroup({ + name: new FormControl(), + tag: new FormControl(), + serviceName: new FormControl(), + type: new FormControl(), + //location: new FormControl(), + namespace: new FormControl(), + labels: new FormControl(), + notes: new FormControl() + }); + + // FORM fields and validations + this.msAddForm = this.fb.group({ + name: ['', [Validators.required]], + tag: ['', [Validators.required, Validators.pattern('^([a-z0-9](-[a-z0-9])*)+$'), Validators.minLength(5), Validators.maxLength(50)]], + serviceName: ['', [Validators.pattern('^([a-z](-[a-z])*)+$'), Validators.maxLength(25)]], + type: ['', [Validators.required]], + //location: ['', [Validators.required]], + namespace: ['', [Validators.pattern('^([a-z0-9](-[a-z0-9])*)+$')]], + labels: ['', []], + notes: ['', []] + }, + {updateOn: "blur"} + ); + + // TYPE Dropdown + this.types = [ + { type: 'FM_COLLECTOR' }, + { type: 'PM_COLLECTOR' }, + { type: 'ANALYTIC' }, + { type: 'TICK' }, + { type: 'OTHER' } + ]; + // LOCATION Dropdown + this.locations = [ + { location: 'EDGE' }, + { location: 'CENTRAL' }, + { location: 'UNSPECIFIED' } + ]; + + // "Update" was selected, so populate the current row data in the GUI + if (this.currentRow) { + this.guiHeader = "Microservice Update"; + this.addOrUpdate = "Update"; + this.populateFields(); + } + + } + + populateFields() { + let labelsStr: string; + if (this.currentRow['metadata']['labels']) { + labelsStr = this.currentRow['metadata']['labels'].join(' ') + } + + // Prevent validation (length check) from failing in the html + if (this.currentRow['serviceName']) { + this.serviceName = this.currentRow['serviceName'] + } + + this.msAddForm.patchValue({ + name: this.currentRow['name'], + tag: this.currentRow['tag'], + serviceName: this.serviceName, + type: {type: this.currentRow['type']}, + //location: {location: this.currentRow['location']}, + namespace: this.currentRow['namespace'], + labels: labelsStr, + notes: this.currentRow['metadata']['notes'] + }) + } + + // The handler emits 'null' back to parent to close dialog and make it available again when clicked + closeDialog() { + this.visible = false; + this.handler.emit(null); + } + + // Create the JSON to be sent to the parent component + // The "labels" functions below take into account leading/trailing spaces, multiple spaces between labels, and conversion into an array + createOutputJson() { + this.msAddChangeString = { + name: this.msAddForm.value['name'].trim(), + tag: this.msAddForm.value['tag'], + serviceName: this.msAddForm.value['serviceName'], + type: this.msAddForm.value['type'].type, + location: 'UNSPECIFIED', + //location: this.msAddForm.value['location'].location, + namespace: this.msAddForm.value['namespace'].trim(), + metadata: { + labels: this.msAddForm.value['labels'].trim().replace(/\s{2,}/g, ' ').split(" "), + notes: this.msAddForm.value['notes'] + }, + user: this.username + }; + } + + saveMs() { + this.createOutputJson(); + this.msAddChangeJson = JSON.stringify(this.msAddChangeString); + this.handler.emit(this.msAddChangeJson); + } + +} diff --git a/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.css b/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.css new file mode 100644 index 0000000..8ecfc18 --- /dev/null +++ b/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.css @@ -0,0 +1,48 @@ +/* + * # ============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========================================================= + */ + +.input{ + padding-top: 8px; +} + +.inputLabel { + font-weight: 600; + margin-left: 20px; + width: 165px; +} + +.inputFieldXSm { + width: 75px; + height: 30px; + padding-left: 6px; +} +.inputFieldSm { + width: 200px; + height: 30px; + padding-left: 6px; +} +.inputFieldMed { + width: 300px; + height: 30px; + padding-left: 6px; +} +.inputFieldLg { + width: 400px; + height: 30px; + padding-left: 6px; +}
\ No newline at end of file diff --git a/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.html b/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.html new file mode 100644 index 0000000..804de33 --- /dev/null +++ b/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.html @@ -0,0 +1,93 @@ +<!-- + # ============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========================================================= + --> + +<p-dialog [(visible)]="visible" [header]="guiHeader" appendTo="body" [modal]="true" [transitionOptions]="'300ms'" [style]="{width: '720px'}" [baseZIndex]="10000" + [closable]="false" (onHide)="closeDialog()"> + + <!-- "Add / Change MS Instance" Error Message --> + <p-toast key="instanceAddChangeError"></p-toast> + + <form [formGroup]="msInstanceAddForm"> + <!-- * * * Name * * * --> + <div class="input"> + <label class="inputLabel">MS Name</label> + <b>{{msName}}</b> + </div> + <!-- * * * Release * * * --> + <div class="input"> + <label class="inputLabel">Release<span style="color:red">*</span></label> + <p-dropdown [options]="msInstanceReleases" placeholder="Select Release" formControlName="release"></p-dropdown> + </div> + <!-- * * * Scrum Lead / UID * * * --> + <div class="input"> + <label class="inputLabel">Scrum Lead/UID</label> + <input class="inputFieldLg" type="text" pInputText formControlName="scrumLead" /> / <input class="inputFieldXSm" type="text" pInputText formControlName="scrumLeadId" /> + </div> + <!-- * * * Systems Engineer / UID * * * --> + <div class="input"> + <label class="inputLabel">Systems Engineer/UID</label> + <input class="inputFieldLg" type="text" pInputText formControlName="systemsEngineer" /> / <input class="inputFieldXSm" type="text" pInputText formControlName="systemsEngineerId" /> + </div> + <!-- * * * Developer / UID * * * --> + <div class="input"> + <label class="inputLabel">Developer<span style="color:red">*</span>/UID<span style="color:red">*</span></label> + <input class="inputFieldLg" type="text" pInputText formControlName="developer" /> / <input class="inputFieldXSm" type="text" pInputText formControlName="developerId" /> + </div> + <!-- * * * PST Due Date * * * --> + <div class="input"> + <label class="inputLabel">PST Due Date</label> + <p-calendar appendTo="body" [baseZIndex]="10001" dateFormat="yy-mm-dd" formControlName="pstDueDate" [showIcon]="true"></p-calendar> + </div> + <!-- * * * PST Due Iteration * * * --> + <div class="input"> + <label class="inputLabel">PST Due Iteration</label> + <input class="inputFieldSm" type="text" pInputText formControlName="pstDueIteration" /> + </div> + <!-- * * * ETE Due Date * * * --> + <div class="input"> + <label class="inputLabel">ETE Due Date</label> + <p-calendar appendTo="body" [baseZIndex]="10001" dateFormat="yy-mm-dd" formControlName="eteDueDate" [showIcon]="true"></p-calendar> + </div> + <!-- * * * ETE Due Iteration * * * --> + <div class="input"> + <label class="inputLabel">ETE Due Iteration</label> + <input class="inputFieldSm" type="text" pInputText formControlName="eteDueIteration" /> + </div> + <!-- * * * Labels * * * --> + <div class="input"> + <label class="inputLabel">Labels</label> + <input class="inputFieldLg" type="text" pInputText formControlName="labels" /> + </div> + <span style="padding: 0px 0px 0px 188px; font-size: 13px;">(Separate labels with a space)</span> + <!-- * * * Notes * * * --> + <div class="input"> + <label class="inputLabel" style="vertical-align: top">Notes</label> + <textarea class="inputFieldLg" [rows]="1" [cols]="30" pInputTextarea autoResize="autoResize" formControlName="notes"></textarea> + </div> + + <!-- * * * ADD and Cancel buttons * * * --> + <div style="float: right; padding: 10px 25px 5px;"> + <button pButton type="button" (click)="closeDialog()" label="Cancel"></button> + <button pButton type="submit" (click)="submitMsInstance()" class="ui-button-success" [label]="addOrUpdate" style="width: 77px" + [disabled]="!msInstanceAddForm.valid || + !msInstanceAddForm.value['developer'].trim() || + !msInstanceAddForm.value['developerId'].trim()"></button> + </div> + </form> + +</p-dialog>
\ No newline at end of file diff --git a/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.spec.ts b/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.spec.ts new file mode 100644 index 0000000..2318a2c --- /dev/null +++ b/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.spec.ts @@ -0,0 +1,71 @@ +/* + * # ============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 { DatePipe } from '@angular/common'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; +import { MessageService } from 'primeng/api'; +import { ButtonModule } from 'primeng/button'; +import { CalendarModule } from 'primeng/calendar'; +import { DialogModule } from 'primeng/dialog'; +import { DropdownModule } from 'primeng/dropdown'; +import { ToastModule } from 'primeng/toast'; + +import { MsInstanceAddComponent } from './ms-instance-add.component'; + +describe('MsInstanceAddComponent', () => { + let component: MsInstanceAddComponent; + let fixture: ComponentFixture<MsInstanceAddComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [MsInstanceAddComponent], + imports: [ + DialogModule, + DropdownModule, + ToastModule, + FormsModule, + ReactiveFormsModule, + ButtonModule, + HttpClientTestingModule, + RouterTestingModule, + CalendarModule, + ], + providers: [ + MessageService, + DatePipe, + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MsInstanceAddComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.ts b/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.ts new file mode 100644 index 0000000..228ddc1 --- /dev/null +++ b/mod2/ui/src/app/ms-instance-add/ms-instance-add.component.ts @@ -0,0 +1,189 @@ +/* + * # ============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, Input, Output, EventEmitter } from '@angular/core'; +import { MicroserviceInstanceService } from '../services/microservice-instance.service'; +import { MessageService } from 'primeng/api'; +import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms'; +import { AuthService } from '../services/auth.service'; +import { DatePipe } from '@angular/common'; + +@Component({ + selector: 'app-ms-instance-add', + templateUrl: './ms-instance-add.component.html', + styleUrls: ['./ms-instance-add.component.css'] +}) +export class MsInstanceAddComponent implements OnInit { + + guiHeader: string = "Microservice Instance ADD"; + // Used for the Add/Update button label + addOrUpdate: string = "Add"; + + msInstanceAddForm: FormGroup; + msInstanceToAdd: AddMsInstance; + addInstanceTo: string = ""; + username: string; + msInstanceReleases: { label: string, value: string }[] = [ + { label: '2004', value: '2004' }, + { label: '2006', value: '2006' }, + { label: '2008', value: '2008' }, + { label: '2009', value: '2009' }, + { label: '2010', value: '2010' }, + { label: '2011', value: '2011' }, + { label: '2012', value: '2012' } + ] + + @Input() visible: boolean; + @Input() msName: string; + @Input() msInstanceChange: string; // Use to differentiate Add from Change, since currentRow can be problematic + @Input() currentRow: any; + @Output() handler: EventEmitter<any> = new EventEmitter(); + + constructor(private addChangeMsInstanceApi: MicroserviceInstanceService, private messageService: MessageService, private fb: FormBuilder, private authService: AuthService, private datePipe: DatePipe) { } + + ngOnInit() { + this.username = this.authService.getUser().username; + + this.msInstanceAddForm = new FormGroup({ + name: new FormControl(), + release: new FormControl(), + scrumLead: new FormControl(), + scrumLeadId: new FormControl(), + systemsEngineer: new FormControl(), + systemsEngineerId: new FormControl(), + developer: new FormControl(), + developerId: new FormControl(), + status: new FormControl(), + pstDueDate: new FormControl(), + pstDueIteration: new FormControl(), + eteDueDate: new FormControl(), + eteDueIteration: new FormControl(), + labels: new FormControl(), + notes: new FormControl() + }); + + this.msInstanceAddForm = this.fb.group({ + name: ['', []], + release: ['', [Validators.required]], + scrumLead: ['', []], + scrumLeadId: ['', []], + systemsEngineer: ['', []], + systemsEngineerId: ['', []], + developer: ['', [Validators.required]], + developerId: ['', [Validators.required]], + status: ['', []], + pstDueDate: ['', []], + pstDueIteration: ['', []], + eteDueDate: ['', []], + eteDueIteration: ['', []], + labels: ['', []], + notes: ['', []] + }); + + if (this.msInstanceChange) { + this.guiHeader = "Microservice Instance Update"; + this.addOrUpdate = "Update"; + this.populateFields(); + } + } + + populateFields() { + this.msName = this.currentRow['name']; + + let labelsStr: string; + if (this.currentRow['metadata']['labels']) { + labelsStr = this.currentRow['metadata']['labels'].join(' ') + } + + this.msInstanceAddForm.patchValue({ + release: this.currentRow['release'], + scrumLead: this.currentRow['metadata']['scrumLead'], + scrumLeadId: this.currentRow['metadata']['scrumLeadId'], + systemsEngineer: this.currentRow['metadata']['systemsEngineer'], + systemsEngineerId: this.currentRow['metadata']['systemsEngineerId'], + developer: this.currentRow['metadata']['developer'], + developerId: this.currentRow['metadata']['developerId'], + pstDueDate: this.currentRow['pstDueDate'], + pstDueIteration: this.currentRow['pstDueIteration'], + eteDueDate: this.currentRow['eteDueDate'], + eteDueIteration: this.currentRow['eteDueIteration'], + labels: labelsStr, + notes: this.currentRow['metadata']['notes'] + }) +} + + /* * * * On click of cancel * * * */ + closeDialog() { + this.visible = false; + this.handler.emit(null) + } + + /* * * * On click of add * * * */ + submitMsInstance() { + // Prevent error on "split" if record existed before "labels" were implemented + let labels: string[] = [] + if (!this.msInstanceAddForm.value['labels']){ + labels = [] + } else { + labels = this.msInstanceAddForm.value['labels'].trim().replace(/\s{2,}/g, ' ').split(" ") + } + + //build request body + this.msInstanceToAdd = { + name: this.msName, + release: this.msInstanceAddForm.value['release'], + metadata: { + scrumLead: this.msInstanceAddForm.value['scrumLead'], + scrumLeadId: this.msInstanceAddForm.value['scrumLeadId'], + systemsEngineer: this.msInstanceAddForm.value['systemsEngineer'], + systemsEngineerId: this.msInstanceAddForm.value['systemsEngineerId'], + developer: this.msInstanceAddForm.value['developer'], + developerId: this.msInstanceAddForm.value['developerId'], + pstDueDate: this.msInstanceAddForm.value['pstDueDate'], + pstDueIteration: this.msInstanceAddForm.value['pstDueIteration'], + eteDueDate: this.msInstanceAddForm.value['eteDueDate'], + eteDueIteration: this.msInstanceAddForm.value['eteDueIteration'], + labels: labels, + notes: this.msInstanceAddForm.value['notes'] + }, + user: this.username + } + + this.handler.emit(this.msInstanceToAdd) //return request body back to parent + } +} + +export interface AddMsInstance { + name: string, + release: string, + metadata: { + scrumLead: string, + scrumLeadId: string, + systemsEngineer: string, + systemsEngineerId: string, + developer: string, + developerId: string, + pstDueDate: any, + pstDueIteration: string, + eteDueDate: any, + eteDueIteration: string, + labels: string[], + notes: string + } + user: string +}
\ No newline at end of file diff --git a/mod2/ui/src/app/msInstances/msInstances.component.css b/mod2/ui/src/app/msInstances/msInstances.component.css new file mode 100644 index 0000000..62c5ac8 --- /dev/null +++ b/mod2/ui/src/app/msInstances/msInstances.component.css @@ -0,0 +1,148 @@ +/* + * # ============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{ + text-align: center; + font-size: 12px; +} + +.table_column_filter{ + width: 100%; + height: 20px; + font-size: 10px; +} + +.table_div{ + margin: 0px 50px 10px 20px; + min-width: 980px; + width: 98%; + border: 1px solid darkslategray; +} + +.fa-refresh{ + cursor: pointer; +} + +textarea +{ + font-size: 12px; +} + +.row-expand-layout{ + display: grid; + grid-template-columns: 17% 30% 30% auto; + grid-gap: 10px; + grid-auto-rows: minmax(100px, auto); +} + +.row-expand-card{ + font-size: 12px; + border-radius: 5px; + border: 1px solid slategray; + padding: 10px; + /* This height prevents vertical scroll bar in Notes */ + height: 92px; + overflow: hidden; + min-width: 195px; +} + +.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-left: 15px; + font-size: 12px; +} + +.table_title{ + margin-left: 15%; +} + +.table_actions_button{ + background-color: transparent; + border: none; + width: 20px; + height: 20px; + vertical-align: middle; +} + +.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.87) +} + +.redStatus{ + background-color: rgba(255, 29, 29, 0.733) +} + +.blueStatus{ + background-color: rgba(0, 183, 255, 0.432) +} + +.greyStatus{ + background-color: rgba(150, 150, 150, 0.432) +} + +.toast-newline { + white-space: pre-line; +} + +.ui-state-highlight { + background-color: #878C94 !important; + color: black !important; +}
\ No newline at end of file diff --git a/mod2/ui/src/app/msInstances/msInstances.component.html b/mod2/ui/src/app/msInstances/msInstances.component.html new file mode 100644 index 0000000..1545de3 --- /dev/null +++ b/mod2/ui/src/app/msInstances/msInstances.component.html @@ -0,0 +1,221 @@ +<!-- + # ============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"> + <p-table #dt *ngIf="loadTable" [columns]="cols" [value]="msInstances" sortMode="multiple" [paginator]="true" + [rows]="18" [rowsPerPageOptions]="[10,12,14,16,18,20,25,50]" selectionMode="multiple" + [(selection)]="selectedMsInstances" (onFilter)="onTableFiltered(dt.filteredValue)" dataKey="id"> + <ng-template pTemplate="caption"> + + <div class="table_caption_header"> + <!--Microservices Table Header--> + <div style="float: left;"> + <!--Refresh--> + <i class="fa fa-refresh" (click)="getAllInstances()"></i> + <!--Global Filter--> + <input type="text" pInputText size="50" placeholder="Global Filter" + (input)="dt.filterGlobal($event.target.value, 'contains')" + class="table_global_filter"> + <i class="fa fa-search" style="margin:4px 0 0 8px"></i> + </div> + + <div class="table_title"> + <h4><b>Microservice Instances</b></h4> + </div> + </div> + + </ng-template> + + <!--column headers--> + <ng-template pTemplate="header" let-columns> + <tr> + <th style="width: 3em"></th> + <th class="ui-state-highlight" *ngFor="let col of columns" style="outline: none; vertical-align: bottom; text-align: center;" [pSortableColumn]="col.field" [ngStyle]="{'width': col.width}"> + {{col.header}}<br> + <p-sortIcon [field]="col.field" style="font-size: 8px;"></p-sortIcon> + </th> + <!--actions column--> + <th style="width: 7%;"> + Actions + </th> + </tr> + <!--Second header row for individual column filters--> + <tr> + <th style="width: 3em"></th> + <th *ngFor="let col of columns" [ngSwitch]="col.field"> + <input 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)="checkCanGenerateBp()" [matMenuTriggerFor]="menu" + 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 style="background-color: rgba(128, 128, 128, 0.25);"> + <span style="font-size: 12px; margin-left: 10px; font-weight: 500;"><i class="pi pi-plus" + style="font-size: 10px;"></i> Add</span> + </div> + + <div matTooltip="{{generateSelectedBPsTooltip}}" [matTooltipDisabled]="canGenerateSelectedBPs" matTooltipPosition="left"> + <button mat-menu-item (click)="generateSelectedBlueprints()" class="table_action_item" [disabled]="!canGenerateSelectedBPs" style="margin-top: 5px;">Generate Selected Blueprints</button> + </div> + </mat-menu> + </div> + </th> + </tr> + </ng-template> + + <!--row data--> + <ng-template pTemplate="body" let-rowData let-expanded="expanded" let-msElem> + <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: fit-content; width: -moz-max-content; padding: 0px 5px 0px 5px; border-radius: 3px; font-weight: 600;" [ngClass]="{ + 'greenStatus' : msElem[col.field] === 'DEV_COMPLETE' || msElem[col.field] === 'CERTIFIED' || msElem[col.field] === 'PROD_DEPLOYED', + 'blueStatus' : msElem[col.field] === 'NEW' || msElem[col.field] === 'IN_DEV' || msElem[col.field] === 'IN_TEST'}"> + {{msElem[col.field]}} + </div> + <div *ngIf="col.field!=='status'">{{msElem[col.field]}}</div> + </td> + + <td> + <div style="text-align: center"> + <p-tableCheckbox [value]="rowData" style="padding-right: 5px;"></p-tableCheckbox> + <button pButton type="button" class="ui-button-secondary" [matMenuTriggerFor]="menu" + 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 style="background-color: rgba(128, 128, 128, 0.25);"> + <span style="font-size: 12px; margin-left: 10px; font-weight: 500;"><i class="pi pi-plus" style="font-size: 10px;"></i> Add</span> + </div> + <!-- * * * * Add component spec * * * * --> + <button mat-menu-item class="table_action_item" (click)="showAddCSDialog(rowData)">Component Spec...</button> + <!-- * * * * Generate Blueprint * * * * --> + <div matTooltip="No Active Component Spec" [matTooltipDisabled]="rowData.activeSpec!==null" matTooltipPosition="left"> + <button mat-menu-item class="table_action_item" (click)="generateBlueprints(rowData)" [disabled]="rowData.activeSpec===null">Generate 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-search" style="font-size: 10px;"></i> View</span> + </div> + <!-- * * * * Go to spec files table * * * * --> + <div matTooltip="No Component Specs" [matTooltipDisabled]="rowData.activeSpec!==null" matTooltipPosition="left"> + <button mat-menu-item class="table_action_item" (click)="viewCompSpecs(rowData)" [disabled]="rowData.activeSpec===null">Component Specs</button> + </div> + <!-- * * * * Go to blueprints table * * * * --> + <button mat-menu-item class="table_action_item" (click)="viewBlueprints(rowData)" [disabled]="rowData.activeSpec===null">Blueprints</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-pencil"></i> Update</span> + </div> + <!-- * * * * Update ms instance record * * * * --> + <button mat-menu-item class="table_action_item" (click)="showAddChangeDialog(rowData)">Update MS Instance...</button> + </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}} + </div> + <!-- People --> + <div class="row-expand-card" style="background-color: rgba(160, 159, 95, 0.295)"> + <b>Scrum Lead: </b>{{rowData.metadata.scrumLead}} + <span *ngIf="rowData.metadata.scrumLeadId"> ({{rowData.metadata.scrumLeadId}})</span><br/> + <b>Systems Engineer: </b>{{rowData.metadata.systemsEngineer}} + <span *ngIf="rowData.metadata.systemsEngineerId"> ({{rowData.metadata.systemsEngineerId}})</span><br/> + <b>Developer: </b>{{rowData.metadata.developer}} + <span *ngIf="rowData.metadata.developerId"> ({{rowData.metadata.developerId}})</span> + </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: '62px'}"> + <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: 5px; border-radius: 3px;">{{label}}</span> + </div> + </div> + </div> + </div> + </td> + </tr> + </ng-template> + + </p-table> + + <!--download buttons for exporting table to either csv or excel file--> + <div *ngIf="loadTable" class="table_export_buttons_alignment"> + <button pButton type="button" class="table_export_button" (click)="exportTable('csv')" matTooltip="Export Table to CSV" matTooltipPosition="above" 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" class="table_export_button" (click)="exportTable('excel')" matTooltip="Export Table to XLSX" 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> + + <!-- Dialog to Change an MS Instance --> + <app-ms-instance-add *ngIf="showAddChangeMsInstance" [visible]="showAddChangeMsInstance" [msName]="msName" [msInstanceChange]="msInstanceChange" [currentRow]="currentRow" (handler)="addChangeMsInstance($event)"></app-ms-instance-add> + + <!-- Dialog to Add a Component Spec --> + <app-comp-spec-add *ngIf="showCsAddDialog" [visible]="showCsAddDialog" (handler)="addNewCs($event)"></app-comp-spec-add> + + <!--Pop-up for "Success" changing MS Instance--> + <p-toast key="changeSuccess"></p-toast> + <!--Pop-up for "Success" adding Component Spec--> + <p-toast key="compSpecAdded" [style]="{width: '400px'}"></p-toast> + <!--Pop-up for "Error" adding Component Spec--> + <p-toast class="toast-newline" key="errorOnCsAdd" [style]="{width: '700px'}"></p-toast> + + <p-toast class="toast-newline" key="csViewError" [style]="{width: '700px'}"></p-toast> + + <p-toast key="bpGenMessage" [style]="{width: '450px'}"></p-toast> + +</div>
\ No newline at end of file diff --git a/mod2/ui/src/app/msInstances/msInstances.component.spec.ts b/mod2/ui/src/app/msInstances/msInstances.component.spec.ts new file mode 100644 index 0000000..5a26b58 --- /dev/null +++ b/mod2/ui/src/app/msInstances/msInstances.component.spec.ts @@ -0,0 +1,127 @@ +/* + * # ============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 { CalendarModule } from 'primeng/calendar'; +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 { CompSpecAddComponent } from '../comp-spec-add/comp-spec-add.component'; +import { MsInstanceAddComponent } from '../ms-instance-add/ms-instance-add.component'; + +import { MsInstancesComponent } from './msInstances.component'; + +describe('MsInstancesComponent', () => { + let component: MsInstancesComponent; + let fixture: ComponentFixture<MsInstancesComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + MsInstancesComponent, + CompSpecAddComponent, + MsInstanceAddComponent + ], + imports: [ + Ng4LoadingSpinnerModule, + TableModule, + MatMenuModule, + ScrollPanelModule, + ToastModule, + DialogModule, + DropdownModule, + FormsModule, + ReactiveFormsModule, + ButtonModule, + CalendarModule, + HttpClientTestingModule, + ToastModule, + RouterTestingModule, + MatTooltipModule + ], + providers: [ + MessageService, + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MsInstancesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it(`should fill msInstances Object`, () => { + const fixture = TestBed.createComponent(MsInstancesComponent); + const app = fixture.debugElement.componentInstance; + + let mockMsInstance = [{ + id: 'testId1234', + name: 'test-MS', + release: '2008', + version: '1.0.0', + status: 'New', + msInfo: { + id: 'testBaseMsId1234', + name: 'test Base Ms', + tag: 'test-MS-tag', + }, + metadata: { + scrumLead: 'test', + scrumLeadId: 'testId', + systemsEngineer: 'test', + systemsEngineerId: 'testId', + developer: 'test', + developerId: 'testId', + pstDueDate: '01-01-2020 12:00', + pstDueIteration: '1.1', + eteDueDate: '01-01-2020 12:00', + eteDueIteration: '1.1', + createdBy: 'test', + createdOn: '01-01-2020 12:00', + updatedBy: 'test', + updatedOn: '01-01-2020 12:00', + notes: 'test', + labels: ['test'], + }, + activeSpec: 'test' + }] + + app.fillTable(mockMsInstance) + + expect(app.loadTable).toEqual(true); + expect(app.msInstances.length).toEqual(1); + }); +}); diff --git a/mod2/ui/src/app/msInstances/msInstances.component.ts b/mod2/ui/src/app/msInstances/msInstances.component.ts new file mode 100644 index 0000000..e011cb9 --- /dev/null +++ b/mod2/ui/src/app/msInstances/msInstances.component.ts @@ -0,0 +1,511 @@ +/* + * # ============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 } from '@angular/core'; +import { MatTableDataSource } from '@angular/material/table'; +import { Table } from 'primeng/table'; +import { MessageService } from 'primeng/api'; +import { trigger, state, style, transition, animate } from '@angular/animations'; +import { MicroserviceInstanceService } from '../services/microservice-instance.service'; +import { DatePipe } from '@angular/common'; +import { DeploymentArtifactService } from '../services/deployment-artifact.service'; +import { CompSpecAddService } from '../services/comp-spec-add.service'; +import { BreadcrumbService } from '../services/breadcrumb.service'; +import { Router } from '@angular/router'; +import { Ng4LoadingSpinnerService } from 'ng4-loading-spinner'; +import { DownloadService } from '../services/download.service'; + +@Component({ + selector: 'app-msInstances', + templateUrl: './msInstances.component.html', + styleUrls: ['./msInstances.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] +}) +export class MsInstancesComponent implements OnInit { + @ViewChild(Table, { static: false }) dt: Table; + @ViewChild('myInput', { static: false }) myInputVariable: ElementRef; + + + /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ + msInstances: msInstance[] = []; + expandedItems: Array<any> = new Array<any>(); + dataSource = new MatTableDataSource<msInstance>(this.msInstances); + cols: any[] = [ + { field: 'baseMsName', header: 'MS Name' }, + { field: 'tag', header: 'MS Tag' }, + { field: 'release', header: 'Release', width: '7%' }, + { field: 'pstDueDate', header: 'PST Date', width: '9%' }, + { field: 'pstDueIteration', header: 'PST Iteration', width: '6.5%' }, + { field: 'eteDueDate', header: 'ETE Date', width: '9%' }, + { field: 'eteDueIteration', header: 'ETE Iteration', width: '6.5%' }, + { field: 'status', header: 'Status', width: '125px' } + ]; + selectedMsInstances: msInstance[] = []; + columns: any[]; + loadTable: boolean; + filteredRows: any; + downloadItems: { label: string; command: () => void; }[]; + showAddChangeMsInstance: boolean; + currentRow: any; + msInstanceChange: string = "change"; + generatedBPs: any[] = []; + canGenerateSelectedBPs: boolean = false; + generateSelectedBPsTooltip: string = ''; + + // Json to add CS (Component Spec) to DB, returned from child + csAddJson: any; + + showCsAddDialog: boolean = false; + + showViewCs: boolean =false; + msInstanceId: string = ''; + errorList: string[]; + + constructor(private spinnerService: Ng4LoadingSpinnerService, private msInstanceApi: MicroserviceInstanceService, + private bpApis: DeploymentArtifactService, private addCsApi: CompSpecAddService, private messageService: MessageService, + private datePipe: DatePipe, private router: Router, private downloadService: DownloadService, private bread: BreadcrumbService) { } + + ngOnInit() { + + this.getAllInstances(); + + } + + getAllInstances() { + this.spinnerService.show(); + this.msInstances = []; + this.loadTable = false; + + this.msInstanceApi.getAllMsInstances() + .subscribe((data: any[]) => { + this.fillTable(data) + }) + + this.columns = this.cols.map(col => ({ title: col.header, dataKey: col.field })); + } + + + // * * * * * Show the Dialog to Change an MS Instance (<app-ms-instance-add> tag in the html) * * * * * + showAddChangeDialog(rowData) { + this.msInstanceId = rowData['id'] + this.showAddChangeMsInstance = true; + this.currentRow = rowData; + } + + /* * * * Call API to Change an MS Instance * * * */ + addChangeMsInstance(jsonFromChildDialog) { + if (jsonFromChildDialog === null) { + this.showAddChangeMsInstance = false; + } else { + this.msInstanceApi.addChangeMsInstance("CHANGE", this.msInstanceId, jsonFromChildDialog).subscribe( + (data) => { + this.updateCurrentRow(data); + this.messageService.add({ key: 'changeSuccess', severity: 'success', summary: 'Success', detail: "MS Instance Updated", life: 5000 }); + this.showAddChangeMsInstance = false; + }, + (errResponse) => { + if (errResponse.error.message) { + this.messageService.add({ key: 'instanceAddChangeError', severity: 'error', summary: 'Error', detail: errResponse.error.message, sticky: true }); + } else { + this.messageService.add({ key: 'instanceAddChangeError', severity: 'error', summary: 'Error', detail: errResponse.error.status, sticky: true }); + } + } + ) + } + } + + updateCurrentRow(responseData) { + const newRow = responseData; + this.currentRow['release'] = newRow['release']; + this.currentRow['metadata']['scrumLead'] = newRow['metadata']['scrumLead']; + this.currentRow['metadata']['scrumLeadId'] = newRow['metadata']['scrumLeadId']; + this.currentRow['metadata']['systemsEngineer'] = newRow['metadata']['systemsEngineer']; + this.currentRow['metadata']['systemsEngineerId'] = newRow['metadata']['systemsEngineerId']; + this.currentRow['metadata']['developer'] = newRow['metadata']['developer']; + this.currentRow['metadata']['developerId'] = newRow['metadata']['developerId']; + this.currentRow['pstDueDate'] = this.datePipe.transform(newRow['metadata']['pstDueDate'], 'yyyy-MM-dd'); + this.currentRow['pstDueIteration'] = newRow['metadata']['pstDueIteration']; + this.currentRow['eteDueDate'] = this.datePipe.transform(newRow['metadata']['eteDueDate'], 'yyyy-MM-dd'); + this.currentRow['eteDueIteration'] = newRow['metadata']['eteDueIteration']; + this.currentRow['metadata']['labels'] = newRow['metadata']['labels']; + this.currentRow['metadata']['notes'] = newRow['metadata']['notes']; + this.currentRow['metadata']['updatedBy'] = newRow['metadata']['updatedBy']; + this.currentRow['metadata']['updatedOn'] = this.datePipe.transform(newRow['metadata']['updatedOn'], 'MM-dd-yyyy HH:mm'); + } + + // * * * * * Show the Dialog to Add a CS (in the html) * * * * * + // * * * * * Store the MS Instance ID for the URL and the "current row" to update when a CS is saved * * * * * + showAddCSDialog(rowData) { + this.showCsAddDialog = true; + this.msInstanceId = rowData['id']; + this.currentRow = rowData; + } + + // * * * * * Add a CS * * * * * + addNewCs(jsonFromChildDialog) { + let compSpecAddMessage = ''; + if (jsonFromChildDialog) { + this.csAddJson = jsonFromChildDialog; + if((JSON.parse(this.csAddJson)).policyJson === null){ + compSpecAddMessage = 'Component Spec Added'; + } else { + console.log("here") + compSpecAddMessage = 'Component Spec and Policy added ' + } + + this.addCsApi.addCsToCatalog(this.msInstanceId, this.csAddJson).subscribe( + (response: any) => { + this.messageService.add({ key: 'compSpecAdded', severity: 'success', summary: 'Success', detail: compSpecAddMessage, life: 5000 }); + this.showCsAddDialog = false; + this.currentRow['activeSpec'] = true; + }, + errResponse => { + if (errResponse.error.errors) { + this.messageService.add({ key: 'errorOnCsAdd', severity: 'error', summary: errResponse.error.message, detail: errResponse.error.errors.join('\n'), sticky: true}); + } else { + let summary = errResponse.error.status + " - " + errResponse.error.error; + this.messageService.add({ key: 'errorOnCsAdd', severity: 'error', summary: summary, detail: errResponse.error.message, sticky: true}); + } + }); + } else { + this.showCsAddDialog = false + }; + } + +/* * * * View Component Specs +msName: string; +msRelease: string; + showViewCsDialog(data){ + this.msInstanceId = data['id'] + this.msName = data['name'] + this.msRelease = data['release'] + this.showViewCs = true; + } + csView(data){ + if(data===null){ + this.showViewCs = false + } else { + this.showViewCs = false + this.messageService.add({ key: 'csViewError', severity: 'error', summary: 'Error Message', detail: data.error.message, sticky: true }); + } + } + * * * */ + + /* * * * Generate single blueprint * * * */ + generateBlueprints(data){ + this.bpApis.postBlueprint(data['id']).subscribe((response) => { + this.messageService.add({ key: 'bpGenMessage', severity: 'success', summary: 'Success Message', detail: 'Blueprint Generated', life: 5000 }); + }, (errResponse) => { + this.messageService.add({ key: 'bpGenMessage', severity: 'error', summary: 'Error Message', detail: errResponse.error.message, life: 15000 }); + }) + } + + /* * * * Check if generate selected blueprints button should be disabled and set tooltip message * * * */ + checkCanGenerateBp() { + if (this.selectedMsInstances.length > 0) { + + let noActiveSpecs: string[] = []; + let checkReleases: boolean = true; + let firstRelease = this.selectedMsInstances[0]['release']; + for (let elem of this.selectedMsInstances) { + if (elem.release !== firstRelease){ + checkReleases = false + this.canGenerateSelectedBPs = false + this.generateSelectedBPsTooltip = 'Cannot Generate Blueprints For Different Releases' + break + } + if (elem.activeSpec === null) { + noActiveSpecs.push(elem.name) + this.generateSelectedBPsTooltip += elem.name + } + } + + if (noActiveSpecs.length < 1 && checkReleases) { + this.canGenerateSelectedBPs = true + } else if (noActiveSpecs.length > 0 && checkReleases){ + this.canGenerateSelectedBPs = false + this.generateSelectedBPsTooltip = 'No Active Specs For : ' + let i: number = 1; + for (let elem of noActiveSpecs) { + if (i === noActiveSpecs.length) { + this.generateSelectedBPsTooltip += '{' + elem + '}' + } else { + this.generateSelectedBPsTooltip += '{' + elem + '}, ' + } + i++ + } + } + } else { + this.canGenerateSelectedBPs = false + this.generateSelectedBPsTooltip = "No Instances Selected" + } + } + + /* * * * Generate multiple blueprint * * * */ + successfulBpGens: number; + generateSelectedBlueprints(){ + this.successfulBpGens = 0; + this.count = 0; + this.selectionLength = this.selectedMsInstances.length; + + this.spinnerService.show(); + + (async () => { + for (let instance of this.selectedMsInstances) { + this.bpApis.postBlueprint(instance.id).subscribe((response) => { + this.bpGenSuccess() + }, (errResponse) => { + this.bpGenError(errResponse.error.message) + }) + await timeout(1500); + } + })(); + + function timeout(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + this.selectedMsInstances = [] + } + + /* * * * For BP Gen Successes * * * */ + selectionLength: number; + count: number; + bpGenSuccess(){ + this.successfulBpGens++; + this.count++; + if (this.count === this.selectionLength){ + if(this.successfulBpGens > 0){ + this.messageService.add({ key: 'bpGenMessage', severity: 'success', summary: 'Success Message', detail: 'Blueprints Generated', life: 5000 }); + this.spinnerService.hide() + } + } + } + + /* * * * For BP Gen Errors * * * */ + bpGenError(err){ + this.count++; + if (this.count === this.selectionLength) { + if (this.successfulBpGens > 0) { + this.messageService.add({ key: 'bpGenMessage', severity: 'success', summary: 'Success Message', detail: 'Blueprints Generated', life: 5000 }); + this.spinnerService.hide() + } + } + this.messageService.add({ key: 'bpGenMessage', severity: 'error', summary: 'Error Message', detail: err, life: 15000 }); + } + + /* * * * View the Blueprints for the selected MS Instance * * * */ + viewBlueprints(rowData) { + this.router.navigate(["blueprints"], {queryParams:{tag: rowData['tag'], release:rowData['release'] }}); + this.bread.setBreadcrumbs("Blueprints", "add"); + } + + /* * * * View the Component Spec for the selected MS Instance * * * */ + viewCompSpecs(rowData) { + this.router.navigate(["CompSpecs"], { queryParams: { instanceId: rowData['id'] }}); + this.bread.setBreadcrumbs("Component Specs", "add"); + } + + /* * * * Stores filtered data in new array * * * */ + onTableFiltered(values) { + if (values) { this.filteredRows = values; } + else { this.filteredRows = this.msInstances; } + } + + /* * * * Export ms instance table to excel or csv * * * */ + exportTable(exportTo) { + let downloadElements: any[] = [] + + //labels array not handled well by excel download so converted them to a single string + for(let row of this.filteredRows){ + let labels; + let notes; + if(exportTo === "excel"){ + if(row.metadata.labels !== undefined){ + 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({ + MS_Name: row.name, + MS_Tag: row.tag, + Release: row.release, + PST_Due_Date: row.pstDueDate, + PST_Due_Iteration: row.pstDueIteration, + ETE_Due_Date: row.eteDueDate, + ETE_Due_Iteration: row.eteDueIteration, + Status: row.status, + Created_By: row.metadata.createdBy, + Created_On: row.metadata.createdOn, + Updated_By: row.metadata.updatedBy, + Updated_On: row.metadata.updatedOn, + Scrum_Lead: row.metadata.scrumLead, + Scrum_Lead_Id: row.metadata.scrumLeadId, + Systems_Engineer: row.metadata.systemsEngineer, + Systems_Engineer_Id: row.metadata.systemsEngineerId, + Developer: row.metadata.developer, + Developer_Id: row.metadata.developerId, + Notes: notes, + Labels: labels + }) + } + + let csvHeaders = []; + + if (exportTo === "csv") { + csvHeaders = [ + "MS_Name", + "MS_Tag", + "Release", + "PST_Due_Date", + "PST_Due_Iteration", + "ETE_Due_Date", + "ETE_Due_Iteration", + "Status", + "Created_By", + "Created_On", + "Updated_By", + "Updated_On", + "Scrum_Lead", + "Scrum_Lead_Id", + "Systems_Engineer", + "Systems_Engineer_Id", + "Developer", + "Developer_Id", + "Notes", + "Labels" + ]; + } + + this.downloadService.exportTableData(exportTo, downloadElements, csvHeaders) + } + + /* * * * Fill ms instance table * * * */ + fillTable(data) { + + for (let elem of data) { + + /* * * Now storing as dates (not strings) on DB, so need to convert old data (mm-dd-yyyy and m-d-yyyy) * * */ + let pstDueDate: any; + if (elem.metadata.pstDueDate && (elem.metadata.pstDueDate.length <= 11 && + elem.metadata.pstDueDate.length > 7)) { + pstDueDate = new Date(elem.metadata.pstDueDate.replace(/-/g, '/')) // dash is invalid date format, FF fails + pstDueDate = this.datePipe.transform(pstDueDate, 'yyyy-MM-dd') + } else if (elem.metadata.pstDueDate) { + pstDueDate = this.datePipe.transform(elem.metadata.pstDueDate, 'yyyy-MM-dd') + } else { + pstDueDate = elem.metadata.pstDueDate + } + + let eteDueDate: any; + if (elem.metadata.eteDueDate && (elem.metadata.eteDueDate.length <= 11 && + elem.metadata.eteDueDate.length > 7)) { + eteDueDate = new Date(elem.metadata.eteDueDate.replace(/-/g, '/')) // dash is invalid date format, FF fails + eteDueDate = this.datePipe.transform(eteDueDate, 'yyyy-MM-dd') + } else if (elem.metadata.eteDueDate) { + eteDueDate = this.datePipe.transform(elem.metadata.eteDueDate, 'yyyy-MM-dd') + } else { + eteDueDate = elem.metadata.eteDueDate + } + + var tempElem: msInstance = { + id: elem.id, + name: elem.name, + tag: elem.msInfo.tag, + release: elem.release, + version: elem.version, + status: elem.status, + baseMsId: elem.msInfo.id, + baseMsName: elem.msInfo.name, + metadata: { + scrumLead: elem.metadata.scrumLead, + scrumLeadId: elem.metadata.scrumLeadId, + systemsEngineer: elem.metadata.systemsEngineer, + systemsEngineerId: elem.metadata.systemsEngineerId, + developer: elem.metadata.developer, + developerId: elem.metadata.developerId, + 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'), + notes: elem.metadata.notes, + labels: elem.metadata.labels, + }, + pstDueDate: pstDueDate, + pstDueIteration: elem.metadata.pstDueIteration, + eteDueDate: eteDueDate, + eteDueIteration: elem.metadata.eteDueIteration, + activeSpec: elem.activeSpec + } + this.msInstances.push(tempElem) + } + + this.filteredRows = this.msInstances + this.loadTable = true; + this.spinnerService.hide() + } +} + +export interface msInstance{ + id: string, + name: string, + tag: string, + release: string, + version: string, + status: string, + baseMsId: string, + baseMsName: string , + metadata: { + scrumLead: string, + scrumLeadId: string, + systemsEngineer: string, + systemsEngineerId: string, + developer: string, + developerId: string, + createdBy: string, + createdOn: string, + updatedBy: string, + updatedOn: string, + notes: string, + labels: string[], + } + pstDueDate: string, + pstDueIteration: string, + eteDueDate: string, + eteDueIteration: string, + activeSpec: any +}
\ No newline at end of file diff --git a/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.css b/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.css new file mode 100644 index 0000000..1130851 --- /dev/null +++ b/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.css @@ -0,0 +1,27 @@ +/* + * # ============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========================================================= + */ + +iframe{ + padding-top: 0px; + border: none; +} + +#content{ + position:absolute; + left: 0; right: 0; bottom: 0; top: 0px; +}
\ No newline at end of file diff --git a/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.html b/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.html new file mode 100644 index 0000000..417ee39 --- /dev/null +++ b/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.html @@ -0,0 +1,23 @@ +<!-- + # ============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 id=loadingSpinner [timeout]="60000"></ng4-loading-spinner> +<div id="content"> + <iframe width="100%" height="98.5%" [src]="video | safe" onload="document.getElementById('loadingSpinner').style.display='none';"></iframe> +</div> + diff --git a/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.spec.ts b/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.spec.ts new file mode 100644 index 0000000..d88c06d --- /dev/null +++ b/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.spec.ts @@ -0,0 +1,48 @@ +/* + * # ============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 { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Ng4LoadingSpinnerModule } from 'ng4-loading-spinner'; +import { SafePipe } from '../onboarding-tools/onboarding-tools.component'; + +import { OnboardingToolsComponent } from './onboarding-tools.component'; + +describe('OnboardingToolsComponent', () => { + let component: OnboardingToolsComponent; + let fixture: ComponentFixture<OnboardingToolsComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [OnboardingToolsComponent, SafePipe], + imports: [ + Ng4LoadingSpinnerModule, + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(OnboardingToolsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.ts b/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.ts new file mode 100644 index 0000000..d400120 --- /dev/null +++ b/mod2/ui/src/app/onboarding-tools/onboarding-tools.component.ts @@ -0,0 +1,52 @@ +/* + * # ============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, ViewEncapsulation, ViewChild, ElementRef, PipeTransform, Pipe, OnInit } from '@angular/core'; +import { DomSanitizer } from "@angular/platform-browser"; +import { Ng4LoadingSpinnerService } from 'ng4-loading-spinner'; +import { environment } from '../../environments/environment'; + +@Pipe({ name: 'safe' }) +export class SafePipe implements PipeTransform { + constructor(private sanitizer: DomSanitizer) { } + transform(url) { + return this.sanitizer.bypassSecurityTrustResourceUrl(url); + } +} + +@Component({ + selector: 'app-onboarding-tools', + templateUrl: './onboarding-tools.component.html', + styleUrls: ['./onboarding-tools.component.css'] +}) + +export class OnboardingToolsComponent implements OnInit { + + title = 'Onboarding Tools'; + + //video: string = `http://${environment.api_baseURL}:30991/onboarding-toolbox/blueprint-generator` + video: string = 'http://dcae-onboarding-toolbox-fe.ecomp.idns.cip.att.com:30991/onboarding-toolbox/blueprint-generator' + + constructor(private spinnerService: Ng4LoadingSpinnerService) { + this.spinnerService.show(); + } + + ngOnInit() { + } + +} diff --git a/mod2/ui/src/app/register/register.component.css b/mod2/ui/src/app/register/register.component.css new file mode 100644 index 0000000..730a52d --- /dev/null +++ b/mod2/ui/src/app/register/register.component.css @@ -0,0 +1,18 @@ +/* + * # ============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========================================================= + */ + diff --git a/mod2/ui/src/app/register/register.component.html b/mod2/ui/src/app/register/register.component.html new file mode 100644 index 0000000..74f9347 --- /dev/null +++ b/mod2/ui/src/app/register/register.component.html @@ -0,0 +1,62 @@ +<!-- + # ============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========================================================= + --> + +<p-card header="Register a New User" [style]="{margin:'100px 200px 100px 200px'}" class="ui-card"> + <hr class="line-break"> + <form [formGroup]="form" class="p-5 bg-faded" style="margin-left: 250px;margin-bottom: 100px;"> + <div class="ui-g ui-fluid"> + <div class="ui-g-12 ui-md-6"> + <div class="ui-inputgroup" > + <span class="ui-inputgroup-addon"><i class="pi pi-user" style="line-height: 1.25;"></i></span> + <input type="text" pInputText placeholder="ATT UID" formControlName="username" class="ui-inputtext"> + </div> + </div> + </div> + <i *ngIf="form.get('username').errors && form.get('username').errors.minlength" style="width: 140px;margin-left: 20px;font-size: small;color: red;">username should be at least 5 characters</i> + <i *ngIf="form.get('username').errors && form.get('username').errors.maxlength" style="width: 140px;margin-left: 20px;font-size: small;color: red;">username should be at most 10 characters</i> + <div class="ui-g ui-fluid" > + <div class="ui-g-12 ui-md-6"> + <div class="ui-inputgroup" > + <span class="ui-inputgroup-addon"><i class="pi pi-user" style="line-height: 1.25;"></i></span> + <input type="text" pInputText placeholder="Full Name" formControlName="fullName" class="ui-inputtext"> + </div> + </div> + </div> + <div class="ui-g ui-fluid" > + <div class="ui-g-12 ui-md-6"> + <div class="ui-inputgroup"> + <button pButton type="button" icon="pi pi-refresh" class="ui-button-warn" (click)="generateNewPassword()" ></button> + <input type="text" pInputText formControlName="password" placeholder="Generate password" class="ui-inputtext"> + </div> + </div> + </div> + <i *ngIf="form.get('password').errors && form.get('password').errors.minlength" style="width: 140px;margin-left: 20px;font-size: small;color: red;">password should be at least 6 characters</i> + <div class="ui-g ui-fluid" > + <div class="ui-g-12 ui-md-6"> + <div class="ui-inputgroup" > + <span class="ui-inputgroup-addon"><i class="pi pi-users" style="line-height: 1.25; height: 1em;"></i></span> + <p-multiSelect [options]="rolesFromBackend" formControlName="roles" [showToggleAll]="true" [virtualScroll]="true" [filter]="false" [style]="{height:'3em'}" class="ui-multiselect"></p-multiSelect> + </div> + </div> + </div> + <p-footer class="text-left ui-g-12"> + <button pButton type="button" class="ui-button-info" label="Cancel" (click)="cancel()" style="margin-right: .25em"></button> + <button pButton type="submit" class="ui-button-success" label="Register" [disabled]="!form.valid" (click)="submit()"></button> + </p-footer> + </form> +</p-card> diff --git a/mod2/ui/src/app/register/register.component.spec.ts b/mod2/ui/src/app/register/register.component.spec.ts new file mode 100644 index 0000000..d05fcd1 --- /dev/null +++ b/mod2/ui/src/app/register/register.component.spec.ts @@ -0,0 +1,61 @@ +/* + * # ============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 { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; +import { CardModule } from 'primeng/card'; +import { MultiSelectModule } from 'primeng/multiselect'; + +import { RegisterComponent } from './register.component'; + +describe('RegisterComponent', () => { + let component: RegisterComponent; + let fixture: ComponentFixture<RegisterComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [RegisterComponent], + imports: [ + FormsModule, + ReactiveFormsModule, + MultiSelectModule, + CardModule, + HttpClientTestingModule, + RouterTestingModule + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(RegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/register/register.component.ts b/mod2/ui/src/app/register/register.component.ts new file mode 100644 index 0000000..28b0677 --- /dev/null +++ b/mod2/ui/src/app/register/register.component.ts @@ -0,0 +1,99 @@ +/* + * # ============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 } from '@angular/core'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { AuthService } from '../services/auth.service'; +import { Router } from '@angular/router'; +import { User } from '../models/User'; +import { AuthResponse } from '../models/AuthResponse'; +import {SelectItem} from 'primeng/api'; +import { UserService } from '../services/user.service'; + +@Component({ + selector: 'app-register', + templateUrl: './register.component.html', + styleUrls: ['./register.component.css'] +}) +export class RegisterComponent implements OnInit { + + user: User = { + username:'', + fullName:'', + password:'', + roles: [] + }; + form: FormGroup; + roleOptions: SelectItem[]; + rolesFromBackend = []; + + constructor(private fb: FormBuilder, private authService: AuthService, private router: Router, private userService: UserService) { } + + ngOnInit() { + this.form = this.fb.group({ + username: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(10)]], + fullName: ['', [Validators.required]], + password: ['', [Validators.required, Validators.minLength(6)]], + roles:['', [Validators.required]] + }); + // this.roleOptions = [{label:'Admin', value:{name:'ROLE_ADMIN'}}, {label:'User', value:{name:'ROLE_USER'}}, {label:'ScrumLead', value:{name:'ROLE_SCRUMLEAD'}}, {label:'Developer', value:{name:'ROLE_DEVELOPER'}}, {label:'PST', value:{name:'ROLE_PST'}}, {label:'ETE', value:{name:'ROLE_ETE'}}, {label:'Ops', value:{name:'ROLE_OPS'}}]; + this.userService.getRoles().subscribe(res=>{ + Object.values(res).forEach(ele=>{ + this.rolesFromBackend.push({label:ele.substring(5), value:ele}); + }); + }); + } + + generateNewPassword() { + this.form.value.password = ''; + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&'; + var array = new Uint32Array(32); + window.crypto.getRandomValues(array); + for(let i=0;i<32;i++) { + const index = Math.floor(array[i] % chars.length); + this.form.value.password += chars.charAt(index); + } + this.form.patchValue({password: this.form.value.password}); + } + + cancel() { + const result = window.confirm("Are you sure to quit registering current user and go back to user management?"); + if(result === true){ + this.router.navigate(['/users']); + } + } + + submit() { + this.user.fullName = this. form.value.fullName; + this.user.username = this.form.value.username; + this.user.password = this.form.value.password; + this.user.roles = this.form.value.roles; + console.log(this.user.roles); + this.authService.register(this.user) + .subscribe( (res: AuthResponse) => { + alert(res.message); + this.router.navigate(['/users']); + }, (err) => { + console.log(err); + alert(err.error.message); + this.form.reset(); + } + ); + } + +} diff --git a/mod2/ui/src/app/reset-password/reset-password.component.css b/mod2/ui/src/app/reset-password/reset-password.component.css new file mode 100644 index 0000000..730a52d --- /dev/null +++ b/mod2/ui/src/app/reset-password/reset-password.component.css @@ -0,0 +1,18 @@ +/* + * # ============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========================================================= + */ + diff --git a/mod2/ui/src/app/reset-password/reset-password.component.html b/mod2/ui/src/app/reset-password/reset-password.component.html new file mode 100644 index 0000000..748216a --- /dev/null +++ b/mod2/ui/src/app/reset-password/reset-password.component.html @@ -0,0 +1,64 @@ +<!-- + # ============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========================================================= + --> + +<p-card header="Password Reset" [style]="{margin:'100px 300px 500px 300px'}"> + <hr class="line-break"> + <form [formGroup]="form" (ngSubmit)="submit()" class="p-5 bg-faded" style="margin-left: 200px;margin-bottom: 100px;"> + <div class="ui-g ui-fluid" > + <div class="ui-g-12 ui-md-4"> + <div class="ui-inputgroup" > + <span class="ui-inputgroup-addon"><i class="pi pi-user" style="line-height: 1.25;"></i></span> + <input type="text" pInputText placeholder="User id" formControlName="id" > + </div> + </div> + </div> + <div *ngIf="!authService.isAdmin; else admin"> + <div class="ui-g ui-fluid"> + <div class="ui-g-12 ui-md-4"> + <div class="ui-inputgroup" > + <span class="ui-inputgroup-addon"><i class="pi pi-key" style="line-height: 1.25;"></i></span> + <input type="text" pInputText placeholder="New Password" formControlName="newPassword"> + </div> + </div> + </div> + <div class="ui-g ui-fluid"> + <div class="ui-g-12 ui-md-4"> + <div class="ui-inputgroup" > + <span class="ui-inputgroup-addon"><i class="pi pi-key" style="line-height: 1.25;"></i></span> + <input type="text" pInputText placeholder="Confirm Password" formControlName="confirm_newPassword"> + </div> + </div> + </div> + </div> + <ng-template class="ui-g ui-fluid" #admin> + <div class="ui-g-12 ui-md-4"> + <div class="ui-inputgroup"> + <input type="text" pInputText formControlName="password" placeholder="generate password"> + <button pButton type="button" icon="pi pi-refresh" class="ui-button-warn" (click)="generateNewPassword()" ></button> + </div> + </div> + </ng-template> + <p-footer class="text-left ui-g-12"> + <button pButton type="button" class="ui-button-info" label="Cancel" (click)="cancel()" style="margin-right: .25em"></button> + <button pButton type="submit" class="ui-button-success" label="Login" [disabled]="!form.valid"></button> + </p-footer> + <div class="text-left ui-g-12"> + <a routerLink="/register">Not a registered user? Click here to register now!</a> + </div> + </form> + </p-card> diff --git a/mod2/ui/src/app/reset-password/reset-password.component.spec.ts b/mod2/ui/src/app/reset-password/reset-password.component.spec.ts new file mode 100644 index 0000000..a26d90b --- /dev/null +++ b/mod2/ui/src/app/reset-password/reset-password.component.spec.ts @@ -0,0 +1,58 @@ +/* + * # ============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 { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; +import { CardModule } from 'primeng/card'; + +import { ResetPasswordComponent } from './reset-password.component'; + +describe('ResetPasswordComponent', () => { + let component: ResetPasswordComponent; + let fixture: ComponentFixture<ResetPasswordComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ResetPasswordComponent], + imports: [ + FormsModule, + ReactiveFormsModule, + CardModule, + HttpClientTestingModule, + RouterTestingModule + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ResetPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/reset-password/reset-password.component.ts b/mod2/ui/src/app/reset-password/reset-password.component.ts new file mode 100644 index 0000000..b704b40 --- /dev/null +++ b/mod2/ui/src/app/reset-password/reset-password.component.ts @@ -0,0 +1,78 @@ +/* + * # ============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 } from '@angular/core'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { AuthService } from '../services/auth.service'; +import { Router } from '@angular/router'; +import { User } from '../models/User'; + +@Component({ + selector: 'app-reset-password', + templateUrl: './reset-password.component.html', + styleUrls: ['./reset-password.component.css'] +}) +export class ResetPasswordComponent implements OnInit { + + form: FormGroup; + + constructor(private fb: FormBuilder, public authService: AuthService, private router: Router) { } + + ngOnInit() { + this.form = this.fb.group({ + id: ['', [Validators.required]], + password: '', + newPassword: '', + confirm_newPassword: '' + }); + } + + cancel() { + this.form.reset(); + } + + generateNewPassword() { + this.form.value.password = ''; + let p = ''; + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&'; + for(let i=1;i<12;i++) { + const index = Math.floor(Math.random() * chars.length + 1); + this.form.value.password += chars.charAt(index); + } + this.form.patchValue({password: this.form.value.password}); + } + + submit() { + const id = this.form.value.id; + if(!this.authService.isAdmin){ + if(this.form.value.newPassword === this.form.value.confirm_newPassword){ + const password = this.form.value.newPassword; + console.log(id); + console.log(password); // toJane: need to call user API + } else { + alert('Your passwords do not match, please re-confirm!'); + this.form.patchValue({newPassword: '', confirm_newPassword: ''}); + } + } else { + const password = this.form.value.password; + console.log(id); + console.log(password);// toJane: need to call user API + } +} + +} diff --git a/mod2/ui/src/app/services/auth.service.spec.ts b/mod2/ui/src/app/services/auth.service.spec.ts new file mode 100644 index 0000000..eecc055 --- /dev/null +++ b/mod2/ui/src/app/services/auth.service.spec.ts @@ -0,0 +1,43 @@ +/* + * # ============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 { TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ], + imports: [ + HttpClientTestingModule, + RouterTestingModule + ] + }); + }); + it('should be created', () => { + const service: AuthService = TestBed.get(AuthService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/auth.service.ts b/mod2/ui/src/app/services/auth.service.ts new file mode 100644 index 0000000..49bb5f1 --- /dev/null +++ b/mod2/ui/src/app/services/auth.service.ts @@ -0,0 +1,95 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { HttpClient, HttpHandler, HttpHeaders } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/internal/operators'; +import { User } from '../models/User'; +import { JwtHelperService } from '@auth0/angular-jwt'; +import { AuthResponse } from '../models/AuthResponse'; +import { Router } from '@angular/router'; +import { environment } from '../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService{ + + private user: User = { + username: '', + roles:[] + }; + authPass=false; + isAdmin=false; + reLoginMsg = false; + + constructor( + private http:HttpClient, + private jwtHelper: JwtHelperService, + private router: Router + ) { + + } + + register(user: User): Observable<AuthResponse> { + return this.http.post<AuthResponse>(`http://${environment.api_baseURL}:31003/api/auth/signup`, user); + // return this.http.post<AuthResponse>(`http://localhost:8082/api/auth/signup`, user); + + } + + login(user: User): Observable<AuthResponse> { + return this.http.post<AuthResponse>(`http://${environment.api_baseURL}:31003/api/auth/signin`, user) + .pipe( + tap((res: AuthResponse) => { + localStorage.setItem('token', res.token); + this.setUser(); + this.authPass = true; + }) + ); + } + + setUser() { + this.user.username = this.jwtHelper.decodeToken(localStorage.getItem('token')).sub; + this.user.roles = this.jwtHelper.decodeToken(localStorage.getItem('token')).roles; + this.user.fullName = this.jwtHelper.decodeToken(localStorage.getItem('token')).fullName; + + } + + checkLogin(): Observable<boolean>{ + return this.http.post<boolean>(`http://${environment.api_baseURL}:31003/api/auth/validate-token`, null); + } + + getJwt() { + if(localStorage.getItem('token')){ + return localStorage.getItem('token'); + } + return ""; + } + + logout() { + localStorage.removeItem('token'); + this.authPass = false; + this.router.navigate(['/login']); + } + + getUser(): User { + return this.user; + } + +} diff --git a/mod2/ui/src/app/services/base-microservice.service.spec.ts b/mod2/ui/src/app/services/base-microservice.service.spec.ts new file mode 100644 index 0000000..1cef5bd --- /dev/null +++ b/mod2/ui/src/app/services/base-microservice.service.spec.ts @@ -0,0 +1,42 @@ +/* + * # ============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, TestBed } from '@angular/core/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { BaseMicroserviceService } from './base-microservice.service'; + +describe('BaseMicroserviceService', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }).compileComponents(); + })); + + it('should be created', () => { + const service: BaseMicroserviceService = TestBed.get(BaseMicroserviceService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/base-microservice.service.ts b/mod2/ui/src/app/services/base-microservice.service.ts new file mode 100644 index 0000000..a5c64b1 --- /dev/null +++ b/mod2/ui/src/app/services/base-microservice.service.ts @@ -0,0 +1,35 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; +import { HttpClient } from '@angular/common/http'; + +@Injectable({ + providedIn: 'root' +}) +export class BaseMicroserviceService { + + private url: string = `http://${environment.api_baseURL}:31001/api/`; + + constructor(private http: HttpClient) { } + + getAllBaseMs() { + return this.http.get(this.url + "base-microservice"); + } +} diff --git a/mod2/ui/src/app/services/breadcrumb.service.ts b/mod2/ui/src/app/services/breadcrumb.service.ts new file mode 100644 index 0000000..aa647aa --- /dev/null +++ b/mod2/ui/src/app/services/breadcrumb.service.ts @@ -0,0 +1,73 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { Observable, of, Subject, BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) + +export class BreadcrumbService { + + breadcrumbs: any[] = []; + + breadcrumbs$: Subject<any[]> = new BehaviorSubject([]); + + constructor() { + this.setBreadcrumbs('', "reset"); + } + + setBreadcrumbs(component: string, action: string) { + if (action == "reset") { + this.breadcrumbs = []; + this.breadcrumbs.push({ page: 'Home', link: '/home' }); + } + + // If the breadcrumb item is clicked, remove evwrything to the right of the clicked item + if (action == "crumbClicked") { + const pos = this.breadcrumbs.map(function(crumb) { return crumb.page }).indexOf(component); + for (1; this.breadcrumbs.length -1 -pos; 1) { + this.breadcrumbs.pop() + } + } else { // Add the component that was selected + if (component == 'Microservices') { + this.breadcrumbs.push({ page: 'Microservices', link: '/base-microservices' }); + } else if (component == 'MS Instances') { + this.breadcrumbs.push({ page: 'MS Instances', link: '/ms-instances' }); + } else if (component == 'Blueprints') { + this.breadcrumbs.push({ page: 'Blueprints', link: '/blueprints' }); + } else if (component == 'Component Specs') { + this.breadcrumbs.push({ page: 'Component Specs', link: '/CompSpecs' }); + } else if (component == 'User Management') { + this.breadcrumbs.push({ page: 'User Management', link: '/users' }); + } else if (component == 'Onboarding Tools') { + this.breadcrumbs.push({ page: 'Onboarding Tools', link: '/OnboardingTools' }); + } else if (component == 'Spec Validator') { + this.breadcrumbs.push({ page: 'Spec Validator', link: '/spec-validator' }); + } + } + this.notifySubscriber() + } + + + notifySubscriber() { + this.breadcrumbs$.next(this.breadcrumbs); + } +} + diff --git a/mod2/ui/src/app/services/comp-spec-add.service.spec.ts b/mod2/ui/src/app/services/comp-spec-add.service.spec.ts new file mode 100644 index 0000000..f7f5913 --- /dev/null +++ b/mod2/ui/src/app/services/comp-spec-add.service.spec.ts @@ -0,0 +1,42 @@ +/* + * # ============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, TestBed } from '@angular/core/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { CompSpecAddService } from './comp-spec-add.service'; + +describe('CompSpecAddService', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }).compileComponents(); + })); + + it('should be created', () => { + const service: CompSpecAddService = TestBed.get(CompSpecAddService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/comp-spec-add.service.ts b/mod2/ui/src/app/services/comp-spec-add.service.ts new file mode 100644 index 0000000..d269ac4 --- /dev/null +++ b/mod2/ui/src/app/services/comp-spec-add.service.ts @@ -0,0 +1,45 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { environment } from '../../environments/environment'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) + +export class CompSpecAddService { + + private URL: string = `http://${environment.api_baseURL}:31001/api/specification/`; + + constructor(private http: HttpClient) { } + + addCsToCatalog(msInstanceId: string, addCsJson: any): Observable<any> { + let url = this.URL + msInstanceId; + let body = addCsJson; + let headers = new HttpHeaders({ + 'Content-Type': 'application/json' + }); + + let options = {headers:headers} + return this.http.post<any>(url, body, options); + } + +} diff --git a/mod2/ui/src/app/services/comp-specs-service.service.spec.ts b/mod2/ui/src/app/services/comp-specs-service.service.spec.ts new file mode 100644 index 0000000..58cf0a2 --- /dev/null +++ b/mod2/ui/src/app/services/comp-specs-service.service.spec.ts @@ -0,0 +1,43 @@ +/* + * # ============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, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { compSpecsService } from './comp-specs-service.service'; + +describe('CompSpecsServiceService', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + RouterTestingModule + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }).compileComponents(); + })); + it('should be created', () => { + const service: compSpecsService = TestBed.get(compSpecsService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/comp-specs-service.service.ts b/mod2/ui/src/app/services/comp-specs-service.service.ts new file mode 100644 index 0000000..6fcd92c --- /dev/null +++ b/mod2/ui/src/app/services/comp-specs-service.service.ts @@ -0,0 +1,43 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class compSpecsService { + + private baseUrl: string = `http://${environment.api_baseURL}:31001/api/specification/`; + constructor(private http: HttpClient) { + } + + getAllCompSpecs(instanceId) { + let url = this.baseUrl + instanceId + + return this.http.get(url); + } + + post(msId, body){ + let url: string = this.baseUrl + msId + + return this.http.post<any>(url, body) + } +} diff --git a/mod2/ui/src/app/services/deployment-artifact.service.spec.ts b/mod2/ui/src/app/services/deployment-artifact.service.spec.ts new file mode 100644 index 0000000..f392732 --- /dev/null +++ b/mod2/ui/src/app/services/deployment-artifact.service.spec.ts @@ -0,0 +1,43 @@ +/* + * # ============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, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { DeploymentArtifactService } from './deployment-artifact.service'; + +describe('DeploymentArtifactService', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + RouterTestingModule + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }).compileComponents(); + })); + it('should be created', () => { + const service: DeploymentArtifactService = TestBed.get(DeploymentArtifactService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/deployment-artifact.service.ts b/mod2/ui/src/app/services/deployment-artifact.service.ts new file mode 100644 index 0000000..e344bff --- /dev/null +++ b/mod2/ui/src/app/services/deployment-artifact.service.ts @@ -0,0 +1,71 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../environments/environment'; +import { AuthService } from './auth.service'; + +@Injectable({ + providedIn: 'root' +}) +export class DeploymentArtifactService { + + private URL: string = `http://${environment.api_baseURL}:31001/api/deployment-artifact/`; + private username = this.authService.getUser().username; + private url_User = "?user=" + this.username; + + constructor(private http: HttpClient, private authService: AuthService) { } + + getAllBlueprints() { + return this.http.get(this.URL); + } + + getStatuses() { + let url = this.URL + 'statuses' + + return this.http.get(url) + } + + patchBlueprintStatus(update, bpId){ + let url = this.URL + bpId + "?user=" + this.username + let status: string = update + let body = { + status: status + } + + return this.http.patch(url, body) + } + + postBlueprint(instanceID: string){ + let url = '' + + url = this.URL + instanceID + "?user=" + this.username + console.log(url) + + return this.http.post<any>(url, '') + } + + deleteBlueprint(instanceID: string){ + console.log(instanceID) + let url = this.URL + instanceID + this.url_User + + return this.http.delete(url) + } + +} diff --git a/mod2/ui/src/app/services/download.service.spec.ts b/mod2/ui/src/app/services/download.service.spec.ts new file mode 100644 index 0000000..1df7bfd --- /dev/null +++ b/mod2/ui/src/app/services/download.service.spec.ts @@ -0,0 +1,41 @@ +/* + * # ============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, TestBed } from '@angular/core/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { DownloadService } from './download.service'; + +describe('DownloadService', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }).compileComponents(); + })); + it('should be created', () => { + const service: DownloadService = TestBed.get(DownloadService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/download.service.ts b/mod2/ui/src/app/services/download.service.ts new file mode 100644 index 0000000..619eff7 --- /dev/null +++ b/mod2/ui/src/app/services/download.service.ts @@ -0,0 +1,85 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import * as saveAs from 'file-saver'; + +@Injectable({ + providedIn: 'root' +}) +export class DownloadService { + + constructor() { } + + /* * * * Download json file * * * */ + downloadJSON(content, fileName){ + let file = new Blob([JSON.stringify(content)], { type: 'text;charset=utf-8' }); + let name: string = `${fileName}.json` + saveAs(file, name) + } + + /* * * * Export ms instance table to excel or csv * * * */ + exportTableData(exportTo, downloadElements, arrHeader) { + if (exportTo === "excel") { + import("xlsx").then(xlsx => { + const worksheet = xlsx.utils.json_to_sheet(downloadElements); + const workbook = { Sheets: { 'data': worksheet }, SheetNames: ['data'] }; + const excelBuffer: any = xlsx.write(workbook, { bookType: 'xlsx', type: 'array' }); + this.saveAsExcelFile(excelBuffer, "Table_Data"); + }); + } else if (exportTo === "csv") { + let csvData = this.convertToCSV(downloadElements, arrHeader) + var blob = new Blob([csvData], { type: 'text/csv' }) + saveAs(blob, "Table_Data.csv"); + } + } + saveAsExcelFile(buffer: any, fileName: string): void { + import("file-saver").then(FileSaver => { + let EXCEL_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8'; + let EXCEL_EXTENSION = '.xlsx'; + const data: Blob = new Blob([buffer], { + type: EXCEL_TYPE + }); + FileSaver.saveAs(data, fileName + '_export_' + new Date().getTime() + EXCEL_EXTENSION); + }); + } + convertToCSV(objArray, headerList) { + let array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray; + let str = ''; + let row = ''; + for (let index in headerList) { + row += headerList[index] + ','; + } + row = row.slice(0, -1); + str += row + '\r\n'; + for (let i = 0; i < array.length; i++) { + let line = ''; + for (let index in headerList) { + let head = headerList[index]; + if (array[i][head] === null || array[i][head] === undefined) { + line += ',' + } else { + if (head === "Labels" && array[i][head].length > 1) { line += '[' + array[i][head].join('] [') + '],'; } + else { line += array[i][head] + ','; } + } + } + str += line + '\r\n'; + } + return str; + } +} diff --git a/mod2/ui/src/app/services/global-filters.service.spec.ts b/mod2/ui/src/app/services/global-filters.service.spec.ts new file mode 100644 index 0000000..cadfd3f --- /dev/null +++ b/mod2/ui/src/app/services/global-filters.service.spec.ts @@ -0,0 +1,30 @@ +/* + * # ============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 { TestBed } from '@angular/core/testing'; + +import { GlobalFiltersService } from './global-filters.service'; + +describe('GlobalFiltersService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: GlobalFiltersService = TestBed.get(GlobalFiltersService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/global-filters.service.ts b/mod2/ui/src/app/services/global-filters.service.ts new file mode 100644 index 0000000..0937f92 --- /dev/null +++ b/mod2/ui/src/app/services/global-filters.service.ts @@ -0,0 +1,55 @@ +/* + * # ============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 { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class GlobalFiltersService { + + //global filter values + instanceName: string; + instanceTag: string; + release: string; + status: string; + + shouldFilter: boolean = false; + + constructor() { } + + setFilters(filters){ + this.instanceName = filters.instanceName + this.instanceTag = filters.instanceTag + this.release = filters.release + this.status = filters.status + } + + getFilters(){ + let globalFilters = {instanceName: this.instanceName, instanceTag: this.instanceTag, release: this.release, status: this.status} + return globalFilters + } + + checkShouldFilter(){ + return this.shouldFilter + } + + setShouldFilter(){ + this.shouldFilter = !this.shouldFilter + } +} diff --git a/mod2/ui/src/app/services/jwt-interceptor.service.spec.ts b/mod2/ui/src/app/services/jwt-interceptor.service.spec.ts new file mode 100644 index 0000000..6fea9ba --- /dev/null +++ b/mod2/ui/src/app/services/jwt-interceptor.service.spec.ts @@ -0,0 +1,30 @@ +/* + * # ============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 { TestBed } from '@angular/core/testing'; + +import { JwtInterceptorService } from './jwt-interceptor.service'; + +describe('JwtInterceptorService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: JwtInterceptorService = TestBed.get(JwtInterceptorService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/jwt-interceptor.service.ts b/mod2/ui/src/app/services/jwt-interceptor.service.ts new file mode 100644 index 0000000..2647583 --- /dev/null +++ b/mod2/ui/src/app/services/jwt-interceptor.service.ts @@ -0,0 +1,40 @@ +/* + * # ============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 { Injectable, Injector } from '@angular/core'; +import { HttpInterceptor } from '@angular/common/http'; +import { AuthService } from './auth.service'; + +@Injectable({ + providedIn: 'root' +}) +export class JwtInterceptorService implements HttpInterceptor{ + + constructor(private injector: Injector) { } + + intercept(req: import("@angular/common/http").HttpRequest<any>, next: import("@angular/common/http").HttpHandler): import("rxjs").Observable<import("@angular/common/http").HttpEvent<any>> { + let authService = this.injector.get(AuthService); + let jwtReq = req.clone({ + setHeaders: { + Authorization: `Bearer ${authService.getJwt()}` + } + }) + return next.handle(jwtReq); + } + +} diff --git a/mod2/ui/src/app/services/microservice-instance.service.spec.ts b/mod2/ui/src/app/services/microservice-instance.service.spec.ts new file mode 100644 index 0000000..b3c4074 --- /dev/null +++ b/mod2/ui/src/app/services/microservice-instance.service.spec.ts @@ -0,0 +1,41 @@ +/* + * # ============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, TestBed } from '@angular/core/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { MicroserviceInstanceService } from './microservice-instance.service'; + +describe('MicroserviceInstanceService', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }).compileComponents(); + })); + it('should be created', () => { + const service: MicroserviceInstanceService = TestBed.get(MicroserviceInstanceService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/microservice-instance.service.ts b/mod2/ui/src/app/services/microservice-instance.service.ts new file mode 100644 index 0000000..96cce77 --- /dev/null +++ b/mod2/ui/src/app/services/microservice-instance.service.ts @@ -0,0 +1,46 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; +import { HttpClient } from '@angular/common/http'; +import { AddMsInstance } from '../microservices/microservices.component'; + +@Injectable({ + providedIn: 'root' +}) +export class MicroserviceInstanceService { + + private url: string = `http://${environment.api_baseURL}:31001/api/microservice-instance/`; + + constructor(private http: HttpClient) { } + + getAllMsInstances() { + return this.http.get(this.url); + } + + addChangeMsInstance(addOrChange: string, msNameOrId: string, body: AddMsInstance){ + let URL = this.url + msNameOrId + + if (addOrChange == "ADD") { + return this.http.post<any>(URL, body); + } else { + return this.http.patch<any>(URL, body); + } + } +} diff --git a/mod2/ui/src/app/services/ms-add.service.spec.ts b/mod2/ui/src/app/services/ms-add.service.spec.ts new file mode 100644 index 0000000..dc53a56 --- /dev/null +++ b/mod2/ui/src/app/services/ms-add.service.spec.ts @@ -0,0 +1,42 @@ +/* + * # ============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, TestBed } from '@angular/core/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { MsAddService } from './ms-add.service'; + +describe('MsAddService', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }).compileComponents(); + })); + + it('should be created', () => { + const service: MsAddService = TestBed.get(MsAddService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/ms-add.service.ts b/mod2/ui/src/app/services/ms-add.service.ts new file mode 100644 index 0000000..367799d --- /dev/null +++ b/mod2/ui/src/app/services/ms-add.service.ts @@ -0,0 +1,52 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { environment } from '../../environments/environment'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) + + + +export class MsAddService { + + private URL: string = `http://${environment.api_baseURL}:31001/api/base-microservice`; + + constructor(private http: HttpClient) { } + + addChangeMsToCatalog(addOrChange, msID, addChangeMsJson): Observable<any> { + let url = this.URL; + let headers = new HttpHeaders({'Content-Type': 'application/json'}); + let options = {headers:headers}; + let body; + + body = addChangeMsJson; + + if (addOrChange == "Add") { + return this.http.post<any>(url, body, options); + } else { + url = url + "/" + msID; + return this.http.patch<any>(url, body, options); + } + } + +} diff --git a/mod2/ui/src/app/services/spec-validation.service.spec.ts b/mod2/ui/src/app/services/spec-validation.service.spec.ts new file mode 100644 index 0000000..e83f832 --- /dev/null +++ b/mod2/ui/src/app/services/spec-validation.service.spec.ts @@ -0,0 +1,41 @@ +/* + * # ============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, TestBed } from '@angular/core/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { SpecValidationService } from './spec-validation.service'; + +describe('SpecValidationService', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }).compileComponents(); + })); + it('should be created', () => { + const service: SpecValidationService = TestBed.get(SpecValidationService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/spec-validation.service.ts b/mod2/ui/src/app/services/spec-validation.service.ts new file mode 100644 index 0000000..2785efe --- /dev/null +++ b/mod2/ui/src/app/services/spec-validation.service.ts @@ -0,0 +1,53 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { environment } from '../../environments/environment'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; + +@Injectable({ + providedIn: 'root' +}) +export class SpecValidationService { + + private URL: string = 'http://zlecdyh2adcc1s2dokr04.1463c9.dyh2a.tci.att.com:31002/'; + + constructor(private http: HttpClient) { } + + sendSpecFile(specContent, type, release){ + + let url = '' + + if(release === "2007"){ + url = `${this.URL}v6/api/comp_spec_validator/` + } + + let body = { + compspec_content: specContent, + type: type + } + + return this.http.post(url, body) + } + + getSchema(type){ + let url = `${this.URL}v6/api/schema/${type}` + + return this.http.get(url) + } +} diff --git a/mod2/ui/src/app/services/user.service.spec.ts b/mod2/ui/src/app/services/user.service.spec.ts new file mode 100644 index 0000000..979e9fa --- /dev/null +++ b/mod2/ui/src/app/services/user.service.spec.ts @@ -0,0 +1,44 @@ +/* + * # ============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, TestBed } from '@angular/core/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; + +import { UserService } from './user.service'; + +describe('UserService', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }).compileComponents(); + })); + + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: UserService = TestBed.get(UserService); + expect(service).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/services/user.service.ts b/mod2/ui/src/app/services/user.service.ts new file mode 100644 index 0000000..ec1d503 --- /dev/null +++ b/mod2/ui/src/app/services/user.service.ts @@ -0,0 +1,53 @@ +/* + * # ============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 { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { User } from '../models/User'; +import { AuthResponse } from '../models/AuthResponse'; +import { environment } from '../../environments/environment'; + +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + constructor(private http: HttpClient) {} + + getUsers(): Observable<User[]> { + return this.http.get<User[]>(`http://${environment.api_baseURL}:31003/api/users/getAll`); + } + + editUser(username: string, user: User): Observable<any>{ + return this.http.patch<any>(`http://${environment.api_baseURL}:31003/api/users/admin/${username}`, user); + } + + editProfile(username: string, user: User): Observable<any>{ + return this.http.patch<any>(`http://${environment.api_baseURL}:31003/api/users/user/${username}`, user); + } + + deleteUser(username: string): Observable<{message:string}> { + return this.http.delete<{message: string}>(`http://${environment.api_baseURL}:31003/api/users/${username}`); + } + + getRoles() { + return this.http.get(`http://${environment.api_baseURL}:31003/api/roles`); + } + +} diff --git a/mod2/ui/src/app/shared/shared-module.ts b/mod2/ui/src/app/shared/shared-module.ts new file mode 100644 index 0000000..7ab21b6 --- /dev/null +++ b/mod2/ui/src/app/shared/shared-module.ts @@ -0,0 +1,61 @@ +/* + * # ============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 { NgModule } from '@angular/core'; +import {MatSelectModule} from '@angular/material/select'; +import { FormsModule } from '@angular/forms'; +import {MatTableModule} from '@angular/material/table'; +import { MatFormFieldModule, MatInputModule } from '@angular/material'; +import { MatPaginatorModule } from '@angular/material'; +//import { MatSort } from '@angular/material'; +import { MatToolbarModule, MatIconModule, MatSidenavModule, MatListModule, MatButtonModule } from '@angular/material'; + +@NgModule({ + declarations: [ + ], + imports: [ + MatSelectModule, + FormsModule, + MatToolbarModule, + MatIconModule, + MatSidenavModule, + MatListModule, + MatButtonModule, + MatTableModule, + MatPaginatorModule, + MatFormFieldModule, + MatInputModule + //MatSort + + ], + exports: [ + MatSelectModule, + FormsModule, + MatToolbarModule, + MatIconModule, + MatSidenavModule, + MatListModule, + MatButtonModule, + MatTableModule, + MatPaginatorModule, + MatFormFieldModule, + MatInputModule + // MatSort + ] + }) + export class SharedModule { }
\ No newline at end of file diff --git a/mod2/ui/src/app/user-management/user-management.component.css b/mod2/ui/src/app/user-management/user-management.component.css new file mode 100644 index 0000000..495d0f8 --- /dev/null +++ b/mod2/ui/src/app/user-management/user-management.component.css @@ -0,0 +1,54 @@ +/* + * # ============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========================================================= + */ + +.pi-trash:hover { + color:red; +} +.pi-pencil:hover { + color:blue; +} + +label { + cursor: pointer; +} + +.input{ + padding-top: 10px; +} + +.inputLabel { + font-weight: 600; + margin-left: 20px; + width: 140px; +} + +.inputFieldSm { + width: 200px; + height: 35px; + padding-left: 6px; +} +.inputFieldMed { + width: 300px; + height: 35px; + padding-left: 6px; +} +.inputFieldLg { + width: 400px; + height: 35px; + padding-left: 6px; +}
\ No newline at end of file diff --git a/mod2/ui/src/app/user-management/user-management.component.html b/mod2/ui/src/app/user-management/user-management.component.html new file mode 100644 index 0000000..6885fe1 --- /dev/null +++ b/mod2/ui/src/app/user-management/user-management.component.html @@ -0,0 +1,89 @@ +<!-- + # ============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========================================================= + --> + +<div style="margin: 0px 20px 10px 20px"> + <p-table #dt [value]="users" [rowHover]="true"> + <ng-template pTemplate="caption"> + <h5><strong>System User List and Management</strong><i style="font-size: smaller;"> ( for admin only )</i></h5> + <br/> + <a routerLink="/register">Want to register a new user? Click here!</a> + </ng-template> + <ng-template pTemplate="header"> + <tr> + <th style="width: 15%">Username (ATT UID)</th> + <th>Full Name</th> + <th>Roles</th> + <!-- <th>Active Status</th> --> + <th style="width: 15%">Actions</th> + </tr> + </ng-template> + <ng-template pTemplate="body" let-user> + <tr> + <td>{{user.username}}</td> + <td>{{user.fullName}}</td> + <td>{{user.roles}}</td> + <!-- <td>true</td> --> + <td> + <i class="pi pi-trash" (click)="handleDelete(user.username)" pTooltip="delete user" tooltipPosition="right"></i> + <i class="pi pi-pencil" (click)="handleEdit(user)" pTooltip="edit user" tooltipPosition="right" style="margin-left: .5em;"></i> + </td> + </tr> + </ng-template> + </p-table> + + <!--edit user information dialog--> + <p-dialog [(visible)]="editUserFlag" appendTo="body" [modal]="true" [transitionOptions]="'300ms'" [style]="{width: '635px'}" [baseZIndex]="10000" + [closable]="true" (onHide)="closeEditDialog()"> + <p-header style="display: inline-flex;"> + Edit User Information + </p-header> + + <form [formGroup]="editUserForm"> + <!-- * * * Username * * * --> + <div class="input"> + <label class="inputLabel">ATT UID</label> + <b>{{editUser.username}}</b> + </div> + <!-- * * * User Full Name * * * --> + <div class="input"> + <label class="inputLabel">Full Name</label> + <input class="inputFieldSm" type="text" pInputText formControlName="fullName"/> + </div> + <!-- * * * Roles * * * --> + <div class="input"> + <label class="inputLabel">Roles</label> + <p-multiSelect [options]="rolesFromBackend" formControlName="roles" [showToggleAll]="true" [virtualScroll]="true" [filter]="false" [style]="{height:'3.6em', width:'200px'}"></p-multiSelect> + <!-- <b>{{editUser.roles}}</b> --> + </div> + <!-- * * * Re-Generate Password * * * --> + <div class="input"> + <div class="ui-inputgroup"> + <label class="inputLabel">New Password</label> + <button pButton type="button" icon="pi pi-refresh" class="ui-button-warn" (click)="generateNewPassword()" ></button> + <input type="text" pInputText formControlName="password" placeholder="Generate password" class="ui-inputtext" pTooltip="Password should be greater than 5 characters" tooltipPosition="right"> + </div> + </div> + <i *ngIf="editUserForm.get('password').errors && editUserForm.get('password').errors.minlength" style="width: 140px;margin-left: 20px;font-size: small;color: red;">password should be at least 6 characters</i> + <!-- * * * Submit and Cancel buttons * * * --> + <div style="margin-top: 2em; margin-left: 1.3em; margin-bottom: 2em;"> + <button pButton type="button" (click)="closeEditDialog()" label="Cancel"></button> + <button pButton type="submit" (click)="submitEdit(editUser)" class="ui-button-success" label="Submit" style="width: 70px"></button> + </div> + </form> +</p-dialog> +</div>
\ No newline at end of file diff --git a/mod2/ui/src/app/user-management/user-management.component.spec.ts b/mod2/ui/src/app/user-management/user-management.component.spec.ts new file mode 100644 index 0000000..2cb6a6e --- /dev/null +++ b/mod2/ui/src/app/user-management/user-management.component.spec.ts @@ -0,0 +1,63 @@ +/* + * # ============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 { RouterTestingModule } from '@angular/router/testing'; +import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt'; +import { DialogModule } from 'primeng/dialog'; +import { MultiSelectModule } from 'primeng/multiselect'; +import { TableModule } from 'primeng/table'; + +import { UserManagementComponent } from './user-management.component'; + +describe('UserManagementComponent', () => { + let component: UserManagementComponent; + let fixture: ComponentFixture<UserManagementComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [UserManagementComponent], + imports: [ + TableModule, + DialogModule, + MultiSelectModule, + FormsModule, + ReactiveFormsModule, + HttpClientTestingModule, + RouterTestingModule + ], + providers: [ + { provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, + JwtHelperService + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserManagementComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/mod2/ui/src/app/user-management/user-management.component.ts b/mod2/ui/src/app/user-management/user-management.component.ts new file mode 100644 index 0000000..b7ee6e7 --- /dev/null +++ b/mod2/ui/src/app/user-management/user-management.component.ts @@ -0,0 +1,149 @@ +/* + * # ============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, ChangeDetectionStrategy } from '@angular/core'; +import { User } from '../models/User'; +import { UserService } from '../services/user.service'; +import { Router } from '@angular/router'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { SelectItem } from 'primeng/api'; +import { AuthService } from '../services/auth.service'; + +@Component({ + selector: 'app-user-management', + templateUrl: './user-management.component.html', + styleUrls: ['./user-management.component.css'] +}) +export class UserManagementComponent implements OnInit { + + users: User[] = []; + editUser: User = { + username:'', + fullName:'', + roles: [] + }; + editUserFlag: boolean = false; + editUserForm: FormGroup; + rolesFromBackend = []; + selectedRoles : Array<String>= []; + + constructor(private userService: UserService, private router: Router, private fb: FormBuilder, private authService: AuthService) { } + + ngOnInit() { + this.userService.getUsers().subscribe((res: User[]) => { + this.users = res; + this.users.map(user=>{ + let tempRoles = []; + user.roles.map(role=>{ + tempRoles.push(role.name.substring(5)); + }); + user.roles = tempRoles; + }); + + }); + this.editUserForm = this.fb.group({ + username: '', + fullName: '', + password: [null, [ Validators.minLength(6)]], + roles: [this.selectedRoles, [Validators.required]] + }); + this.userService.getRoles().subscribe(res=>{ + Object.values(res).forEach(ele=>{ + this.rolesFromBackend.push({label:ele.substring(5), value:ele}); + }); + }); + } + + handleDelete(username) { + const result = window.confirm('Are you sure to delete this user?'); + if(result === true) { + this.userService.deleteUser(username).subscribe(res=>{ + alert(res.message); + this.userService.getUsers().subscribe( r => { + this.users = r; + this.users.map(user=>{ + let tempRoles = []; + user.roles.map(role=>{ + tempRoles.push(role.name.substring(5)); + }); + user.roles = tempRoles; + }); + }); + }); + } + } + + handleEdit(user) { + this.selectedRoles = []; + this.editUserFlag = true; + this.editUser = user; + this.editUserForm.get('username').setValue(user.username); + this.editUserForm.get('fullName').setValue(user.fullName); + user.roles.map(ele => { + let temp = "ROLE_" + ele; + this.selectedRoles.push(temp); + }) + + this.editUserForm.get('roles').setValue(this.selectedRoles); + + } + + closeEditDialog() { + this.editUserForm.reset(); + this.editUserFlag = false; + } + + generateNewPassword() { + this.editUserForm.value.password = ''; + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&'; + var array = new Uint32Array(32); + window.crypto.getRandomValues(array); + for(let i=0;i<32;i++) { + const index = Math.floor(array[i] % chars.length); + this.editUserForm.value.password += chars.charAt(index); + } + this.editUserForm.patchValue({password: this.editUserForm.value.password}); + } + + submitEdit(user) { + this.editUserFlag = false; + console.log(this.editUserForm.value.fullName); + let tempUser = this.editUserForm.value as User; + console.log(tempUser); + this.userService.editUser(user.username, this.editUserForm.value as User).subscribe(res=>{ + alert("User information updated successfully."); + this.userService.getUsers().subscribe( r => { + this.users = r; + this.users.map(user=>{ + let tempRoles = []; + user.roles.map(role=>{ + tempRoles.push(role.name.substring(5)); + }); + user.roles = tempRoles; + }); + }, (err)=>{ + // alert(err.error.message); + alert("Sorry but your credentials are out of date. Please log in again to resolve this."); + this.authService.logout(); + }); + }, (err)=>{ + alert(err.error.message); + }); + } + +} |