diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 59111b6c8602d03d4c0d954364413f6f6dfd2b3c..ca9b96403403231ff452d8fd94b9fe9207c104be 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ sonar: image: trion/ng-cli:17.3.7 only: - develop + - /^release/ script: - npm ci --force - npm run sonar -- -Dsonar.host.url=${SONAR_HOST} -Dsonar.projectKey=${SONAR_PROJECT_KEY} -Dsonar.projectName=${SONAR_PROJECT_NAME} -Dsonar.branch.name=develop -Dsonar.login=${SONAR_LOGIN_TOKEN} diff --git a/build.gradle b/build.gradle index b0f93a8b25f6963e6821448de89bd40a1cb6ccc0..a443e5382e46befdc5870955a7650591f7e79e96 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id "org.sonarqube" version "3.2.0" } -version = '1.6.5' +version = '1.7.0-SNAPSHOT' task buildGUI(type: Exec) { println 'Building using Angular CLI' diff --git a/package-lock.json b/package-lock.json index 2158b8b4014ac3f0dbe34ebdc01a4c2d4d563d8d..9587e3edb93829b96b09f645a89e4907917c705d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nmaas-portal", - "version": "1.6.5", + "version": "1.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nmaas-portal", - "version": "1.6.5", + "version": "1.7.0", "license": "Apache 2.0", "dependencies": { "@angular/animations": "17.3.12", diff --git a/package.json b/package.json index 09a8e774d77de12a925911a29ee16690f7e7194c..6c76618594cd9546a117d73864d93c6fccfd124d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nmaas-portal", - "version": "1.6.5", + "version": "1.7.0", "license": "Apache 2.0", "angular-cli": {}, "scripts": { diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index c16ff45ea28e9f74a889c241a77b0d005624c223..610c10ccf5c6a61d2610e4be2b3fdefe9e51fcde 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -5,13 +5,16 @@ import { AppMarketRoutes } from './appmarket'; import { WelcomeRoutes } from './welcome/welcome.routes'; import {ServiceUnavailableRoutes} from './service-unavailable/service-unavailable.routes'; import {PageNotFoundComponent} from './shared/page-not-found/page-not-found.component'; +import {LoginSuccessComponent} from './auth/login-success/login-success.component'; const appRoutes: Routes = [ ...WelcomeRoutes, ...AppMarketRoutes, ...ServiceUnavailableRoutes, { path: 'notfound', component: PageNotFoundComponent }, - { path: '**', redirectTo: '/welcome' } + { path: 'login-success', component: LoginSuccessComponent }, + { path: '**', redirectTo: '/welcome' }, + ]; export const routing = RouterModule.forRoot(appRoutes, { scrollPositionRestoration: 'enabled' }); diff --git a/src/app/appmarket/admin/configuration/details/configurationdetails.component.html b/src/app/appmarket/admin/configuration/details/configurationdetails.component.html index 92aafcd35ac0b0fe8ffbd4b30836afee1c5b256e..de8a5d141eabd06407384500438ac27cf15ed1a3 100644 --- a/src/app/appmarket/admin/configuration/details/configurationdetails.component.html +++ b/src/app/appmarket/admin/configuration/details/configurationdetails.component.html @@ -131,6 +131,17 @@ </div> </div> + <div class="form-group"> + <label for="bulkDeploymentQueueRefresh" + class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DEPLOYMENT_QUEUE_REFRESH' | translate}}</label> + <div class="col-sm-9 pd-top-7"> + <div class="input-width"> + <input class="form-control" type="number" id="bulkDeploymentQueueRefresh" name="bulkDeploymentQueueRefresh" + [(ngModel)]="this.configuration.bulkDeploymentQueueRefresh"> + </div> + + </div> + </div> <div class="flex justify-content-end"> <button class="btn btn-primary" type="submit">{{ 'PORTAL_CONFIGURATION.SUBMIT_BUTTON' | translate }}</button> diff --git a/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts b/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts index 742069adfd21e9f305a3b14ceb8ccbbdcdf4c0ae..449459e4f4b881d437f763ae6433b6b45ad4b7b7 100644 --- a/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts +++ b/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts @@ -13,7 +13,6 @@ import {NgxPaginationModule} from 'ngx-pagination'; import {AppRestartModalComponent} from '../modals/app-restart-modal'; import {AppAbortModalComponent} from '../modals/app-abort-modal'; import {RouterTestingModule} from '@angular/router/testing'; -import {StorageServiceModule} from 'ngx-webstorage-service'; import {AppInstanceState, User} from '../../../model'; import {Role} from '../../../model/userrole'; import {ServiceAccessMethodType} from '../../../model/service-access-method'; @@ -272,7 +271,6 @@ describe('Component: AppInstance', () => { PipesModule, FormioModule, RouterTestingModule, - StorageServiceModule, JwtModule.forRoot({}), TranslateModule.forRoot({ loader: { diff --git a/src/app/appmarket/appmarket.module.ts b/src/app/appmarket/appmarket.module.ts index 8bd9ce86288da65d981ac9fe9af4363df7b433b3..1539264532ea45f31fb58cb059887338c5cecf57 100644 --- a/src/app/appmarket/appmarket.module.ts +++ b/src/app/appmarket/appmarket.module.ts @@ -21,7 +21,6 @@ import {ClustersModule} from './admin/clusters/clusters.module'; import {ClusterService} from '../service/cluster.service'; import {ConfigurationModule} from './admin/configuration/configuration.module'; import {MonitorModule} from './admin/monitor/monitor.module'; -import {StorageServiceModule} from 'ngx-webstorage-service'; import {TranslateModule} from '@ngx-translate/core'; import {HttpClientModule} from '@angular/common/http'; import {BrowserModule} from '@angular/platform-browser'; @@ -48,6 +47,12 @@ import {NgxPaginationModule} from 'ngx-pagination'; import {InputTextModule} from 'primeng/inputtext'; import {BulkSearchPipe} from './bulkDeployment/bulk-list/bulk-search.pipe'; import {CheckboxModule} from 'primeng/checkbox'; +import { InputSwitchModule } from 'primeng/inputswitch'; +import { OverlayPanelModule } from 'primeng/overlaypanel'; +import { SidebarModule } from 'primeng/sidebar'; +import { ProgressBarModule } from 'primeng/progressbar'; + + @NgModule({ declarations: [ @@ -66,7 +71,6 @@ import {CheckboxModule} from 'primeng/checkbox'; ], imports: [ FormsModule, - StorageServiceModule, CommonModule, RouterModule, SharedModule, @@ -94,6 +98,10 @@ import {CheckboxModule} from 'primeng/checkbox'; InputTextModule, TooltipModule, CheckboxModule, + InputSwitchModule, + OverlayPanelModule, + SidebarModule, + ProgressBarModule ], exports: [ AppMarketComponent, diff --git a/src/app/appmarket/bulkDeployment/appdeployment.service.ts b/src/app/appmarket/bulkDeployment/appdeployment.service.ts index e754e949023a94f7ceb1fcb65c90f7f0f8a7ab62..a44c0f9cc361bc8f12b247c5f9a1534fdbfe11f0 100644 --- a/src/app/appmarket/bulkDeployment/appdeployment.service.ts +++ b/src/app/appmarket/bulkDeployment/appdeployment.service.ts @@ -1,10 +1,11 @@ import {Injectable} from '@angular/core'; import {ApplicationBase} from '../../model/application-base'; -import {HttpClient} from '@angular/common/http'; +import {HttpClient, HttpParams} from '@angular/common/http'; import {AppConfigService} from '../../service'; import {BulkResponse} from '../../model/bulk-response'; import {Observable} from 'rxjs'; import {BulkDeployment} from '../../model/bulk-deployment'; +import { BulkQueueDetails } from '../../model/bulk-queue-details'; @Injectable({ providedIn: 'root' @@ -71,16 +72,20 @@ export class AppdeploymentService { return this.http.post<BulkDeployment>(this.getUrl() + 'domains', formParams); } - public getBulksDomainDeployments(): Observable<BulkDeployment[]> { - return this.http.get<BulkDeployment[]>(this.getUrl() + 'domains'); + public getBulksDomainDeployments(showDeleted: boolean = false): Observable<BulkDeployment[]> { + const formParams = new HttpParams().append('deleted', showDeleted); + + return this.http.get<BulkDeployment[]>(this.getUrl() + 'domains', {params:formParams}); } public getBulksDomainDeploymentsOwner(): Observable<BulkDeployment[]> { return this.http.get<BulkDeployment[]>(this.getUrl() + 'domains/vl'); } - public getBulksAppDeployments(): Observable<BulkDeployment[]> { - return this.http.get<BulkDeployment[]>(this.getUrl() + 'apps'); + public getBulksAppDeployments(showDeleted: boolean = false): Observable<BulkDeployment[]> { + const formParams = new HttpParams().append('deleted', showDeleted); + + return this.http.get<BulkDeployment[]>(this.getUrl() + 'apps', {params:formParams}); } public getBulksAppDeploymentsOwner(): Observable<BulkDeployment[]> { @@ -99,5 +104,14 @@ export class AppdeploymentService { return this.http.get<BulkDeployment>(this.getUrl() + `refresh/${id}`) } + public removeBulkDeployment(id: number, removeAll: boolean) { + const params = new HttpParams().set('removeAll', removeAll); + return this.http.delete(this.getUrl() +`${id}`, { params }); + } + + public getQueueDetails(id: number) : Observable<BulkQueueDetails> { + return this.http.get<BulkQueueDetails>(this.getUrl() + `queue/${id}`) + } + } diff --git a/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.html b/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.html index e34eee5938d37ec4330b3cd1f1ebdc2d9659707f..340557d5220556e32498ffcac2549231bde94c23 100644 --- a/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.html +++ b/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.html @@ -1,2 +1,2 @@ -<app-bulk-list [header]="'BULK.APP.HEADER'" [bulks]="bulks" [mode]="mode"></app-bulk-list> +<app-bulk-list [header]="'BULK.APP.HEADER'" [bulks]="bulks" [mode]="mode" (refresh)="onRefresh($event)"></app-bulk-list> diff --git a/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.ts b/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.ts index 9256cfa4ea2a99172b68b96471a1312b92bb206d..4a773179c64068601af012a9fe6506a818eecfbe 100644 --- a/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.ts +++ b/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.ts @@ -20,16 +20,20 @@ export class BulkAppListComponent implements OnInit { } ngOnInit(): void { + this.onRefresh(); + } + + onRefresh(showDeleted = false) : void { if (this.authService.getRoles().find(value => value === 'ROLE_VL_MANAGER') !== undefined) { this.deployService.getBulksAppDeploymentsOwner().subscribe(data => { data = data.sort((a, b) => new Date(b.creationDate).getTime() - new Date(a.creationDate).getTime()) this.bulks = data }); } else { - this.deployService.getBulksAppDeployments().subscribe(data => { + this.deployService.getBulksAppDeployments(showDeleted).subscribe(data => { data = data.sort((a, b) => new Date(b.creationDate).getTime() - new Date(a.creationDate).getTime()) this.bulks = data }); } } -} + } diff --git a/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.html b/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.html index 7a9fc5815787d93905e271793dc971856f8a1bcc..06d42a13307598d26cd863a2d5d086d6b4284510 100644 --- a/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.html +++ b/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.html @@ -7,7 +7,40 @@ <div *ngIf="mode=== bulkTypeApp"> <button class="btn btn-primary" [routerLink]="['/admin/apps/bulks/new']">New deployment</button> </div> + <div class="flex"> + <div *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']" class="flex align-items-center mr-6"> + + <p-button + type="button" + class="mr-2" + (onClick)="sidebarVisible4 = true" + label="Deployments Queue" + severity="secondary" + ></p-button> + <p-sidebar [(visible)]="sidebarVisible4" position="left" styleClass="w-30rem"> + <h3>Deployments Queue</h3> + <div class="flex flex-column"> + <div class="flex grid"> + <label class="col-10" for="jobInQueue">Jobs in queue</label> + <span class="col-2 " id="jobInQueue">{{queueDetails?.jobInQueue}}</span> + </div> + <div class="flex grid"> + <label class="col-10" for="jobInProcess">In progress</label> + <span class="col-2 " id="jobInProcess">{{queueDetails?.jobInProcess}}</span> + </div> + <div class="flex grid"> + <label class="col-10" for="jobInProcessId">Current proccessing bulk id</label> + <span class="col-2" id="jobInProcessId">{{queueDetails?.jobInProcessId}}</span> + </div> + </div> + </p-sidebar> + + </div> + <div *roles="['ROLE_SYSTEM_ADMIN']" class="flex align-items-center mr-6 pt-2"> + <label *ngIf="mode=== bulkTypeApp" class="mr-2" for="showDeleted">Show all</label> + <p-inputSwitch *ngIf="mode=== bulkTypeApp" id="showDeleted" (onChange)="refreshBulks()" [(ngModel)]="showDeleted" ngDefaultControl/> + </div> <div class="flex align-items-center mr-1">{{ 'BULK.LIST.PER_PAGE' | translate }}:</div> <span id="selectionItems" class="dropdown" style="vertical-align: middle; display: inline-block; margin-right: 1rem;"> @@ -102,8 +135,8 @@ <li *ngIf="mode === bulkTypeApp && bulk?.state !== 'REMOVED'"> <a (click)="getAppBulkDetails(bulk?.id)"> {{"BULK.APP.DOWNLOAD_CSV" | translate}}</a> </li> - <li *ngIf="mode === bulkTypeApp && bulk?.state !== 'REMOVED' "> - <a (click)="modal.show()">{{ 'BULK.LIST.REMOVE' | translate }}</a> + <li *ngIf="mode === bulkTypeApp && !(bulk?.state === 'REMOVED' || bulk?.deleted )"> + <a (click)="modal.show(); removeBulkId=bulk?.id">{{ 'BULK.LIST.REMOVE' | translate }}</a> </li> </ul> </span> @@ -133,6 +166,6 @@ </div> <div class="nmaas-modal-footer"> <button type="button" class="btn btn-secondary" (click)="modal.hide()">{{'APP_CHANGE_STATE_MODAL.CANCEL_BUTTON' | translate}}</button> - <button type="button" class="btn btn-primary" [disabled]="false">{{'BULK.REMOVE.REMOVE' | translate}}</button> + <button type="button" class="btn btn-primary" (click)="this.removeBulk()">{{'BULK.REMOVE.REMOVE' | translate}}</button> </div> </nmaas-modal> diff --git a/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.ts b/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.ts index 8288379d544ee7f88744f62d8ee497a98c9d199c..4883a6bb95b8301576a8479a0d1aac8524bd0cb3 100644 --- a/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.ts +++ b/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.ts @@ -1,17 +1,20 @@ -import {Component, Input, QueryList, ViewChild, ViewChildren} from '@angular/core'; +import {Component, EventEmitter, Input, OnDestroy, OnInit, Output, QueryList, ViewChild, ViewChildren} from '@angular/core'; import {BulkDeployment} from '../../../model/bulk-deployment'; import {BulkType} from '../../../model/bulk-response'; import {SortableHeaderDirective} from '../../../service/sort-domain.directive'; import {ModalComponent} from '../../../shared'; import {AppdeploymentService} from '../appdeployment.service'; import {DomSanitizer} from '@angular/platform-browser'; +import { BulkQueueDetails } from '../../../model/bulk-queue-details'; +import { map, timer } from 'rxjs'; +import { ConfigurationService } from '../../../service'; @Component({ selector: 'app-bulk-list', templateUrl: './bulk-list.component.html', styleUrls: ['./bulk-list.component.css'] }) -export class BulkListComponent { +export class BulkListComponent implements OnDestroy, OnInit { public static BULK_ENTRY_DETAIL_KEY_APP_INSTANCE_NO = 'appInstanceNo'; public static BULK_ENTRY_DETAIL_KEY_APP_INSTANCE_NAME = 'appName'; @@ -32,6 +35,10 @@ export class BulkListComponent { @ViewChild(ModalComponent, {static: true}) public readonly modal: ModalComponent; + @Output() + public refresh: EventEmitter<boolean> = new EventEmitter<boolean>(); + + public showDeleted = false; public readonly bulkTypeDomain = BulkType.DOMAIN; public readonly bulkTypeApp = BulkType.APPLICATION; @@ -42,9 +49,26 @@ export class BulkListComponent { public searchValue = ''; public removeAll = false; + public removeBulkId = 0; + + public queueDetails : BulkQueueDetails; + + public refreshQueue = undefined; + public sidebarVisible4 = false; + + public configRefresh = 60; + constructor(private appDeploy: AppdeploymentService, - private sanitizer: DomSanitizer) { + private sanitizer: DomSanitizer, + private configService: ConfigurationService) { + } + + public ngOnInit(): void { + this.configService.getConfiguration().subscribe(conf => { + this.configRefresh = conf.bulkDeploymentQueueRefresh; + this.update(); + }) } public getApplicationName(details: Map<string, string>) { @@ -147,4 +171,44 @@ export class BulkListComponent { } return null } + + public removeBulk(): void { + this.appDeploy.removeBulkDeployment(this.removeBulkId, this.removeAll).subscribe(_ => { + this.refreshBulks(); + this.modal.hide(); + }) + this.removeAll = false; + } + + public refreshBulks(): void { + this.refresh.emit(this.showDeleted); + } + + public getQueueDetails(): void { + this.appDeploy.getQueueDetails(0).subscribe(queue => { + console.log(queue); + this.queueDetails = queue; + }) + } + + public update() { + this.refreshQueue = timer(0, this.refreshQueue * 1000).pipe(map(() => { + this.getQueueDetails(); + })).subscribe() + } + + public getQueryNumber() { + return this.queueDetails?.jobInQueue + this.queueDetails?.jobInProcess; + } + + + public ngOnDestroy() { + if (this.refreshQueue !== undefined) { + this.refreshQueue.unsubscribe(); + } + } + + public open() { + + } } diff --git a/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.css b/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.css index 415a4d52c3f56b52ae0540dc65b55bf5b1c3d599..66d99ef9dc8e1b044db0c92653980615c5db3ea5 100644 --- a/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.css +++ b/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.css @@ -34,4 +34,13 @@ white-space: nowrap !important; } +:host ::ng-deep .job-done-bar { + .p-progressbar-value { + border: 0 none; + margin: 0; + background:green; + } +} + + diff --git a/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.html b/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.html index c40a1ff6969dfe5322906d7e116981f228ef913a..9c870590051db4025f7b3d0dffbaeee499a71785 100644 --- a/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.html +++ b/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.html @@ -62,7 +62,7 @@ <td>{{getDomainId(response)}}</td> <td>{{getDomainName(response)}}</td> <td>{{getDomainCodeName(response)}}</td> - <td style="width: 5%" class="text-right"> + <td style="width: 5%" class="text-right" *ngIf="bulk.state !== 'REMOVED'"> <i *ngIf="response.type === 'DOMAIN'" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/admin/domains/view/', response?.details['domainId']]"></i> <i *ngIf="response.type === 'USER'" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/admin/users/view', response?.details['userId']]"></i> <!-- <span class="dropdown"> @@ -160,6 +160,15 @@ </div> </div> + <div class="" style="padding-bottom: 5rem;"> + <label for="id" class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.APP_NAME' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="id" name="id" [disabled]="true" + [(ngModel)]="bulk.details['appName']" #name="ngModel"> + </div> + + </div> + <div class="" style="padding-bottom: 5rem"> <label for="creator" class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.CREATOR' | translate }}</label> @@ -187,21 +196,31 @@ </div> </div> - <div class="" style="padding-bottom: 5rem;"> - <label for="id" class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.APP_NAME' | translate }}</label> + <div class="" style="padding-bottom: 5rem"> + <label for="completionDate" + class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.COMPLETION_DATE' | translate }}</label> <div class="col-sm-10"> - <input type="text" class="form-control" id="id" name="id" [disabled]="true" - [(ngModel)]="bulk.details['appName']" #name="ngModel"> + <input type="text" class="form-control" id="completionDate" name="completionDate" [disabled]="true" + placeholder="{{completionDate}}"> </div> - </div> - <div class="flex justify-content-end" style="padding-right: 1.5rem"> + + + <div *ngIf="bulk.state !== 'REMOVED'" class="flex justify-content-end" style="padding-right: 1.5rem"> <button class="btn btn-primary mr-2" (click)="refreshStates()">{{'BULK.APP.REFRESH' | translate}}</button> - <button class="btn btn-primary" (click)="getAppBulkDetails(this.bulkId)">{{'BULK.APP.DOWNLOAD_CSV' | translate}}</button> + <button *ngIf="bulk.state !== 'FAILED'" class="btn btn-primary" (click)="getAppBulkDetails(this.bulkId)">{{'BULK.APP.DOWNLOAD_CSV' | translate}}</button> </div> - <div class="panel panel-default" style="margin-top: 3rem"> + <div class="mt-4"> + <p-progressBar [mode]="progressBarMode"[value]="progressBarValue" id="progressBarr" [style]="{ height: '18px' }" [styleClass]="jobDone ? 'job-done-bar' : 'normal-bar'"> + <ng-template pTemplate="content" let-value> + <span>{{queueDetails?.jobDone}} <span *ngIf="queueDetails?.jobDone !== bulk.entries.length">(+{{queueDetails?.jobInProcess}})</span> / {{bulk.entries.length}}</span> + </ng-template> + </p-progressBar> + + </div> + <div class="panel panel-default" style="margin-top: 1rem"> <div class="panel-heading"> <div style="display: flex; justify-content: start; align-items: center"> @@ -235,8 +254,8 @@ <td>{{getAppInstanceId(response)}}</td> <td>{{getAppInstanceName(response)}}</td> <td>{{getDomainCodeName(response)}}</td> - <td style="width: 5%" class="text-right"> - <i class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/instances/', response?.details['appInstanceId']]"></i> + <td style="width: 5%" class="text-right" > + <i *ngIf="response?.details['appInstanceId'] !== undefined" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/instances/', response?.details['appInstanceId']]"></i> <!-- <span *ngIf="response?.details['appInstanceId'] !== undefined" class="dropdown"> <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> diff --git a/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.spec.ts b/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.spec.ts index 33287d7e82506504fe8fc2bf9a4100084a49ff39..2c9c55fa673bd9e1bafe4db2fc2e97d50359fd76 100644 --- a/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.spec.ts +++ b/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BulkViewComponent } from './bulk-view.component'; import {HttpClientTestingModule} from '@angular/common/http/testing'; import {RouterModule} from '@angular/router'; +import { DatePipe } from '@angular/common'; describe('BulkViewComponent', () => { let component: BulkViewComponent; @@ -14,6 +15,9 @@ describe('BulkViewComponent', () => { imports: [ HttpClientTestingModule, RouterModule.forRoot([]), + ], + providers: [ + DatePipe ] }) .compileComponents(); diff --git a/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.ts b/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.ts index 5895c0b6ba5531de6c627b1026c321333073a0df..4b0c59f9115bcae87a18f76b5ea7c109e3c5f9c9 100644 --- a/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.ts +++ b/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.ts @@ -5,7 +5,9 @@ import {ActivatedRoute, Router} from '@angular/router'; import {BulkResponse, BulkType} from '../../../model/bulk-response'; import {timer} from 'rxjs'; import {map} from 'rxjs/operators'; -import {AppImagesService} from '../../../service'; +import {AppImagesService, ConfigurationService} from '../../../service'; +import { BulkQueueDetails } from '../../../model/bulk-queue-details'; +import { DatePipe } from '@angular/common'; @Component({ selector: 'app-bulk-view', @@ -20,21 +22,40 @@ export class BulkViewComponent implements OnInit, OnDestroy { public refresh = undefined; + public progressBarMode ; + public progressBarValue ; + + public queueDetails :BulkQueueDetails; + + public jobDone = false; + public completionDate = ""; + + public configRefresh = 60; + constructor(public deployService: AppdeploymentService, private route: ActivatedRoute, private router: Router, public appImagesService: AppImagesService, + private datePipe: DatePipe, + private configService: ConfigurationService ) { } ngOnInit(): void { + this.configService.getConfiguration().subscribe(conf => { + this.configRefresh = conf.bulkDeploymentQueueRefresh; + }) + this.route.params.subscribe(params => { if (params['id'] !== undefined) { this.bulkId = +params['id']; this.deployService.getBulkDeployment(this.bulkId).subscribe( (bulk) => { this.bulk = bulk; + this.sortByInstanceId(); this.bulkType = bulk.type; + this.getQueueDetails(); + this.setCompletionDate(bulk); if (this.bulkType === BulkType.APPLICATION) { this.update(); } @@ -88,10 +109,24 @@ export class BulkViewComponent implements OnInit, OnDestroy { } public update() { - this.refresh = timer(0, 20000).pipe(map(() => { + this.refresh = timer(0, this.configRefresh * 1000).pipe(map(() => { this.deployService.getBulkDeployment(this.bulk.id).subscribe(bulk => { this.bulk = bulk; + this.sortByInstanceId(); + this.setCompletionDate(bulk); + if(bulk.state === 'REMOVED') this.refresh.unsubscribe(); + if(bulk.state === 'PROCESSING' && this.queueDetails.jobInProcessId === bulk.id) { + this.progressBarMode = "determinate" + this.setBarValue(); + } else if(bulk.state === 'PROCESSING') { + this.setBarValue(); + this.progressBarMode = "indeterminate" + } else { + this.progressBarMode = "determinate" + this.setBarValue(); + } }) + })).subscribe() } @@ -103,7 +138,7 @@ export class BulkViewComponent implements OnInit, OnDestroy { public getAppBulkDetails(id: number) { this.deployService.getAppBulkDetails(id).subscribe( (data: Blob) => { - console.warn(data) + console.log(data) const blob = new Blob([data], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); @@ -119,7 +154,43 @@ export class BulkViewComponent implements OnInit, OnDestroy { public refreshStates() { this.deployService.refreshStatesInBulkDeployment(this.bulkId).subscribe( deply => { this.bulk = deply; - console.log("Updated states of bulks") + this.sortByInstanceId(); + this.setCompletionDate(deply); + this.getQueueDetails(); }) } + + public setCompletionDate( deployment: BulkDeployment) { + if(this.bulk.completionDate !== undefined && this.bulk.completionDate !== null && deployment.state === 'COMPLETED') { + this.completionDate = this.datePipe.transform(this.bulk.completionDate,'dd-MM-yyyy HH:mm' ) + } else { + this.completionDate = " - " + } + } + + public setBarValue() { + this.getQueueDetails(); + } + + public getQueueDetails(): void { + this.deployService.getQueueDetails(this.bulkId).subscribe(queue => { + this.queueDetails = queue; + if(queue.jobDone === this.bulk.entries.length) { + this.progressBarValue = 100; + this.jobDone = true; + this.progressBarMode = "determinate" + } else if(queue.jobDone === 0) { + this.progressBarMode = "indeterminate" + }else { + this.progressBarMode = "determinate" + this.progressBarValue = queue.jobDone * 100 / this.bulk.entries.length; + this.jobDone = false; + } + + }) + } + + public sortByInstanceId() { + this.bulk.entries.sort((a,b) => this.getAppInstanceId(a) < this.getAppInstanceId(b) ? 1 : -1); + } } diff --git a/src/app/appmarket/users/userdetails/userdetails.component.html b/src/app/appmarket/users/userdetails/userdetails.component.html index 6baf5888a19174411af0589f1fcfed73cbcb78e1..f728a79ce158c48bcbe6db89c05d4452d03a47c3 100644 --- a/src/app/appmarket/users/userdetails/userdetails.component.html +++ b/src/app/appmarket/users/userdetails/userdetails.component.html @@ -7,5 +7,6 @@ <div> <nmaas-userdetails [user]="user" [(userDetailsMode)]="userDetailsMode" [allowedModes]="[ComponentMode.VIEW, ComponentMode.EDIT]" [(errorMessage)]="errorMessage" (onSave)="onSave($event)" (refresh)="onRefresh()"></nmaas-userdetails> <nmaas-userprivileges [allowedModes]="[ComponentMode.VIEW, ComponentMode.CREATE]" [user]="user"></nmaas-userprivileges> + <nmaas-ssh-keys *roles="['ROLE_SYSTEM_ADMIN']" [userMode]="true" [userId]="user?.id"></nmaas-ssh-keys> </div> </div> diff --git a/src/app/auth/auth.guard.ts b/src/app/auth/auth.guard.ts index aea400e0a3ccf6885624238b496fcbd5186d9e2e..2fe88c217381e865b5ed27e7a814578d5c59aa32 100644 --- a/src/app/auth/auth.guard.ts +++ b/src/app/auth/auth.guard.ts @@ -2,6 +2,7 @@ import {Injectable} from '@angular/core'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import {AuthService} from './auth.service'; import {ConfigurationService} from '../service'; +import { debounceTime } from 'rxjs'; @Injectable() export class AuthGuard { @@ -11,7 +12,7 @@ export class AuthGuard { public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { if (this.auth.isLogged()) { - this.maintenanceService.getConfiguration().subscribe(value => { + this.maintenanceService.getConfiguration().pipe(debounceTime(500)).subscribe(value => { if (!this.auth.hasRole('ROLE_SYSTEM_ADMIN') && value.maintenance) { this.auth.logout(); this.router.navigate(['/welcome/login']); diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts index 27bb76c9fc8bfe15a5c61ce85a34c6eee4dbe0fc..cab76d65d8c483ba806570bc6daa101e29e701c3 100644 --- a/src/app/auth/auth.module.ts +++ b/src/app/auth/auth.module.ts @@ -5,6 +5,7 @@ import {AppConfigService} from '../service/appconfig.service'; import {AuthService} from './auth.service' import {AuthGuard} from './auth.guard' import {RoleGuard} from './role.guard'; +import { LoginSuccessComponent } from './login-success/login-success.component'; export const jwtOptionsFactory = (appConfig: AppConfigService) => ({ @@ -15,6 +16,9 @@ export const jwtOptionsFactory = (appConfig: AppConfigService) => ({ }); @NgModule({ + declarations: [ + LoginSuccessComponent + ], providers: [ AuthGuard, RoleGuard, diff --git a/src/app/auth/auth.service.ts b/src/app/auth/auth.service.ts index 6fd5f6febafa62fc7d3843a3e9d22629b066fa79..ad7c6b8fcfa0166568751f3f9e1e3b4aa1a11938 100644 --- a/src/app/auth/auth.service.ts +++ b/src/app/auth/auth.service.ts @@ -34,8 +34,8 @@ export class AuthService { private appConfig: AppConfigService, private jwtHelper: JwtHelperService) { } - - private storeToken(token: string): void { + //TODO make this static again and serive this feature in other way + public storeToken(token: string): void { localStorage.setItem(this.appConfig.config.tokenName, token); } diff --git a/src/app/auth/login-success/login-success.component.css b/src/app/auth/login-success/login-success.component.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/auth/login-success/login-success.component.html b/src/app/auth/login-success/login-success.component.html new file mode 100644 index 0000000000000000000000000000000000000000..bf2b4b51596bcb734c40c6d46e1ec4917d9b6f71 --- /dev/null +++ b/src/app/auth/login-success/login-success.component.html @@ -0,0 +1 @@ +<p>login-success works!</p> diff --git a/src/app/auth/login-success/login-success.component.spec.ts b/src/app/auth/login-success/login-success.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2a446bbc2bc60e4ade6a4f4d3fa81c78bfdc21f --- /dev/null +++ b/src/app/auth/login-success/login-success.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginSuccessComponent } from './login-success.component'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ActivatedRoute } from '@angular/router'; +import { ActivatedRouteStub } from '../../shared/test-utils'; +import { AuthService } from '../auth.service'; + +describe('LoginSuccessComponent', () => { + let component: LoginSuccessComponent; + let fixture: ComponentFixture<LoginSuccessComponent>; + + const authUserSpy = jasmine.createSpyObj('AuthService', ['storeToken']); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ LoginSuccessComponent ], + imports: [ + RouterTestingModule + ], + providers: [ + {provide: ActivatedRoute, useValue: new ActivatedRouteStub({token: '123'})}, + {provide: AuthService, useValue: authUserSpy}, + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LoginSuccessComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/auth/login-success/login-success.component.ts b/src/app/auth/login-success/login-success.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..02d3386850147f1e758a80f4b435d973e5abb42f --- /dev/null +++ b/src/app/auth/login-success/login-success.component.ts @@ -0,0 +1,28 @@ +import {Component, OnInit} from '@angular/core'; +import {ActivatedRoute, Router} from '@angular/router'; +import {AuthService} from '../auth.service'; + +@Component({ + selector: 'app-login-success', + templateUrl: './login-success.component.html', + styleUrls: ['./login-success.component.css'] +}) +export class LoginSuccessComponent implements OnInit { + constructor(private readonly router: Router, + private readonly route: ActivatedRoute, + private readonly authService: AuthService) { + } + + + ngOnInit(): void { + this.route.queryParams.subscribe(params => { + const token = params['token']; + const refreshToken = params['refresh_token']; + if (token) { + this.authService.storeToken(token); + } + this.router.navigate(['/portal']) + }) + + } +} diff --git a/src/app/model/bulk-deployment.ts b/src/app/model/bulk-deployment.ts index bf8828bb54fb0fa2dfdbfb0ad7cfc50f725979c9..1af669ecbe0bd3e05068993accf1dd17854d90e4 100644 --- a/src/app/model/bulk-deployment.ts +++ b/src/app/model/bulk-deployment.ts @@ -10,6 +10,8 @@ export class BulkDeployment { public type: BulkType; public details: Map<string, string>; public parallelDeploymentsLimit: number; + public deleted: boolean; + public completionDate: Date; } export enum BulkDeploymentState { diff --git a/src/app/model/bulk-queue-details.ts b/src/app/model/bulk-queue-details.ts new file mode 100644 index 0000000000000000000000000000000000000000..a86da4af02b6c0151783ddbe7733b54db57d5877 --- /dev/null +++ b/src/app/model/bulk-queue-details.ts @@ -0,0 +1,8 @@ +export class BulkQueueDetails { + + public jobInProcess: number; + public jobInProcessId: number; + public jobInQueue: number; + public jobDone: number; + public bulkJobInQueue: number; +} \ No newline at end of file diff --git a/src/app/model/configuration.ts b/src/app/model/configuration.ts index 3bffd6e8f90421fa78c8e722ece5d6de9b8aeb2d..5bdb54fdace5d822fef1526fbfe092aa13117288 100644 --- a/src/app/model/configuration.ts +++ b/src/app/model/configuration.ts @@ -11,4 +11,5 @@ export class Configuration { public appInstanceFailureEmailList: string[] = []; public bulkDeploymentJobCron: string; public parallelDeploymentsLimit: number; + public bulkDeploymentQueueRefresh: number; } diff --git a/src/app/service/appconfig.service.ts b/src/app/service/appconfig.service.ts index 46e77329d2aee5953b8563cf0114e2411917296e..e15d9b5c331c6cfdc8d7bf68ed9c24ec3d8bed3e 100644 --- a/src/app/service/appconfig.service.ts +++ b/src/app/service/appconfig.service.ts @@ -1,5 +1,5 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; @Injectable({ @@ -10,7 +10,8 @@ export class AppConfigService { public jwtAllowedDomains: string[] = [] - constructor(private http: HttpClient) { } + constructor(private http: HttpClient) { + } public load() { return new Promise<void>((resolve) => { @@ -23,39 +24,43 @@ export class AppConfigService { }); } + public getOidcUrl(): string { + return this.config.apiUrl + '/oauth2/authorization/my-oidc'; + } + public getApiUrl(): string { - if (this.config == null) { - return 'http://localhost/api'; - } - return this.config.apiUrl; + if (this.config == null) { + return 'http://localhost/api'; + } + return this.config.apiUrl; } public getNmaasGlobalDomainId(): number { - if (this.config == null) { - return 0; - } - return this.config.nmaas.globalDomainId || 0; + if (this.config == null) { + return 0; + } + return this.config.nmaas.globalDomainId || 0; } public getHttpTimeout(): number { - if (this.config == null) { - return 10000; - } - return this.config.http.timeout || 10000; + if (this.config == null) { + return 10000; + } + return this.config.http.timeout || 10000; } public getShowGitInfo(): boolean { - if (this.config == null) { - return false; - } - return this.config.showGitInfo || false; + if (this.config == null) { + return false; + } + return this.config.showGitInfo || false; } public getShowChangelog(): boolean { - if (this.config == null) { - return false; - } - return this.config.showChangelog || false; + if (this.config == null) { + return false; + } + return this.config.showChangelog || false; } public getSiteKey(): string { @@ -70,6 +75,6 @@ export class AppConfigService { } public getLandingProfile(): string { - return this.config.landing || '' + return this.config.landing || '' } } diff --git a/src/app/service/configuration.service.ts b/src/app/service/configuration.service.ts index 6e03d6f911758fc5dbbe32e2c3a4a92b5789b17a..9d93b4018f6ead3cfc3eb71c3db60417323b71bd 100644 --- a/src/app/service/configuration.service.ts +++ b/src/app/service/configuration.service.ts @@ -14,7 +14,7 @@ export class ConfigurationService extends GenericDataService{ constructor(http: HttpClient, appConfig: AppConfigService) { super(http, appConfig); - this.uri = this.appConfig.getApiUrl() + '/configuration/' + this.uri = this.appConfig.getApiUrl() + '/configuration' } public getConfiguration(): Observable<Configuration> { @@ -22,7 +22,7 @@ export class ConfigurationService extends GenericDataService{ } public updateConfiguration(configuration: Configuration): Observable<any>{ - return this.put(this.uri + configuration.id, configuration); + return this.put(this.uri + "/"+ configuration.id, configuration); } } diff --git a/src/app/service/domain.service.ts b/src/app/service/domain.service.ts index dfff8ef6b175c94517fe78cdbb03feafe3f65202..7ed0257a4aca76390edd525a84274cb6667c150d 100644 --- a/src/app/service/domain.service.ts +++ b/src/app/service/domain.service.ts @@ -24,7 +24,7 @@ export class DomainService extends GenericDataService { constructor(http: HttpClient, appConfig: AppConfigService) { super(http, appConfig); this.updateRequiredFlag = false; - this.url = this.appConfig.getApiUrl() + '/domains/'; + this.url = this.appConfig.getApiUrl() + '/domains'; } public getGlobalDomainId(): number { @@ -40,7 +40,7 @@ export class DomainService extends GenericDataService { } public getOne(domainId: number): Observable<Domain> { - return this.get<Domain>(this.url + domainId); + return this.get<Domain>(this.url + '/' + domainId); } public add(domain: Domain): Observable<Id> { @@ -48,19 +48,19 @@ export class DomainService extends GenericDataService { } public update(domain: Domain): Observable<any> { - return this.put<Domain, Id>(this.url + domain.id, domain); + return this.put<Domain, Id>(this.url + '/' + domain.id, domain); } public updateTechDetails(domain: Domain): Observable<any> { - return this.patch<Domain, Id>(this.url + domain.id, domain) + return this.patch<Domain, Id>(this.url + '/' + domain.id, domain) } public updateDcnConfigured(domain: Domain): Observable<any> { - return this.patch<Domain, Id>(this.url + domain.id + '/dcn?configured=' + domain.domainDcnDetails.dcnConfigured, null); + return this.patch<Domain, Id>(this.url + '/' + domain.id + '/dcn?configured=' + domain.domainDcnDetails.dcnConfigured, null); } public updateDomainState(domain: Domain): Observable<any> { - return this.patch<Domain, Id>(this.url + domain.id + '/state?active=' + !domain.active, null); + return this.patch<Domain, Id>(this.url + '/' + domain.id + '/state?active=' + !domain.active, null); } public remove(domainId: number, softRemove?: boolean): Observable<any> { @@ -68,15 +68,15 @@ export class DomainService extends GenericDataService { if (softRemove !== undefined) { params = params.append("softRemove", softRemove.toString()) } - return this.http.delete(this.url + domainId, {params}) + return this.http.delete(this.url + '/' + domainId, {params}) } public getMyDomains(): Observable<Domain[]> { - return this.get<Domain[]>(this.url + 'my'); + return this.get<Domain[]>(this.url + '/my'); } public getUsers(domainId: number): Observable<User[]> { - return this.get<User[]>(this.url + 'users'); + return this.get<User[]>(this.url + '/users'); } public setUpdateRequiredFlag(flag: boolean) { @@ -89,50 +89,50 @@ export class DomainService extends GenericDataService { // GROUPS public getAllDomainGroups(): Observable<DomainGroup[]> { - return this.get<DomainGroup[]>(this.url + 'group'); + return this.get<DomainGroup[]>(this.url + '/group'); } public getDomainGroup(domainGroupId: number): Observable<DomainGroup> { - return this.get<DomainGroup>(this.url + 'group/' + domainGroupId); + return this.get<DomainGroup>(this.url + '/group/' + domainGroupId); } public deleteDomainGroup(domainGroupId: number): Observable<void> { - return this.delete<void>(this.url + 'group/' + domainGroupId); + return this.delete<void>(this.url + '/group/' + domainGroupId); } public addDomainsToGroup(groupCodeName: string, domainIds: number[]): Observable<DomainGroup> { - return this.post(this.url + 'group/' + groupCodeName, domainIds); + return this.post(this.url + '/group/' + groupCodeName, domainIds); } public deleteDomainFromGroup(groupId: number, domainId: number): Observable<DomainGroup> { - return this.patch(this.url + 'group/' + groupId, domainId); + return this.patch(this.url + '/group/' + groupId, domainId); } public createDomainGroup(domainGroup: DomainGroup): Observable<Id> { - return this.post(this.url + 'group', domainGroup); + return this.post(this.url + '/group', domainGroup); } public updateDomainGroup(domainGroup: DomainGroup, id: number): Observable<Id> { - return this.put(this.url + 'group/' + id, domainGroup); + return this.put(this.url + '/group/' + id, domainGroup); } public updateDomainGroupManagers(managers: User[], id: number): Observable<DomainGroup> { - return this.put(this.url + 'group/members/' + id, managers); + return this.put(this.url + '/group/members/' + id, managers); } public getAnnotations(): Observable<DomainAnnotation[]> { - return this.get<DomainAnnotation[]>(this.url + 'annotations') + return this.get<DomainAnnotation[]>(this.url + '/annotations') } public addAnnotations(annotation: KeyValue): Observable<void> { - return this.post(this.url + 'annotations', annotation) + return this.post(this.url + '/annotations', annotation) } - public deleteAnnotation(id: number) : Observable<void>{ - return this.delete(`${this.url}annotations/${id}`) + public deleteAnnotation(id: number) : Observable<void> { + return this.delete(`${this.url}/annotations/${id}`) } public updateAnnotation(annotation: DomainAnnotation): Observable<void> { - return this.put(`${this.url}annotations/${annotation.id}`, annotation) + return this.put(`${this.url}/annotations/${annotation.id}`, annotation) } } diff --git a/src/app/service/sshkey.service.ts b/src/app/service/sshkey.service.ts index f6b47b7245f782a9190f9bae5b6d1347cd398bdf..32424f472417e56ad29d71c83ff42cb5959f862b 100644 --- a/src/app/service/sshkey.service.ts +++ b/src/app/service/sshkey.service.ts @@ -26,4 +26,17 @@ export class SSHKeyService extends GenericDataService { public invalidate(id: number): Observable<any> { return this.delete(this.appConfig.getApiUrl() + '/user/keys/' + id); } + + public getAllByUserId(userId: number): Observable<SSHKeyView[]> { + return this.get<SSHKeyView[]>(this.appConfig.getApiUrl() + '/user/keys/view/' + userId); + } + + public createKeyForUser(request: SSHKeyRequest, userId: number ): Observable<any> { + return this.put<SSHKeyRequest, any>(this.appConfig.getApiUrl() + '/user/keys/view/' + userId, request); + } + + public invalidateUserKey(keyId: number, userId: number): Observable<any> { + return this.delete(this.appConfig.getApiUrl() + '/user/keys/view/' + userId + "/" + keyId); + } + } diff --git a/src/app/service/user.service.ts b/src/app/service/user.service.ts index 66db21d2b48f72e4a20521a5d2242480f955cc58..e139180f752173a84594c97211e16e8ddb17ae96 100644 --- a/src/app/service/user.service.ts +++ b/src/app/service/user.service.ts @@ -20,7 +20,7 @@ export class UserService extends GenericDataService { public getAll(domainId?: number): Observable<User[]> { return this.get<User[]>(domainId === undefined || domainId === this.domainService.getGlobalDomainId() ? - this.getUsersUrl() : this.getDomainUsersUrl(domainId)); + this.getUsersUrlWithoutDash() : this.getDomainUsersUrl(domainId)); } public getOne(userId: number, domainId?: number): Observable<User> { @@ -32,7 +32,7 @@ export class UserService extends GenericDataService { } public getRoles(userId: number, domainId?: number): Observable<UserRole[]> { - return this.get<UserRole[]>((domainId === undefined ? this.getUsersUrl() : this.getDomainUsersUrl(domainId)) + userId + '/roles'); + return this.get<UserRole[]>((domainId === undefined ? this.getUsersUrl() : this.getDomainUsersUrl(domainId)) + userId + '/roles'); } public updateUser(userId: number, user: User): Observable<any> { @@ -87,6 +87,11 @@ export class UserService extends GenericDataService { return this.appConfig.getApiUrl() + '/users/'; } + protected getUsersUrlWithoutDash(): string { + return this.appConfig.getApiUrl() + '/users'; + } + + protected getDomainUsersUrl(domainId: number): string { return this.appConfig.getApiUrl() + '/domains/' + domainId + '/users/'; } diff --git a/src/app/shared/about/about.component.css b/src/app/shared/about/about.component.css index cbf25d83e53161e2bfe9524f48abbe82c72e31e3..4a9f3de4150444470b0d301c1752aa16ef708229 100644 --- a/src/app/shared/about/about.component.css +++ b/src/app/shared/about/about.component.css @@ -16,3 +16,11 @@ padding-left: 15px; padding-right:15px; } +.position{ + display: flex; +} +@media (max-width: 991px){ + .position{ + flex-direction: column-reverse; + } +} diff --git a/src/app/shared/about/about.component.html b/src/app/shared/about/about.component.html index 624cbd8f01ae0b577f99c2acbb0643eb82124ec8..df6d0eb479e590e7436abc90906638ecbe4fb3d0 100644 --- a/src/app/shared/about/about.component.html +++ b/src/app/shared/about/about.component.html @@ -1,4 +1,4 @@ -<div class="col-md-offset-1 col-md-10 col-lg-offset-1 col-lg-10 row" style="padding-bottom: 80px; padding-top: 80px;"> +<div class="col-md-offset-1 col-md-10 col-lg-offset-1 col-lg-10 row position" style="padding-bottom: 80px; padding-top: 80px;"> <div class="col-md-6"> <div class="" style="padding-bottom: 15px;"> <h2>{{ 'ABOUT.CHANGELOG_TITLE' | translate }}</h2> @@ -41,16 +41,16 @@ <div class="col-md-6"> <div class="" style="padding-bottom: 15px;"> <h2>{{ 'ABOUT.CONTACT_TITLE' | translate }}</h2> - <span>{{ 'ABOUT.CONTACT_DESC' | translate}}</span> - <p>Join the mailing list: </p> + <p>Join the mailing list of your interest: </p> <ul> <li> - <p><a href="https://lists.geant.org/sympa/info/nmaas-announce">low frequency nmaas news</a> - Mailing list for nmaas-related announcements sent out by the nmaas Team members</p> + <p><a href="https://lists.geant.org/sympa/info/nmaas-announce">nmaas-announce</a> - for nmaas-related announcements sent out by the nmaas Team members,</p> </li> <li> - <p><a href="https://lists.geant.org/sympa/info/nmaas-users ">users discussion about nmaas</a> - Mailing list for message exchange between nmaas users and the nmaas Team</p> + <p><a href="https://lists.geant.org/sympa/info/nmaas-users">nmaas-users</a> - for discussions and message exchange between nmaas users and the nmaas Team.</p> </li> </ul> + <span>{{ 'ABOUT.CONTACT_DESC' | translate}}</span> </div> <div class="panel panel-default"> <div class="panel-body"> diff --git a/src/app/shared/page-not-found/page-not-found.component.css b/src/app/shared/page-not-found/page-not-found.component.css index 81255a0b4ffd0b6f3f6e72de771581796a4648da..d1bc030c6435a8816481bec18f3bc499207c5439 100644 --- a/src/app/shared/page-not-found/page-not-found.component.css +++ b/src/app/shared/page-not-found/page-not-found.component.css @@ -1,5 +1,5 @@ .container-fluid { - min-height: 100vh; + min-height: 55vh; padding: 25px; background-repeat: no-repeat; background-color: rgb(255, 255, 255); diff --git a/src/app/shared/page-not-found/page-not-found.component.html b/src/app/shared/page-not-found/page-not-found.component.html index 64b11d2695c7b7e0c9db9c59d82fe8b5b026bc11..48dfe9e1d3a6916e20f9c7da4f431064c0012576 100644 --- a/src/app/shared/page-not-found/page-not-found.component.html +++ b/src/app/shared/page-not-found/page-not-found.component.html @@ -3,7 +3,7 @@ <img src="assets/images/error/404.gif" width="60%"> </div> <div class="row"> - <h1>Oops! Something went wrong.</h1> + <h1>Oops! Something went wrong (404).</h1> <p>Our robot couldn't find this page. Try a different link or head back to the homepage.</p> <button class="btn btn-primary" type="button" (click)="redirect()">Go back</button> </div> diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 038dac09fc2107920049cc71f42f0e12aad504b6..b1f373c8a0f49f624fa8e4628df4e5888f290e8c 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -60,6 +60,7 @@ import {SortableHeaderDirective} from '../service/sort-domain.directive'; import {InputTextModule} from 'primeng/inputtext'; import { DomainNamespaceAnnotationsComponent } from './domain-namespace-annotations/domain-namespace-annotations.component'; import { provideZxvbnServiceForPSM } from 'angular-password-strength-meter/zxcvbn'; +import { AccessTokensComponent } from './users/access-token/access-tokens.component'; import { LeftMenuComponent } from './left-menu/left-menu.component'; import {TableModule} from 'primeng/table'; @@ -127,6 +128,7 @@ import {TableModule} from 'primeng/table'; PreferencesComponent, SortableHeaderDirective, DomainNamespaceAnnotationsComponent, + AccessTokensComponent ], providers: [ PasswordValidator, @@ -177,7 +179,8 @@ import {TableModule} from 'primeng/table'; ModalProvideSshKeyComponent, PreferencesComponent, SortableHeaderDirective, - DomainNamespaceAnnotationsComponent + DomainNamespaceAnnotationsComponent, + AccessTokensComponent ] }) export class SharedModule { diff --git a/src/app/shared/test-utils.ts b/src/app/shared/test-utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..41f9b1914dbfc1d24dc56337bce64bd10d1c40a6 --- /dev/null +++ b/src/app/shared/test-utils.ts @@ -0,0 +1,31 @@ +import { convertToParamMap, ParamMap, Params } from "@angular/router"; +import { Observable, ReplaySubject } from "rxjs"; + +export class ActivatedRouteStub { + // Use a ReplaySubject to share previous values with subscribers + // and pump new values into the `paramMap` observable + private readonly subject = new ReplaySubject<ParamMap>(); + private readonly subjectQuery = new ReplaySubject<ParamMap>(); + snapshot = {}; + + constructor(initialParams?: Params, initialQueryParams?: Params) { + this.setParamMap(initialParams); + } + + /** The mock paramMap observable */ + readonly paramMap = this.subject.asObservable(); + readonly queryParamMap = this.subjectQuery.asObservable(); + + /** Set the paramMap observables's next value */ + setParamMap(params?: Params) { + this.subject.next(convertToParamMap(params)); + } + + setQueryParamMap(params?: Params) { + this.subjectQuery.next(convertToParamMap(params)); + } + + get queryParams() : Observable<ParamMap> { + return this.subject.asObservable(); + } + } \ No newline at end of file diff --git a/src/app/shared/users/access-token/access-token.ts b/src/app/shared/users/access-token/access-token.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e0eb1e41bbaf717eedf8e5b4d1982b298f960b3 --- /dev/null +++ b/src/app/shared/users/access-token/access-token.ts @@ -0,0 +1,7 @@ +export class AccessToken { + public id: number + public name: string + public userId: number + public tokenValue: string + public valid: boolean +} \ No newline at end of file diff --git a/src/app/shared/users/access-token/access-tokens.component.html b/src/app/shared/users/access-token/access-tokens.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e58fa61efbc62b07e6e9692877deb12fc3288b4c --- /dev/null +++ b/src/app/shared/users/access-token/access-tokens.component.html @@ -0,0 +1,66 @@ +<div style="margin-bottom: 15px;" class="panel panel-default"> + <div class="panel-heading">{{'TOKENS.HEADER' | translate}}</div> + <div class="panel-body"> + <table class="table table-hover" aria-describedby="User access tokens table"> + <thead> + <tr> + <th scope="col">{{'TOKENS.TABLE.ID' | translate}}</th> + <th scope="col">{{'TOKENS.TABLE.NAME' | translate}}</th> + <th scope="col">{{'TOKENS.TABLE.VALUE' | translate}}</th> + <th scope="col">{{'TOKENS.TABLE.VALID' | translate}}</th> + <th scope="col">{{'TOKENS.TABLE.ACTIONS' | translate}}</th> + </tr> + </thead> + <tbody> + <tr *ngFor="let token of tokensList"> + <td>{{token.id}}</td> + <td>{{token.name}}</td> + <td>{{token.tokenValue}}</td> + <td>{{token.valid}}</td> + <td *ngIf="token.valid"> + <button type="button" class="btn btn-danger" + (click)="invalidate(token.id)">{{'TOKENS.BUTTON_INVALIDATE' | translate}}</button> + </td> + <td *ngIf="!token.valid"> + <button type="button" class="btn btn-danger" + (click)="deleteToken(token.id)">{{'TOKENS.BUTTON_DELETE' | translate}}</button> + </td> + </tr> + <tr *ngIf="tokensList.length === 0"> + <td colspan="3" style="text-align: center">{{'TOKENS.NO_TOKENS' | translate}}</td> + </tr> + </tbody> + </table> + <div> + <button type="button" class="btn btn-success" + (click)="modal.show()">{{'TOKENS.NEW_TOKEN' | translate}}</button> + + </div> + </div> +</div> + +<nmaas-modal styleModal="info"> + <div class="nmaas-modal-header">{{'TOKENS.MODAL.HEADER' | translate}}</div> + <div class="nmaas-modal-body" style="height: 60%; max-height: 80vh;overflow-y: auto;"> + <form [formGroup]="requestForm" (ngSubmit)="createNewToken()"> + <div class="form-group"> + <label class="control-label" for="new-token-name"> + {{'TOKENS.MODAL.NAME' | translate}}: + </label> + <input id="new-token-name" type="text" class="form-control" formControlName="name"> + </div> + <div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger"> + <div *ngIf="name.errors.required">{{'SSH_KEYS.MODAL.ERROR.NAME_REQUIRED' | translate}}</div> + <div *ngIf="name.errors.minlength">{{'SSH_KEYS.MODAL.ERROR.NAME_MINLENGTH' | translate}}</div> + <div *ngIf="name.errors.maxlength">{{'SSH_KEYS.MODAL.ERROR.NAME_MAXLENGTH' | translate}}</div> + <div *ngIf="name.errors.notUnique">{{name.errors.message}}</div> + + </div> + <input type="submit" class="btn btn-success" value="{{'SSH_KEYS.MODAL.BUTTON_ADD' | translate}}" + [disabled]="!requestForm.valid"> + <button type="button" class="btn btn-primary pull-right" + (click)="modal.hide()">{{'SSH_KEYS.MODAL.BUTTON_CANCEL' | translate}}</button> + + </form> + </div> +</nmaas-modal> \ No newline at end of file diff --git a/src/app/shared/users/access-token/access-tokens.component.spec.ts b/src/app/shared/users/access-token/access-tokens.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..98e26efb7ba9c778fb53e550ba27eec87ff1677f --- /dev/null +++ b/src/app/shared/users/access-token/access-tokens.component.spec.ts @@ -0,0 +1,38 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {AccessTokensComponent} from './access-tokens.component'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {TranslateFakeLoader, TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import {ReactiveFormsModule} from '@angular/forms'; +import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('AccessTokensComponent', () => { + let component: AccessTokensComponent; + let fixture: ComponentFixture<AccessTokensComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AccessTokensComponent], + imports: [ + HttpClientTestingModule, + ReactiveFormsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateFakeLoader + } + }), + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA], + }) + .compileComponents(); + + fixture = TestBed.createComponent(AccessTokensComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/src/app/shared/users/access-token/access-tokens.component.ts b/src/app/shared/users/access-token/access-tokens.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c901eb760d28787fd0d3209eaf474a858ec8f588 --- /dev/null +++ b/src/app/shared/users/access-token/access-tokens.component.ts @@ -0,0 +1,79 @@ + +import {Component, OnInit, ViewChild} from '@angular/core'; +import {Observable} from 'rxjs'; +import {AccessToken} from './access-token'; +import {AccessTokenService} from './access-tokens.service'; +import {ModalComponent} from '../../modal'; +import {UntypedFormBuilder, UntypedFormGroup, Validators} from '@angular/forms'; + +@Component({ + selector: 'app-access-tokens', + templateUrl: './access-tokens.component.html', + styleUrls: [] +}) +export class AccessTokensComponent implements OnInit { + + public tokens: Observable<AccessToken[]> = undefined; + public tokensList: AccessToken[] = []; + + public requestForm: UntypedFormGroup = undefined; + + public newTokenName = ''; + + @ViewChild(ModalComponent, {static: true}) + public readonly modal: ModalComponent; + + constructor(private tokenService: AccessTokenService, + private formBuilder: UntypedFormBuilder) { + } + + ngOnInit() { + this.requestForm = this.formBuilder.group({ + name: ['', [Validators.required, Validators.minLength(1), Validators.maxLength(16)]], + }) + + this.tokens = this.tokenService.getAll(); + this.getData(); + } + + getData() { + this.tokensList = []; + this.tokens.subscribe( + data => this.tokensList.push(...data), + error => console.error(error) + ) + } + + invalidate(id: number) { + this.tokenService.invalidate(id).subscribe( + (_) => this.getData(), + error => console.error(error) + ); + } + + deleteToken(id: number) { + this.tokenService.deleteToken(id).subscribe( + (_) => this.getData(), + error => console.error(error.err) + ); + } + + public createNewToken() { + this.tokenService.createToken(this.requestForm.value.name.trim()).subscribe({ + next: val => { + this.tokensList.push(val) + this.requestForm.reset(); + this.modal.hide(); + }, + error: err => { + console.warn(err.error) + this.requestForm.controls['name'].setErrors({notUnique: true, message: err.error}); + console.log(this.requestForm) + } + }) + } + + get name() { + return this.requestForm.get('name'); + } +} \ No newline at end of file diff --git a/src/app/shared/users/access-token/access-tokens.service.ts b/src/app/shared/users/access-token/access-tokens.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3c8dec74292a256c24e33a3bade736e7928ec5b --- /dev/null +++ b/src/app/shared/users/access-token/access-tokens.service.ts @@ -0,0 +1,36 @@ +import {Observable} from 'rxjs'; +import {AccessToken} from './access-token'; +import {Injectable} from '@angular/core'; +import {GenericDataService} from '../../../service/genericdata.service'; +import {HttpClient} from '@angular/common/http'; +import {AppConfigService} from '../../../service'; + +@Injectable({ + providedIn: 'root' +}) +export class AccessTokenService extends GenericDataService { + + constructor(http: HttpClient, appConfig: AppConfigService) { + super(http, appConfig); + } + + public getAll(): Observable<AccessToken[]> { + return this.http.get<AccessToken[]>(this.getUrl()) + } + + public invalidate(id: number): Observable<void> { + return this.http.put<void>(`${this.getUrl()}/${id}`, '') + } + + public deleteToken(id: number): Observable<void> { + return this.http.put<void>(`${this.getUrl()}/delete/${id}`, '') + } + + public createToken(tokenName: string): Observable<AccessToken> { + return this.http.post<AccessToken>(this.getUrl(), tokenName) + } + + private getUrl(): string { + return this.appConfig.getApiUrl() + '/tokens'; + } +} \ No newline at end of file diff --git a/src/app/shared/users/new-ssh-key/new-ssh-key.component.ts b/src/app/shared/users/new-ssh-key/new-ssh-key.component.ts index 619104369b0f33a17d8f96fb4aaa925f0d1a573a..191b25ae717054a9c62fa618cadd1568f2fb31db 100644 --- a/src/app/shared/users/new-ssh-key/new-ssh-key.component.ts +++ b/src/app/shared/users/new-ssh-key/new-ssh-key.component.ts @@ -1,4 +1,4 @@ -import {Component, EventEmitter, OnInit, Output, ViewChild} from '@angular/core'; +import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core'; import {SSHKeyService} from '../../../service/sshkey.service'; import {SSHKeyRequest} from '../../../model/sshkey-request'; import {UntypedFormBuilder, UntypedFormGroup, Validators} from '@angular/forms'; @@ -17,6 +17,12 @@ export class NewSshKeyComponent implements OnInit { @Output() public out: EventEmitter<any> = new EventEmitter<any>(); + @Input() + public userMode = false; + + @Input() + public userId : number; + public error: string = undefined; public requestForm: UntypedFormGroup = undefined; @@ -38,17 +44,34 @@ export class NewSshKeyComponent implements OnInit { console.log(this.requestForm); request.name = this.requestForm.value.name.trim(); request.key = this.requestForm.value.key.trim(); + if(this.userMode) { + if(this.userId !== null) { + this.keyService.createKeyForUser(request, this.userId).subscribe( + data => { + this.error = undefined; + this.requestForm.reset(); + this.modal.hide(); + this.out.emit(); + }, + error => { + this.error = error.message; + } + ); + } + } else { // profile view this.keyService.createKey(request).subscribe( - data => { - this.error = undefined; - this.requestForm.reset(); - this.modal.hide(); - this.out.emit(); - }, - error => { - this.error = error.message; - } - ); + data => { + this.error = undefined; + this.requestForm.reset(); + this.modal.hide(); + this.out.emit(); + }, + error => { + this.error = error.message; + } + ); + } + } get name() { diff --git a/src/app/shared/users/ssh-keys/ssh-keys.component.html b/src/app/shared/users/ssh-keys/ssh-keys.component.html index 2e8af662c2f5fde4d5d4b93737b90f7831a079e3..e9025bddb5cd7796a569b04f68bb8cd26b94c772 100644 --- a/src/app/shared/users/ssh-keys/ssh-keys.component.html +++ b/src/app/shared/users/ssh-keys/ssh-keys.component.html @@ -1,11 +1,8 @@ -<div style="padding-bottom: 15px;" class="background-section"> - <div style="display: flex; justify-content:space-between"> - <h4 style="font-size:15px; font-weight: bold" >{{'SSH_KEYS.HEADER' | translate}}</h4> - <app-new-ssh-key (out)="getData()"></app-new-ssh-key> - </div> +<div style="margin-bottom: 15px;" class="panel panel-default"> + <div class="panel-heading">{{'SSH_KEYS.HEADER' | translate}}</div> <div class="panel-body"> <div class="flex justify-content-end mb-4"> - + <app-new-ssh-key [userMode]="userMode" [userId]="userId" (out)="getData()"></app-new-ssh-key> </div> <table class="table table-hover" aria-describedby="User ssh keys table"> <thead> diff --git a/src/app/shared/users/ssh-keys/ssh-keys.component.ts b/src/app/shared/users/ssh-keys/ssh-keys.component.ts index 8bee8e6e677c36590e4fcd6a4b9a71725f0ec2c9..7def6034efd3074b784ac160800ff63b92dc70c5 100644 --- a/src/app/shared/users/ssh-keys/ssh-keys.component.ts +++ b/src/app/shared/users/ssh-keys/ssh-keys.component.ts @@ -1,4 +1,4 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; import {SSHKeyService} from '../../../service/sshkey.service'; import {Observable} from 'rxjs'; import {SSHKeyView} from '../../../model/sshkey-view'; @@ -8,16 +8,37 @@ import {SSHKeyView} from '../../../model/sshkey-view'; templateUrl: './ssh-keys.component.html', styleUrls: ['./ssh-keys.component.css'] }) -export class SshKeysComponent implements OnInit { +export class SshKeysComponent implements OnInit, OnChanges { public keys: Observable<SSHKeyView[]> = undefined; public keysList: SSHKeyView[] = []; + @Input() + public userMode = false; + + @Input() + public userId : number; + constructor(private keyService: SSHKeyService) { } ngOnInit() { - this.keys = this.keyService.getAll(); - this.getData(); + if(this.userMode) { + if(this.userId !== null) { + this.keys = this.keyService.getAllByUserId(this.userId); + this.getData(); + } + } else { // profile view + this.keys = this.keyService.getAll(); + this.getData(); + } + + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['userId']) { + this.ngOnInit(); + console.log('Nowa wartość userId:', this.userId); + } } getData() { @@ -33,15 +54,28 @@ export class SshKeysComponent implements OnInit { } invalidate(id: number) { - this.keyService.invalidate(id).subscribe( - data => { - console.log('invalidating ssh key id: ' + id + ' success'); - this.getData(); - }, - error => { - console.error(error); - } - ); + if(this.userMode) { + this.keyService.invalidateUserKey(id, this.userId).subscribe( + data => { + console.log('invalidating ssh key id: ' + id + ' success'); + this.getData(); + }, + error => { + console.error(error); + } + ); + } else { + this.keyService.invalidate(id).subscribe( + data => { + console.log('invalidating ssh key id: ' + id + ' success'); + this.getData(); + }, + error => { + console.error(error); + } + ); + } + } } diff --git a/src/app/welcome/login/login.component.html b/src/app/welcome/login/login.component.html index 27418cc9a946d4901ca623c444d8dde34dfc66ca..7fd223061e3430e40066581208df5f152c1d463a 100644 --- a/src/app/welcome/login/login.component.html +++ b/src/app/welcome/login/login.component.html @@ -26,8 +26,7 @@ </fieldset> </form> <div class="form-group"> - <button type="submit" (click)="triggerSSO()" class="btn btn-primary btn-block" - [disabled]="!this.configuration?.ssoLoginAllowed || loading || ssoLoading"> + <button type="submit" (click)="triggerOIDC()" class="btn btn-primary btn-block"> {{ 'LOGIN.LOGIN_WITH' | translate }} </button> <img alt="sso" *ngIf="ssoLoading" src="data:"/> diff --git a/src/app/welcome/login/login.component.spec.ts b/src/app/welcome/login/login.component.spec.ts index 0fd649ba25d4732652f9919c17045d22051cf1e4..35e39bc620879877c64f1d11636786124b0439f8 100644 --- a/src/app/welcome/login/login.component.spec.ts +++ b/src/app/welcome/login/login.component.spec.ts @@ -10,6 +10,7 @@ import {ConfigurationService, UserService} from '../../service'; import {SSOService} from '../../service/sso.service'; import createSpyObj = jasmine.createSpyObj; import {of} from 'rxjs'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('Component: Login', () => { @@ -28,6 +29,7 @@ describe('Component: Login', () => { FormsModule, ReactiveFormsModule, RouterTestingModule, + HttpClientTestingModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, diff --git a/src/app/welcome/login/login.component.ts b/src/app/welcome/login/login.component.ts index f82e1de1e556d50bb80c48f8d1d624abdf883b80..dbfcc78194a47a0585edbfd2d35d4b8ee52d32ff 100644 --- a/src/app/welcome/login/login.component.ts +++ b/src/app/welcome/login/login.component.ts @@ -2,7 +2,7 @@ import {Component, OnInit, ViewChild, ViewEncapsulation} from '@angular/core'; import {Router} from '@angular/router'; import {AuthService} from '../../auth/auth.service'; -import {ConfigurationService, UserService} from '../../service'; +import {AppConfigService, ConfigurationService, UserService} from '../../service'; import {Configuration} from '../../model/configuration'; import {SSOService} from '../../service/sso.service'; import {SSOConfig} from '../../model/sso'; @@ -37,7 +37,8 @@ export class LoginComponent implements OnInit { private ssoService: SSOService, private fb: UntypedFormBuilder, private userService: UserService, - private translate: TranslateService) { + private translate: TranslateService, + private appConfig: AppConfigService) { this.resetPasswordForm = fb.group({ email: ['', [Validators.required, Validators.email]] }); @@ -71,6 +72,9 @@ export class LoginComponent implements OnInit { ); } + public triggerOIDC() { + window.location.href = this.appConfig.getOidcUrl(); + } public checkSSO() { const params = this.router.parseUrl(this.router.url).queryParams; diff --git a/src/app/welcome/profile/profile.component.html b/src/app/welcome/profile/profile.component.html index 2a2623b44e76bee141239b93330e39675dca2978..a62e21156ef7784524d5b4106c17a1671874a87f 100644 --- a/src/app/welcome/profile/profile.component.html +++ b/src/app/welcome/profile/profile.component.html @@ -34,5 +34,6 @@ [allowedModes]="[ComponentMode.PROFILVIEW, ComponentMode.EDIT]"> </nmaas-userprivileges> <nmaas-ssh-keys></nmaas-ssh-keys> + <app-access-tokens></app-access-tokens> </div> </div>