diff options
Diffstat (limited to 'src/app/modules')
45 files changed, 2915 insertions, 0 deletions
diff --git a/src/app/modules/alerting/alert.component.css b/src/app/modules/alerting/alert.component.css new file mode 100644 index 0000000..aeadd64 --- /dev/null +++ b/src/app/modules/alerting/alert.component.css @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +span { + width: 500px; +} +.alert-success { + color: #6bb324 !important; +} + +.alert-success > button.close::before { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M15.81,14.99l-6.99-7l6.99-7c0.24-0.24,0.2-0.63-0.04-0.83c-0.24-0.2-0.59-0.2-0.79,0l-6.99,7l-6.99-7 C0.75-0.08,0.36-0.04,0.16,0.2c-0.2,0.24-0.2,0.59,0,0.79l6.99,7l-6.99,7c-0.24,0.24-0.2,0.63,0.04,0.83c0.24,0.2,0.59,0.2,0.79,0 l6.99-7l6.99,7c0.24,0.24,0.59,0.24,0.83,0.04C16.04,15.66,16.08,15.26,15.81,14.99C15.85,15.03,15.81,15.03,15.81,14.99z' fill='%236bb324'/%3E%3C/svg%3E") !important; +} +.alert-info { + color: #00a0de !important; +} + +.alert-info > button.close::before { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M15.81,14.99l-6.99-7l6.99-7c0.24-0.24,0.2-0.63-0.04-0.83c-0.24-0.2-0.59-0.2-0.79,0l-6.99,7l-6.99-7 C0.75-0.08,0.36-0.04,0.16,0.2c-0.2,0.24-0.2,0.59,0,0.79l6.99,7l-6.99,7c-0.24,0.24-0.2,0.63,0.04,0.83c0.24,0.2,0.59,0.2,0.79,0 l6.99-7l6.99,7c0.24,0.24,0.59,0.24,0.83,0.04C16.04,15.66,16.08,15.26,15.81,14.99C15.85,15.03,15.81,15.03,15.81,14.99z' fill='%2300a0de'/%3E%3C/svg%3E") !important; +} +.alert-warning { + color: #87604e !important; + border-color: #87604e !important; +} + +.alert-warning > button.close::before { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M15.81,14.99l-6.99-7l6.99-7c0.24-0.24,0.2-0.63-0.04-0.83c-0.24-0.2-0.59-0.2-0.79,0l-6.99,7l-6.99-7 C0.75-0.08,0.36-0.04,0.16,0.2c-0.2,0.24-0.2,0.59,0,0.79l6.99,7l-6.99,7c-0.24,0.24-0.2,0.63,0.04,0.83c0.24,0.2,0.59,0.2,0.79,0 l6.99-7l6.99,7c0.24,0.24,0.59,0.24,0.83,0.04C16.04,15.66,16.08,15.26,15.81,14.99C15.85,15.03,15.81,15.03,15.81,14.99z' fill='%2387604E'/%3E%3C/svg%3E") !important; +} +.alert-danger { + color: #d90000 !important; +} + +.alert-danger > button.close::before { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M15.81,14.99l-6.99-7l6.99-7c0.24-0.24,0.2-0.63-0.04-0.83c-0.24-0.2-0.59-0.2-0.79,0l-6.99,7l-6.99-7 C0.75-0.08,0.36-0.04,0.16,0.2c-0.2,0.24-0.2,0.59,0,0.79l6.99,7l-6.99,7c-0.24,0.24-0.2,0.63,0.04,0.83c0.24,0.2,0.59,0.2,0.79,0 l6.99-7l6.99,7c0.24,0.24,0.59,0.24,0.83,0.04C16.04,15.66,16.08,15.26,15.81,14.99C15.85,15.03,15.81,15.03,15.81,14.99z' fill='%23d90000'/%3E%3C/svg%3E") !important; +} + +.custom-margin { + margin-right: 20px; +} + +i.bi { + font-size: 22px; +} + +.text-breaking { + word-break: break-word; +} diff --git a/src/app/modules/alerting/alert.component.html b/src/app/modules/alerting/alert.component.html new file mode 100644 index 0000000..157966f --- /dev/null +++ b/src/app/modules/alerting/alert.component.html @@ -0,0 +1,106 @@ +<!-- + ~ Copyright (c) 2022. Deutsche Telekom AG + ~ + ~ 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. + ~ + ~ SPDX-License-Identifier: Apache-2.0 + --> + + +<div class="d-flex justify-content-center"> + <span> + <ngb-alert *ngFor="let alert of alerts" type="alert" class="{{ cssClass(alert) }}" [dismissible]="false"> + <button + type="button" + class="close" + [attr.aria-label]="'common.buttons.close' | translate" + (click)="removeAlert(alert)" + ></button> + <div class="d-flex text-breaking"> + <i + class="bi custom-margin" + [class.bi-info-circle-fill]="informativeAlerts.includes(alert.type)" + [class.bi-exclamation-triangle-fill]="!informativeAlerts.includes(alert.type)" + aria-hidden="true" + ></i> + + <div *ngIf="alert.type === AlertType.Error"> + <ng-container *ngIf="alert.id === 'keycloak'; else defaultErrorAlert"> + <span>{{ alert.message }}</span> + <ng-container *ngTemplateOutlet="supportTpl"></ng-container> + </ng-container> + </div> + + <div *ngIf="alert.type !== AlertType.Error"> + <span class="text-justify">{{ alert.message }}</span> + <ng-container *ngIf="alert.id === 'onap_logging'"> + <span>{{ 'common.alert.contactSupport.part1' | translate }}</span> + <a [href]="environment.supportUrlLink">{{ 'common.alert.support' | translate }}</a> + </ng-container> + </div> + </div> + + <ng-template #defaultErrorAlert> + <span *ngIf="alert.urlTree">{{ alert.message }}</span> + <span *ngIf="!alert.errorDetail">{{ alert.message }}</span> + <div *ngIf="alert?.errorDetail?.downstreamSystem as downstreamSystem"> + <span *ngIf="downstreamSystem"> + {{ 'common.alert.errorReporter' | translate: { system: 'common.systems.' + downstreamSystem | translate } }} + </span> + </div> + <div *ngIf="alert.errorDetail?.detail as detail"> + "{{ alert.errorDetail?.detail }}" + <div + *ngIf=" + alert.errorDetail?.downstreamSystem === DownstreamSystem.KEYCLOAK && + alert.errorDetail?.downstreamStatus === 409 + " + > + <span *ngIf="detail.split(' ').pop() === 'username'"> + {{ 'common.block.userAdministration.helpUserNameExists' | translate }} + </span> + <span *ngIf="detail.split(' ').pop() === 'email'"> + {{ 'common.block.userAdministration.helpUserEmailExists' | translate }} + </span> + </div> + </div> + <ng-container *ngTemplateOutlet="supportTpl"></ng-container> + </ng-template> + <ng-template #supportTpl> + <div> + {{ 'common.alert.support' | translate }} + <button + class="btn btn-sm p-0" + (click)="collapse.toggle()" + [attr.aria-expanded]="!isCollapsed" + aria-controls="collapseSupportInfo" + > + <i *ngIf="isCollapsed" class="bi bi-chevron-right text-danger" style="font-size: 18px" aria-hidden="true" [attr.aria-label]="'common.buttons.openSupportLink' | translate"></i> + <i *ngIf="!isCollapsed" class="bi bi-chevron-down text-danger" style="font-size: 18px" aria-hidden="true" [attr.aria-label]="'common.buttons.closeSupportLink' | translate"></i> + </button> + </div> + + <div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed"> + <span>{{ 'common.alert.contactSupport.part1' | translate }}</span + ><a [href]="environment.supportUrlLink" target="_blank">{{ 'common.alert.support' | translate }}</a> + <ng-container *ngIf="alert?.requestId"> + <span>{{ 'common.alert.contactSupport.part2' | translate }}</span> + <div> + {{ alert?.requestId }} + </div> + </ng-container> + </div> + </ng-template> + </ngb-alert> + </span> +</div> diff --git a/src/app/modules/alerting/alert.component.spec.ts b/src/app/modules/alerting/alert.component.spec.ts new file mode 100644 index 0000000..abaf52e --- /dev/null +++ b/src/app/modules/alerting/alert.component.spec.ts @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { AlertComponent } from './alert.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +describe('AlertComponent', () => { + let component: AlertComponent; + let fixture: ComponentFixture<AlertComponent>; + const router = jasmine.createSpyObj('Router', ['navigate']); + beforeEach(async(() => { + TestBed.configureTestingModule({ + providers: [AlertComponent, { provide: Router, useValue: router }], + }).compileComponents(); + fixture = TestBed.createComponent(AlertComponent); + component = fixture.componentInstance; + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('Setting value to id properties', () => { + component.id = 'testId'; + fixture.detectChanges(); + }); + it('Setting value to fade properties', () => { + expect(component.fade).toBe(true); + component.fade = false; + fixture.detectChanges(); + }); +}); diff --git a/src/app/modules/alerting/alert.component.ts b/src/app/modules/alerting/alert.component.ts new file mode 100644 index 0000000..91d22f4 --- /dev/null +++ b/src/app/modules/alerting/alert.component.ts @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Component, Input, OnInit } from '@angular/core'; +import { NavigationStart, Router } from '@angular/router'; +import { Subscription } from 'rxjs'; + +import { Alert, AlertType } from './alert.model'; +import { AlertService } from './alert.service'; +import { UnsubscribeService } from 'src/app/services/unsubscribe/unsubscribe.service'; +import { takeUntil } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; +import { Problem } from '../../../../openapi/output'; +import DownstreamSystemEnum = Problem.DownstreamSystemEnum; + +@Component({ + selector: 'app-alert', + templateUrl: 'alert.component.html', + styleUrls: ['alert.component.css'], + providers: [UnsubscribeService], +}) +export class AlertComponent implements OnInit { + @Input() id = 'default-alert'; + @Input() fade = true; + + isCollapsed = true; + informativeAlerts: AlertType[] = [AlertType.Success, AlertType.Info]; + alerts: Alert[] = []; + alertSubscription!: Subscription; + routeSubscription!: Subscription; + AlertType = AlertType; + environment = environment; + DownstreamSystem = DownstreamSystemEnum; + constructor( + private router: Router, + private alertService: AlertService, + private unsubscribeService: UnsubscribeService, + ) {} + + ngOnInit() { + // subscribe to new alert notifications + this.alertSubscription = this.alertService.alerts + .pipe(takeUntil(this.unsubscribeService.unsubscribe$)) + .subscribe(alert => { + // clear alerts when an empty alert is received + if (!alert.message) { + // filter out alerts without 'keepAfterRouteChange' flag + this.alerts = this.alerts.filter(x => x.keepAfterRouteChange); + + // remove 'keepAfterRouteChange' flag on the rest + this.alerts.forEach(x => delete x.keepAfterRouteChange); + return; + } + if (this.alerts.filter(a => a.message === alert.message).length === 0) { + // add alert to array + this.alerts.push(alert); + } + // auto close alert if required + if (alert.type === AlertType.Warning) { + setTimeout(() => this.removeAlert(alert), 10000); + } + }); + + // clear alerts on location change + this.routeSubscription = this.router.events + .pipe(takeUntil(this.unsubscribeService.unsubscribe$)) + .subscribe(event => { + if (event instanceof NavigationStart) { + this.alertService.clear(this.id); + } + }); + } + + removeAlert(alert: Alert) { + // check if already removed to prevent error on auto close + if (!this.alerts.includes(alert)) { + return; + } + + if (this.fade) { + // fade out alert + this.alerts.find(x => x === alert)!.fade = true; + + // remove alert after faded out + setTimeout(() => { + this.alerts = this.alerts.filter(x => x !== alert); + }, 250); + } else { + // remove alert + this.alerts = this.alerts.filter(x => x !== alert); + } + } + + cssClass(alert: Alert) { + if (!alert) { + return; + } + + const classes = ['show', 'alert', 'alert-dismissable']; + + const alertTypeClass = { + /* + [AlertType.Success]: 'alert alert-success', + [AlertType.Error]: 'alert alert-danger', + [AlertType.Info]: 'alert alert-info', + [AlertType.Warning]: 'alert alert-warning' + */ + [AlertType.Success]: 'alert-success', + [AlertType.Error]: 'alert-danger', + [AlertType.Info]: 'alert-info', + [AlertType.Warning]: 'alert-warning', + }; + + classes.push(alertTypeClass[alert.type]); + + if (alert.fade) { + classes.push('fade'); + } + + return classes.join(' '); + } +} diff --git a/src/app/modules/alerting/alert.model.ts b/src/app/modules/alerting/alert.model.ts new file mode 100644 index 0000000..6e280ce --- /dev/null +++ b/src/app/modules/alerting/alert.model.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Inject, Injectable } from '@angular/core'; +import { Problem } from '../../../../openapi/output'; + +@Injectable({ providedIn: 'root' }) +export class Alert { + id?: string; + type!: AlertType; + message?: string; + autoClose?: boolean; + keepAfterRouteChange?: boolean; + fade?: boolean; + errorDetail?: Problem; + requestId?: string; + urlTree?: string[] + + constructor(@Inject(Alert) init?: Partial<Alert>) { + Object.assign(this, init); + } +} + + + +export enum AlertType { + Success, + Error, + Info, + Warning, +} diff --git a/src/app/modules/alerting/alert.module.ts b/src/app/modules/alerting/alert.module.ts new file mode 100644 index 0000000..064bb32 --- /dev/null +++ b/src/app/modules/alerting/alert.module.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { NgModule } from '@angular/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { CommonModule } from '@angular/common'; + +import { AlertComponent } from './alert.component'; +import { TranslateModule } from '@ngx-translate/core'; + +@NgModule({ + imports: [CommonModule, NgbModule, TranslateModule], + declarations: [AlertComponent], + exports: [AlertComponent], +}) +export class AlertModule {} diff --git a/src/app/modules/alerting/alert.service.spec.ts b/src/app/modules/alerting/alert.service.spec.ts new file mode 100644 index 0000000..5c9d219 --- /dev/null +++ b/src/app/modules/alerting/alert.service.spec.ts @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +// https://dev.to/coly010/unit-testing-angular-services-1anm +import { Alert, AlertType } from './alert.model'; +import { TestBed } from '@angular/core/testing'; +import { AlertModule } from './alert.module'; +import { AlertService } from './alert.service'; +import { Subject } from 'rxjs'; +import SpyObj = jasmine.SpyObj; + +/** + * describe sets up the Test Suite for the TileService + */ +describe('AlertService', () => { + let service: AlertService; + let mockAlert: Alert; + let message: string; + let spyAlert: SpyObj<any>; + let subject: Subject<Alert>; + + /** + * beforeEach tells the test runner to run this code before every test in the Test Suite + * It is using Angular's TestBed to create the testing environment and finally it is injecting the TilesService + * and placing a reference to it in the service variable defined earlier. + */ + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AlertService, AlertModule, Subject], + }); + service = TestBed.inject(AlertService); + subject = TestBed.inject(Subject); + mockAlert = TestBed.inject(Alert); + spyAlert = spyOn(service, 'alert'); + message = 'This is a test-alert'; + mockAlert.message = message; + }); + + it('should be create', () => { + expect(service).toBeTruthy(); + }); + /** + * tests for the alert methods info, warning, error and success with a spyobject + */ + it('should return success alert', () => { + mockAlert.type = AlertType.Success; + service.success(message); + expect(spyAlert).toHaveBeenCalledWith(mockAlert); + }); + + it('should return warning alert', () => { + mockAlert.type = AlertType.Warning; + service.warn(message); + expect(spyAlert).toHaveBeenCalledWith(mockAlert); + }); + + it('should return error alert', () => { + mockAlert.type = AlertType.Error; + service.error(message); + expect(spyAlert).toHaveBeenCalledWith(mockAlert); + }); + + it('should return info alert', () => { + mockAlert.type = AlertType.Info; + service.info(message); + expect(spyAlert).toHaveBeenCalledWith(mockAlert); + }); + + it('clear ', () => { + subject = service['subject']; + const spy = spyOn(subject, 'next'); + const alert = new Alert(); + alert.id = 'default-alert'; + service.clear(); + expect(spy).toHaveBeenCalledWith(alert); + }); +}); diff --git a/src/app/modules/alerting/alert.service.ts b/src/app/modules/alerting/alert.service.ts new file mode 100644 index 0000000..4d81397 --- /dev/null +++ b/src/app/modules/alerting/alert.service.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Injectable } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +import { Alert, AlertType } from './alert.model'; + +@Injectable({ providedIn: 'root' }) +export class AlertService { + private subject = new Subject<Alert>(); + private defaultId = 'default-alert'; + + // enable subscribing to alerts observable + onAlert(id = this.defaultId): Observable<Alert> { + return this.subject.asObservable().pipe(filter(x => x && x.id === id)); + } + get alerts() { + return this.subject; + } + + // convenience methods + success(message: string, options?: Partial<Alert>) { + this.alert(new Alert({ ...options, type: AlertType.Success, message })); + } + + error(message: string, options?: Partial<Alert>) { + this.alert(new Alert({ ...options, type: AlertType.Error, message })); + } + + info(message: string, options?: Partial<Alert>) { + this.alert(new Alert({ ...options, type: AlertType.Info, message })); + } + + warn(message: string, options?: Partial<Alert>) { + this.alert(new Alert({ ...options, type: AlertType.Warning, message })); + } + + // main alert method + alert(alert: Alert) { + alert.id = alert.id || this.defaultId; + this.subject.next(alert); + } + + // clear alerts + clear(id = this.defaultId) { + this.subject.next(new Alert({ id })); + } +} diff --git a/src/app/modules/alerting/index.ts b/src/app/modules/alerting/index.ts new file mode 100644 index 0000000..492986c --- /dev/null +++ b/src/app/modules/alerting/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +export * from './alert.module'; +export * from './alert.service'; +export * from './alert.model'; diff --git a/src/app/modules/app-starter/app-starter-routing.module.ts b/src/app/modules/app-starter/app-starter-routing.module.ts new file mode 100644 index 0000000..6696d3a --- /dev/null +++ b/src/app/modules/app-starter/app-starter-routing.module.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AppStarterComponent } from './app-starter.component'; +import { AuthGuard } from '../../guards/auth.guard'; + +const routes: Routes = [{ path: '', component: AppStarterComponent, canActivate: [AuthGuard] }]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AppStarterRoutingModule {} diff --git a/src/app/modules/app-starter/app-starter.component.css b/src/app/modules/app-starter/app-starter.component.css new file mode 100644 index 0000000..8ec276c --- /dev/null +++ b/src/app/modules/app-starter/app-starter.component.css @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +.card-img-top { + width: 60%; + height: 5vw; + object-fit: contain; +} + +.card-deck > div { + display: flex; + flex: 1 0 0; + flex-direction: column; +} + +.card-deck > div:not(:last-child) { + margin-right: 15px; +} + +.card-deck { + width: 90%; + margin-left: 2%; +} + +.card-deck > div:not(:first-child) { + margin-left: 15px; +} + +.my-group-title { + color: var(--primary); +} + +.card { + border-radius: 20px; + cursor: pointer; + transition: 0.4s; + min-width: 200px; + max-width: 200px; + min-height: 250px; + max-height: 250px; + text-align: center; + margin-right: 2.25rem; +} + +.card-body { + padding-bottom: 0; +} + +.card-title { + min-height: 87px; + font-size: 14px; +} + +/* Works together with bootstraps responsive image class +https://stackoverflow.com/questions/53721711/how-to-set-responsive-images-max-width-bootstrap-4#53723494 +*/ +.img-max { + max-width: 115px; + width: 100%; +} + +.card:hover { + transform: scale(1.1, 1.1); + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); +} + +.disabled-card:hover { + transform: none !important; + box-shadow: none !important; + transition: none !important; +} + +a, +a:hover { + color: #262626; + text-decoration: none; +} + +a:hover { + cursor: pointer; +} + +.nav-tabs, +.nav-links { + border-bottom: 1px solid #b2b2b2; +} + +.nav-link { + background-color: transparent; +} +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: var(--primary); + background-color: #fff; + border-color: #b2b2b2 #b2b2b2 #fff; +} + +.nav-link:focus { + border-style: none; +} + +/* I will leave this for future purpose in case we will have disabled tiles in the Portal */ +/* .disabled-tiles { + opacity: 0.5; + cursor: not-allowed !important; +} */ diff --git a/src/app/modules/app-starter/app-starter.component.html b/src/app/modules/app-starter/app-starter.component.html new file mode 100644 index 0000000..aae2bf3 --- /dev/null +++ b/src/app/modules/app-starter/app-starter.component.html @@ -0,0 +1,47 @@ +<!-- + ~ Copyright (c) 2022. Deutsche Telekom AG + ~ + ~ 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. + ~ + ~ SPDX-License-Identifier: Apache-2.0 + --> + + +<app-breadcrumb> + <app-breadcrumb-item> + <a [routerLink]="['/dashboard']">{{ 'layout.menu.items.home' | translate }}</a> + </app-breadcrumb-item> + <app-breadcrumb-item> + <span aria-current="page">{{ 'appStarter.title' | translate }}</span> + </app-breadcrumb-item> +</app-breadcrumb> +<h2>{{ 'appStarter.title' | translate }}</h2> +<hr /> +<div class="d-flex flex-wrap cards my-5"> + <ng-container *ngIf="tiles$ | async as tiles"> + <div class="card mb-5 qa_tiles_wrapper" *ngFor="let tile of tiles" [ngbTooltip]="'appStarter.tiles.tooltips.enum.' + tile.id | translate"> + <a class="card-block stretched-link text-decoration-none my-3 qa_tiles_not_disabled" [href]="tile.redirectUrl" target="_blank"> + <img + src="assets/images/tiles/{{ tile.imageUrl }}" + class="img-fluid img-max rounded my-2 qa_tiles_not_disabled_img" + alt="{{ tile.imageAltText }}" + /> + <div class="card-body qa_tiles_not_disabled_body"> + <p class="card-title qa_tiles_not_disabled_title">{{ tile.title }}</p> + </div> + </a> + </div> + </ng-container> +</div> + + diff --git a/src/app/modules/app-starter/app-starter.component.ts b/src/app/modules/app-starter/app-starter.component.ts new file mode 100644 index 0000000..c08467f --- /dev/null +++ b/src/app/modules/app-starter/app-starter.component.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Component, OnInit } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; +import { from, Observable, of } from 'rxjs'; +import { Tile } from 'src/app/model/tile'; + +@Component({ + selector: 'app-app-starter', + templateUrl: './app-starter.component.html', + styleUrls: ['./app-starter.component.css'], +}) +export class AppStarterComponent implements OnInit { + //I will leave this for future purpose in case we will have disabled tiles in the Portal + // disabledTiles:number[] = [11,12,13] + + private readonly hostname = environment.hostname.replace('portal-ui-', ''); + + public readonly tiles$: Observable<Tile[]> = from(fetch('/assets/tiles/tiles.json?t=' + Date.now()).then(rsp => rsp.json())) + .pipe( + map(tiles => (tiles.items as Tile[])), + map(tiles => tiles.map(tile => ({ ...tile, redirectUrl: tile.redirectUrl.replace(/HOSTNAME/i, this.hostname) }))), + ); + + + ngOnInit(): void {} +} diff --git a/src/app/modules/app-starter/app-starter.module.ts b/src/app/modules/app-starter/app-starter.module.ts new file mode 100644 index 0000000..ebbd0ce --- /dev/null +++ b/src/app/modules/app-starter/app-starter.module.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { NgModule } from '@angular/core'; +import { AppStarterComponent } from './app-starter.component'; +import { AppStarterRoutingModule } from './app-starter-routing.module'; +import { SharedModule } from '../../shared.module'; + +@NgModule({ + declarations: [AppStarterComponent], + imports: [AppStarterRoutingModule, SharedModule], +}) +export class AppStarterModule {} diff --git a/src/app/modules/auth/auth.config.module.ts b/src/app/modules/auth/auth.config.module.ts new file mode 100644 index 0000000..7f70ba6 --- /dev/null +++ b/src/app/modules/auth/auth.config.module.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { AuthConfig } from 'angular-oauth2-oidc'; + +import { authConfig } from './auth.config'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { AuthInterceptor } from '../../http-interceptors/auth.interceptor'; +import { AuthConfigService } from '../../services/authconfig.service'; + +export function init_app(authConfigService: AuthConfigService) { + return () => authConfigService.initAuth(); +} + +@NgModule({ + providers: [ + { provide: AuthConfig, useValue: authConfig }, + AuthConfigService, + { + provide: APP_INITIALIZER, + useFactory: init_app, + deps: [AuthConfigService], + multi: true, + }, + { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, + ], + declarations: [], +}) +export class AuthConfigModule {} diff --git a/src/app/modules/auth/auth.config.ts b/src/app/modules/auth/auth.config.ts new file mode 100644 index 0000000..3414edd --- /dev/null +++ b/src/app/modules/auth/auth.config.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { AuthConfig } from 'angular-oauth2-oidc'; +import { environment } from '../../../environments/environment'; + +export const authConfig: AuthConfig = { + // Url of the Identity Provider + issuer: environment.keycloak.issuer, + + // URL of the SPA to redirect the user to after login + redirectUri: environment.keycloak.redirectUri, + + // The SPA's id. + // The SPA is registerd with this id at the auth-serverß + clientId: environment.keycloak.clientId, + + responseType: environment.keycloak.responseType, + // set the scope for the permissions the client should request + // The first three are defined by OIDC. + scope: environment.keycloak.scope, + // Remove the requirement of using Https to simplify the demo + // THIS SHOULD NOT BE USED IN PRODUCTION + // USE A CERTIFICATE FOR YOUR IDP + // IN PRODUCTION + requireHttps: environment.keycloak.requireHttps, + // at_hash is not present in JWT token + showDebugInformation: environment.keycloak.showDebugInformation, + disableAtHashCheck: environment.keycloak.disableAtHashCheck, + skipIssuerCheck: environment.keycloak.skipIssuerCheck, + strictDiscoveryDocumentValidation: environment.keycloak.strictDiscoveryDocumentValidation, +}; diff --git a/src/app/modules/auth/injection-tokens.ts b/src/app/modules/auth/injection-tokens.ts new file mode 100644 index 0000000..140b83c --- /dev/null +++ b/src/app/modules/auth/injection-tokens.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { InjectionToken } from '@angular/core'; + +export interface AclConfig { + [key: string]: string[]; +} + +export const ACL_CONFIG = new InjectionToken<AclConfig>('ACL_CONFIG'); diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.css b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.css new file mode 100644 index 0000000..b7b5110 --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.css @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.html b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.html new file mode 100644 index 0000000..2c35f19 --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.html @@ -0,0 +1,63 @@ +<!-- + ~ Copyright (c) 2022. Deutsche Telekom AG + ~ + ~ 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. + ~ + ~ SPDX-License-Identifier: Apache-2.0 + --> + + +<ng-container *ngIf="action"> + <button + *ngIf="action.type | in: REPEAT_ACTIONS" + class="btn btn-invisible p-0 qa_repeat_action" + (click)="onButtonClick(action)" + [attr.aria-label]="'dashboard.apps.userLastAction.tooltip.repeatAction' | translate" + [ngbTooltip]="repeatActionTooltipContent" + container="body" + > + <i class="bi bi-arrow-clockwise pointer" aria-hidden="true"></i> + </button> + <button + *ngIf="action.type | in: VIEW_ACTIONS" + class="btn btn-invisible p-0 qa_view_action" + (click)="onButtonClick(action)" + [attr.aria-label]="'dashboard.apps.userLastAction.tooltip.viewAction' | translate" + [ngbTooltip]="viewActionTooltipContent" + container="body" + > + <i class="bi bi-eyeglasses" aria-hidden="true"></i> + </button> + + <ng-template #repeatActionTooltipContent> + <span + >{{ 'dashboard.apps.userLastAction.actionType.' + action.type | translate }} + {{ 'dashboard.apps.userLastAction.tooltip.again' | translate }} + {{ 'dashboard.apps.userLastAction.entityType.' + action.entity | translate }} + {{ message }}</span + > + </ng-template> + <ng-template #viewActionTooltipContent> + <span + >{{ 'dashboard.apps.userLastAction.actionType.' + ActionType.VIEW | translate }} + <span *ngIf="action.type === ActionType.DEPLOY"> + {{ 'dashboard.apps.userLastAction.tooltip.statusOf' | translate }} + </span> + {{ 'dashboard.apps.userLastAction.entityType.' + action.entity | translate }} + <span *ngIf="action.type === ActionType.DEPLOY"> + {{ 'dashboard.apps.userLastAction.tooltip.deployment' | translate }} + </span> + {{ message }}</span + > + </ng-template> +</ng-container> diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.ts b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.ts new file mode 100644 index 0000000..b30a35e --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ActionModel, ActionRowModel, ActionType, EntityTypeModel } from '../../../../../model/user-last-action.model'; + +@Component({ + selector: 'app-action-button', + templateUrl: './action-button.component.html', + styleUrls: ['./action-button.component.css'], +}) +export class ActionButtonComponent { + public readonly VIEW_ACTIONS = [ActionType.ACK, ActionType.UNACK, ActionType.CLEAR, ActionType.DEPLOY]; + public readonly REPEAT_ACTIONS = [ActionType.SEARCH, ActionType.VIEW, ActionType.EDIT]; + ActionType = ActionType; + + @Input() action: ActionRowModel<EntityTypeModel> | undefined; + @Input() message: string | undefined; + @Output() btnClick = new EventEmitter<ActionModel>(); + + public onButtonClick(action: ActionModel): void { + this.btnClick.emit(action); + } +} diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.css b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.css new file mode 100644 index 0000000..c6f52a8 --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.css @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +img { + height: 20px; + width: 20px; +} diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.html b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.html new file mode 100644 index 0000000..62cf722 --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.html @@ -0,0 +1,105 @@ +<!-- + ~ Copyright (c) 2022. Deutsche Telekom AG + ~ + ~ 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. + ~ + ~ SPDX-License-Identifier: Apache-2.0 + --> + + +<ng-container *ngIf="action"> + <div class="row py-2 border-bottom"> + <div class="col-3"> + <div class="d-flex justify-content-between"> + <span + class="qa_action_created_at" + container="body" + [ngbTooltip]="action.actionCreatedAt | date: FULL_DATE_FORMAT" + >{{ + (action.actionCreatedAt | isToday) + ? (action.actionCreatedAt | date: TIME_FORMAT) + : (action.actionCreatedAt | date: DATE_FORMAT) + }}</span + > + <ng-container [ngSwitch]="action.type"> + <ng-template [ngSwitchCase]="ActionType.CREATE"> + <img + class="qa_create_icon" + src='../../../../../../assets/images/icons/install_graphical.svg' + [attr.alt]="'dashboard.apps.userLastAction.actionType.CREATE' | translate" + /> + </ng-template> + <ng-template [ngSwitchCase]="ActionType.DELETE"> + <img + class="qa_delete_icon" + src='../../../../../../assets/images/icons/eraser_graphical.svg' + [attr.alt]="'dashboard.apps.userLastAction.actionType.DELETE' | translate" + /> + </ng-template> + <ng-template [ngSwitchCase]="ActionType.SEARCH"> + <img + class="qa_search_icon" + src='../../../../../../assets/images/icons/search_graphical.svg' + [attr.alt]="'dashboard.apps.userLastAction.actionType.SEARCH' | translate" + /> + </ng-template> + <ng-template [ngSwitchCase]="ActionType.VIEW"> + <img + class="qa_view_icon" + src='../../../../../../assets/images/icons/visible_graphical.svg' + [attr.alt]="'dashboard.apps.userLastAction.actionType.VIEW' | translate" + /> + </ng-template> + <ng-template [ngSwitchCase]="ActionType.EDIT"> + <img + class="qa_edit_icon" + src='../../../../../../assets/images/icons/edit_graphical.svg' + [attr.alt]="'dashboard.apps.userLastAction.actionType.EDIT' | translate" + /> + </ng-template> + <ng-template [ngSwitchCase]="ActionType.CLEAR"> + <img + class="qa_clear_icon" + src='../../../../../../assets/images/icons/brush_graphical.svg' + [attr.alt]="'dashboard.apps.userLastAction.actionType.CLEAR' | translate" + /> + </ng-template> + <ng-template [ngSwitchCase]="ActionType.ACK"> + <img + class="qa_ack_icon" + src='../../../../../../assets/images/icons/thumbs-up_graphical.svg' + [attr.alt]="'dashboard.apps.userLastAction.actionType.ACK' | translate" + /> + </ng-template> + <ng-template [ngSwitchCase]="ActionType.UNACK"> + <img + class="qa_unack_icon" + src='../../../../../../assets/images/icons/thumbs-down_graphical.svg' + [attr.alt]="'dashboard.apps.userLastAction.actionType.UNACK' | translate" + /> + </ng-template> + <ng-template [ngSwitchCase]="ActionType.DEPLOY"> + <img + class="qa_deployment_icon" + src='../../../../../../assets/images/icons/crane_graphical.svg' + [attr.alt]="'dashboard.apps.userLastAction.actionType.DEPLOY' | translate" + /> + </ng-template> + </ng-container> + </div> + </div> + <div class="col-9"> + <ng-content></ng-content> + </div> + </div> +</ng-container> diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.ts b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.ts new file mode 100644 index 0000000..89e7950 --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Component, Input } from '@angular/core'; +import { ActionModel, ActionType, EntityType } from '../../../../../model/user-last-action.model'; + +@Component({ + selector: 'app-action-row', + templateUrl: './action-row.component.html', + styleUrls: ['./action-row.component.css'], +}) +export class ActionRowComponent { + readonly FULL_DATE_FORMAT = 'E, d MMM Y HH:mm'; + readonly TIME_FORMAT = 'HH:mm'; + readonly DATE_FORMAT = 'd MMM'; + ActionType = ActionType; + EntityType = EntityType; + @Input() action: ActionModel | undefined; +} diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.css b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.css new file mode 100644 index 0000000..b7b5110 --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.css @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.html b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.html new file mode 100644 index 0000000..54f71c6 --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.html @@ -0,0 +1,33 @@ +<!-- + ~ Copyright (c) 2022. Deutsche Telekom AG + ~ + ~ 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. + ~ + ~ SPDX-License-Identifier: Apache-2.0 + --> + + +<div class="d-flex justify-content-between" *ngIf='action'> + <span class="d-inline-block text-truncate w-100" + >{{ 'dashboard.apps.userLastAction.actionType.' + action.type | translate }} + {{ 'dashboard.apps.userLastAction.entityType.' + action.entity | translate | colon }} + {{ action.entityParams.userName }} + </span> + + <app-action-button + *ngIf="action.type === ActionType.EDIT" + [message]="action.entityParams.userName" + [action]="action" + (btnClick)="repeatAction(action)" + ></app-action-button> +</div> diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.ts b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.ts new file mode 100644 index 0000000..b55ad17 --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.ts @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Component, Input } from '@angular/core'; +import { ActionRowModel, ActionType, EntityUserHistoryActionModel } from '../../../../../model/user-last-action.model'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-entity-user-administration-row', + templateUrl: './entity-user-administration-row.component.html', + styleUrls: ['./entity-user-administration-row.component.css'], +}) +export class EntityUserAdministrationRowComponent { + ActionType = ActionType; + @Input() + action: ActionRowModel<EntityUserHistoryActionModel> | undefined; + + constructor(private router: Router) {} + + public repeatAction(action: ActionRowModel<EntityUserHistoryActionModel>): void { + this.router.navigate(['user-administration', action.entityParams.userId, 'edit']); + } +} diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.css b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.css new file mode 100644 index 0000000..60842fa --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.css @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +img { + height: 20px; + width: 20px; +} + +.bg-color-inherit { + background-color: inherit; +} diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.html b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.html new file mode 100644 index 0000000..d696728 --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.html @@ -0,0 +1,85 @@ +<!-- + ~ Copyright (c) 2022. Deutsche Telekom AG + ~ + ~ 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. + ~ + ~ SPDX-License-Identifier: Apache-2.0 + --> + +<ng-template #template> + <ng-container *ngIf="actions$ | async as actions"> + <div class="col-xl-4 col-lg-6 col-sm-12 my-2 qa_USER_LAST_ACTION_TILE" cdkDrag> + <div class="shadow card" style="height: 334.867px"> + <div class="card-header pl-3"> + <div class="d-flex" *ngIf="actionFilterType$ | async as selectedFilter"> + <div class="d-flex align-items-center"> + <i + class="bi bi-arrows-move text-primary draggable text-primary pr-2" + cdkDragHandle + aria-hidden="true" + ></i> + <label class="d-none" for="filterSelect" + >{{ 'dashboard.apps.userLastAction.filter.label' | translate + }}{{ 'dashboard.apps.userLastAction.filter.type.' + selectedFilter | translate }}</label + > + <select + id="filterSelect" + [ngModel]="selectedFilter" + (ngModelChange)="changeFilterType.next($event)" + class="form-select-sm font-weight-bolder bg-color-inherit" + > + <option *ngFor="let filter of actionsFilter" [ngValue]="filter" class="font-weight-normal"> + {{ 'dashboard.apps.userLastAction.filter.type.' + filter | translate }} + </option> + </select> + </div> + <div class="d-flex" *ngIf="actionIntervalType$ | async as selectedInterval"> + <label class="d-none" for="intervalSelect" + >{{ 'dashboard.apps.userLastAction.filter.label' | translate + }}{{ 'dashboard.apps.userLastAction.filter.interval.' + selectedInterval | translate }}</label + > + <select + id="intervalSelect" + [ngModel]="selectedInterval" + (ngModelChange)="changeIntervalType.next($event)" + class="form-select-sm font-weight-bold bg-color-inherit" + > + <option *ngFor="let interval of intervals" [ngValue]="interval" class="font-weight-normal"> + {{ 'dashboard.apps.userLastAction.filter.interval.' + interval | translate }} + </option> + </select> + </div> + </div> + </div> + <div class="card-body overflow-auto"> + <ng-container *ngIf="actions.length > 0; else noData"> + <div *ngFor="let action of actions"> + <app-action-row [action]="action"> + <app-entity-user-administration-row + *ngIf="action.entity === EntityType.USERADMINISTRATION" + [action]="$any(action)" + ></app-entity-user-administration-row> + </app-action-row> + </div> + </ng-container> + </div> + <div class="card-footer"></div> + </div> + </div> + </ng-container> +</ng-template> +<ng-template #noData> + <div class="d-flex justify-content-center qa_class_no_data"> + {{ 'common.filters.noResults' | translate }} + </div> +</ng-template> diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.ts b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.ts new file mode 100644 index 0000000..c03016f --- /dev/null +++ b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.ts @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Component, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; +import { + ActionFilter, + ActionInterval, + ActionModel, + ActionType, + EntityType, +} from '../../../../model/user-last-action.model'; +import { ActionsListResponse } from '../../../../../../openapi/output'; +import { combineLatest, merge, Subject } from 'rxjs'; +import { map, scan, shareReplay, switchMap } from 'rxjs/operators'; +import { UnsubscribeService } from '../../../../services/unsubscribe/unsubscribe.service'; +import { selectDistinctState, UserSettingsService } from '../../../../services/user-settings.service'; +import { LastUserActionSettings, STATE_KEYS } from '../../../../model/user-preferences.model'; +import { HistoryService } from '../../../../services/history.service'; + +@Component({ + selector: 'app-user-last-action-tile', + templateUrl: './user-last-action-tile.component.html', + styleUrls: ['./user-last-action-tile.component.css'], + providers: [UnsubscribeService], +}) +export class UserLastActionTileComponent implements OnInit { + public readonly actionsFilter: ActionFilter[] = Object.values(ActionFilter); + public readonly intervals: ActionInterval[] = Object.values(ActionInterval); + public readonly ActionType = ActionType; + public readonly EntityType = EntityType; + public changeFilterType: Subject<ActionFilter> = new Subject<ActionFilter>(); + public changeIntervalType: Subject<ActionInterval> = new Subject<ActionInterval>(); + + @ViewChild('template', { static: true }) template!: TemplateRef<unknown>; + + constructor( + private viewContainerRef: ViewContainerRef, + private historyService: HistoryService, + private unsubscribeService: UnsubscribeService, + private userSettingsService: UserSettingsService, + ) {} + + private userActionsSettings$ = this.userSettingsService + .selectLastUserAction() + .pipe(shareReplay({ refCount: true, bufferSize: 1 })); + + public actionFilterType$ = this.userActionsSettings$.pipe( + selectDistinctState<LastUserActionSettings, ActionFilter>(STATE_KEYS.FILTER_TYPE), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + public actionIntervalType$ = this.userActionsSettings$.pipe( + selectDistinctState<LastUserActionSettings, ActionInterval>(STATE_KEYS.INTERVAL), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + public actions$ = combineLatest([this.actionFilterType$, this.actionIntervalType$]).pipe( + switchMap(([filter, interval]) => { + const mappedInterval = UserLastActionTileComponent.mapActionInterval(interval); + return this.historyService.getUserActions(mappedInterval).pipe( + map(actions => UserLastActionTileComponent.mapActionsFromResponse(actions)), + map(actions => this.filterBySelectedFilterType(filter, actions)), + ); + }), + ); + + private commands$ = merge( + this.changeIntervalType.pipe(map(interval => ({ interval: interval }))), + this.changeFilterType.pipe(map(filterType => ({ filterType: filterType }))), + ); + + private settings$ = this.userActionsSettings$.pipe( + switchMap(data => this.commands$.pipe(scan((settings, change) => ({ ...settings, ...change }), data))), + ); + + ngOnInit(): void { + this.viewContainerRef.createEmbeddedView(this.template); + + this.settings$ + .pipe( + switchMap(lastUserAction => + this.userSettingsService.updatePreferences({ + dashboard: { + apps: { + lastUserAction, + }, + }, + }), + ), + ) + .subscribe(); + } + + private static mapActionsFromResponse(actions: ActionsListResponse): ActionModel[] { + return actions.items.map((action: any) => { + return { + actionCreatedAt: action.actionCreatedAt, + type: action.action.type, + entity: action.action.entity, + entityParams: { + ...action.action.entityParams, + }, + }; + }); + } + + private static mapActionInterval(interval: ActionInterval): number | undefined { + switch (interval) { + case ActionInterval.ALL: + return undefined; + case ActionInterval.LAST1D: + return 24; + case ActionInterval.LAST1H: + return 1; + case ActionInterval.LAST4H: + return 4; + } + } + + private filterBySelectedFilterType(filter: ActionFilter, actions: ActionModel[]): ActionModel[] { + if (filter === ActionFilter.ALL) { + return actions; + } else if (filter === ActionFilter.SEARCH) { + return actions.filter(action => action.type === ActionType.SEARCH); + } else { + return actions.filter(action => action.type !== ActionType.SEARCH); + } + } +} diff --git a/src/app/modules/dashboard/dashboard-routing.module.ts b/src/app/modules/dashboard/dashboard-routing.module.ts new file mode 100644 index 0000000..68833ba --- /dev/null +++ b/src/app/modules/dashboard/dashboard-routing.module.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '../../guards/auth.guard'; +import { DashboardComponent } from './dashboard.component'; + +const routes: Routes = [{ path: '', component: DashboardComponent, canActivate: [AuthGuard] }]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class DashboardRoutingModule {} diff --git a/src/app/modules/dashboard/dashboard.component.css b/src/app/modules/dashboard/dashboard.component.css new file mode 100644 index 0000000..bdf57d6 --- /dev/null +++ b/src/app/modules/dashboard/dashboard.component.css @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +p { + margin-bottom: 0 !important; +} +li { + list-style-type: none; +} + +.row > * { + flex-shrink: 0; + width: initial; + max-width: initial; + padding-right: initial; + padding-left: initial; + margin-top: initial; +} diff --git a/src/app/modules/dashboard/dashboard.component.html b/src/app/modules/dashboard/dashboard.component.html new file mode 100644 index 0000000..76a8e96 --- /dev/null +++ b/src/app/modules/dashboard/dashboard.component.html @@ -0,0 +1,77 @@ +<!-- + ~ Copyright (c) 2022. Deutsche Telekom AG + ~ + ~ 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. + ~ + ~ SPDX-License-Identifier: Apache-2.0 + --> + + +<app-breadcrumb> + <app-breadcrumb-item> + <span aria-current="page">{{ 'layout.menu.items.dashboard' | translate }}</span> + </app-breadcrumb-item> +</app-breadcrumb> +<ng-container *ngIf="tiles$ | async as apps"> + <div class="w-100 d-flex justify-content-between"> + <h2 class="qa_title">{{ 'layout.menu.items.dashboard' | translate }}</h2> + <ul> + <li + #settingsDrop="ngbDropdown" + [ngbTooltip]="'dashboard.tooltips.settings' | translate" + class="qa_alarm_auto_settings" + ngbDropdown + > + <button + [attr.aria-label]="'dashboard.showSettings' | translate" + class="btn btn-outline-secondary no-border qa_dashboard_show_and_hide_settings_btn" + id="dropdownColumnSettings" + ngbDropdownToggle + > + <i aria-hidden="true" class="bi bi-gear-fill text-muted"></i> + </button> + <div aria-labelledby="dropdownColumnSettings" ngbDropdownMenu style="min-width: 250px"> + <p class="px-4 small text-muted mb-1">{{ 'dashboard.selectApplications' | translate }}</p> + <form class="px-4 py-3 d-flex flex-column align-items-start"> + <div + [appHasPermissions]="'dashboard.tile.' + app.type" + *ngFor="let app of apps" + class="d-flex justify-content-center" + > + <ng-container *ngIf="'dashboard.tile.' + app.type | hasPermission | async"> + <input + type="checkbox" + [(ngModel)]="app.displayed" + (ngModelChange)="updateAction.next(app)" + [ngModelOptions]="{ standalone: true }" + [ngClass]="'qa_dashboard_show_app_' + app.type" + /> + <p class="ml-2">{{ 'dashboard.apps.' + app.type | translate }}</p> + </ng-container> + </div> + </form> + </div> + </li> + </ul> + </div> + <hr /> + <div class="row" cdkDropList (cdkDropListDropped)="dropAction.next($event)"> + <ng-container *ngFor="let app of apps | map: filterDisplayedTiles"> + <ng-container *ngIf="'dashboard.tile.' + app.type | hasPermission | async"> + <ng-container *ngIf="app.type === DashboardApplications.USER_LAST_ACTION_TILE"> + <app-user-last-action-tile></app-user-last-action-tile> + </ng-container> + </ng-container> + </ng-container> + </div> +</ng-container> diff --git a/src/app/modules/dashboard/dashboard.component.ts b/src/app/modules/dashboard/dashboard.component.ts new file mode 100644 index 0000000..043ab6b --- /dev/null +++ b/src/app/modules/dashboard/dashboard.component.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Component, OnInit } from '@angular/core'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { UserSettingsService } from '../../services/user-settings.service'; +import { DashboardApplications, DashboardTileSettings } from '../../model/dashboard.model'; +import { UnsubscribeService } from '../../services/unsubscribe/unsubscribe.service'; +import { map, shareReplay, switchMap, takeUntil } from 'rxjs/operators'; +import { merge, Observable, Subject } from 'rxjs'; + +@Component({ + selector: 'app-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.css'], + providers: [UnsubscribeService], +}) +export class DashboardComponent implements OnInit { + DashboardApplications = DashboardApplications; + + public dropAction = new Subject<CdkDragDrop<string[]>>(); + public updateAction = new Subject<DashboardTileSettings>(); + + constructor(private unsubscribeService: UnsubscribeService, private userSettingsService: UserSettingsService) {} + + public tiles$: Observable<DashboardTileSettings[]> = this.userSettingsService + .selectDashboardAvailableTiles() + .pipe(shareReplay({ refCount: true, bufferSize: 1 })); + + filterDisplayedTiles(tiles: DashboardTileSettings[]): DashboardTileSettings[] { + return tiles.filter(tile => tile.displayed); + } + + public visibleTiles$: Observable<DashboardTileSettings[]> = this.tiles$.pipe( + switchMap(tiles => + this.updateAction.pipe( + map(updatedTile => { + const index = tiles.findIndex(tile => tile.type === updatedTile.type); + tiles[index].displayed = updatedTile.displayed; + return [...tiles]; + }), + ), + ), + ); + + public movedTiles$: Observable<DashboardTileSettings[]> = this.tiles$.pipe( + switchMap(tiles => + this.dropAction.pipe( + map(event => { + moveItemInArray(tiles, event.previousIndex, event.currentIndex); + return tiles; + }), + ), + ), + ); + ngOnInit() { + merge(this.visibleTiles$, this.movedTiles$) + .pipe( + takeUntil(this.unsubscribeService.unsubscribe$), + switchMap(availableTiles => + this.userSettingsService.updatePreferences({ + dashboard: { + apps: { + availableTiles, + }, + }, + }), + ), + ) + .subscribe(); + } +} diff --git a/src/app/modules/dashboard/dashboard.module.ts b/src/app/modules/dashboard/dashboard.module.ts new file mode 100644 index 0000000..8d63307 --- /dev/null +++ b/src/app/modules/dashboard/dashboard.module.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { DashboardRoutingModule } from './dashboard-routing.module'; +import { SharedModule } from '../../shared.module'; +import { + EntityUserAdministrationRowComponent, +} from './apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component'; +import { ActionButtonComponent } from './apps/user-last-action-tile/action-button/action-button.component'; +import { ActionRowComponent } from './apps/user-last-action-tile/action-row/action-row.component'; +import { UserLastActionTileComponent } from './apps/user-last-action-tile/user-last-action-tile.component'; +import { DashboardComponent } from './dashboard.component'; +import { DragDropModule } from '@angular/cdk/drag-drop'; + +@NgModule({ + declarations: [ + DashboardComponent, + UserLastActionTileComponent, + ActionRowComponent, + ActionButtonComponent, + EntityUserAdministrationRowComponent, + ], + imports: [DashboardRoutingModule, SharedModule, DragDropModule], +}) +export class DashboardModule {} diff --git a/src/app/modules/i18n/i18n.module.ts b/src/app/modules/i18n/i18n.module.ts new file mode 100644 index 0000000..52bedbe --- /dev/null +++ b/src/app/modules/i18n/i18n.module.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; + +@NgModule({ + declarations: [], + imports: [ + CommonModule, + HttpClientModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: translateLoaderFactory, + deps: [HttpClient], + }, + }), + ], + exports: [TranslateModule], +}) +export class I18nModule { + constructor(translate: TranslateService) { + translate.addLangs(['en', 'de']); + const browserLang = translate.getBrowserLang(); + translate.use(browserLang.match(/en|de/) ? browserLang : 'en'); + } +} + +export function translateLoaderFactory(httpClient: HttpClient) { + return new TranslateHttpLoader(httpClient, 'assets/i18n/', `.json?t=${new Date().getTime()}`); +} diff --git a/src/app/modules/user-administration/user-administration-form/user-administration-form.component.css b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.css new file mode 100644 index 0000000..f056ef5 --- /dev/null +++ b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.css @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +.custom-invalid-feedback { + width: 100%; + margin-top: 0.25rem; + font-size: 84%; + padding: 0.3rem 0.375rem; + color: var(--dark); + background-color: rgba(217, 0, 0, 0.1); + border-radius: 0.25rem; +} + +.custom-control-input:checked ~ .custom-control-label::before { + background-color: var(--primary); + border-color: #e20088; +} + +.custom-control-input:focus ~ .custom-control-label::before { + box-shadow: 0 0 0 0.2rem rgba(226, 0, 136, 0.25); + border-color: rgba(226, 0, 136, 0.25); +} diff --git a/src/app/modules/user-administration/user-administration-form/user-administration-form.component.html b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.html new file mode 100644 index 0000000..66ede05 --- /dev/null +++ b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.html @@ -0,0 +1,205 @@ +<!-- + ~ Copyright (c) 2022. Deutsche Telekom AG + ~ + ~ 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. + ~ + ~ SPDX-License-Identifier: Apache-2.0 + --> + + +<app-breadcrumb> + <app-breadcrumb-item> + <a [routerLink]="['/dashboard']">{{ 'layout.menu.items.home' | translate }}</a> + </app-breadcrumb-item> + <app-breadcrumb-item> + <a [routerLink]="['/user-administration', 'list']">{{ 'userAdministration.list.title' | translate }}</a> + </app-breadcrumb-item> + <ng-container *ngIf="userId === null"> + <app-breadcrumb-item> + <span aria-current="page">{{ 'userAdministration.form.title.create' | translate }}</span> + </app-breadcrumb-item> + </ng-container> + <ng-container *ngIf="user"> + <app-breadcrumb-item> + <a [routerLink]="['/user-administration', user.id, 'detail']">{{ user.username }}</a> + </app-breadcrumb-item> + <app-breadcrumb-item> + <span aria-current="page">{{ 'userAdministration.form.title.edit' | translate }}</span> + </app-breadcrumb-item> + </ng-container> +</app-breadcrumb> + +<h2 class="py-2 qa_title"> + {{ (userId === null ? 'userAdministration.form.title.create' : 'userAdministration.form.title.edit') | translate }} +</h2> +<hr /> + +<div class="row"> + <!-- Set User Data--> + <div class="col-12 col-lg-6"> + <h4 class="text-monospace border-bottom text-secondary pb-2"> + {{ 'userAdministration.form.headings.setUserData' | translate }} + </h4> + <form class="mb-5" [formGroup]="keycloakUserForm" novalidate> + <div class="form-group row"> + <label class="col-xl-3 col-form-label" for="id">{{ 'userAdministration.fields.id' | translate }}</label> + <div class="col-xl-9"> + <input formControlName="id" class="form-control" id="id" readonly /> + </div> + </div> + <div class="form-group row"> + <label class="col-xl-3 col-form-label" for="username">{{ + 'userAdministration.fields.userName' | translate + }}</label> + <div class="col-xl-9"> + <input + formControlName="username" + class="form-control" + id="username" + [attr.readonly]="this.userId" + [class.is-invalid]="isFormControlInvalid(userName)" + required + aria-required="true" + /> + <div *ngIf="userName && userName?.errors?.required" class="invalid-feedback qa_required_user_name"> + {{ 'common.required' | translate }} + </div> + <div *ngIf="userName && userName?.errors?.pattern" class="invalid-feedback qa_invalid_user_name"> + {{ 'common.form.feedback.invalidCharacters' | translate }} + </div> + </div> + </div> + <div class="form-group row"> + <label class="col-xl-3 col-form-label" for="email">{{ 'userAdministration.fields.email' | translate }}</label> + <div class="col-xl-9"> + <input + formControlName="email" + type="email" + class="form-control" + id="email" + [class.is-invalid]="isFormControlInvalid(email)" + /> + <div *ngIf="email && email?.errors?.email" class="invalid-feedback qa_wrong_format_email"> + {{ 'common.form.feedback.emailWrongFormat' | translate }} + </div> + <div *ngIf="email && email?.errors?.pattern" class="invalid-feedback qa_invalid_email"> + {{ 'common.form.feedback.invalidCharacters' | translate }} + </div> + <div *ngIf="email && email?.errors?.required" class="invalid-feedback qa_required_email"> + {{ 'common.form.feedback.required' | translate }} + </div> + </div> + </div> + <div class="form-group row"> + <label class="col-xl-3 col-form-label" for="firstName">{{ + 'userAdministration.fields.firstName' | translate + }}</label> + <div class="col-xl-9"> + <input + formControlName="firstName" + class="form-control" + id="firstName" + [class.is-invalid]="isFormControlInvalid(firstName)" + /> + <div *ngIf="firstName && firstName?.errors?.pattern" class="invalid-feedback qa_invalid_first_name"> + {{ 'common.form.feedback.invalidCharacters' | translate }} + </div> + </div> + </div> + <div class="form-group row"> + <label class="col-xl-3 col-form-label" for="lastName">{{ + 'userAdministration.fields.lastName' | translate + }}</label> + <div class="col-xl-9"> + <input + formControlName="lastName" + class="form-control" + id="lastName" + [class.is-invalid]="isFormControlInvalid(lastName)" + /> + <div *ngIf="lastName && lastName?.errors?.pattern" class="invalid-feedback qa_invalid_last_name"> + {{ 'common.form.feedback.invalidCharacters' | translate }} + </div> + </div> + </div> + </form> + <!-- SET ROLES--> + <div class="mb-5" style="min-height: 150px"> + <h4 class="text-monospace border-bottom text-secondary pb-2"> + {{ 'userAdministration.form.headings.setRoles.title' | translate }} + </h4> + + <div class="form-row"> + <div class="col-xl-3 col-form-label">{{ 'userAdministration.form.headings.setRoles.title' | translate }}</div> + <div class="col-xl-9"> + <div class="row" style="padding: 0 15px"> + <div class="col-xl-5 p-3 border border-radius mb-md-2" style="min-height: 125px"> + <h5 class="qa_available_roles"> + {{ 'userAdministration.form.headings.setRoles.available' | translate }} + </h5> + <ng-container *ngFor="let checkbox of checkBoxes.available"> + <ng-container *ngIf="checkbox.name.startsWith('onap_')"> + <div class="form-check"> + <input + type="checkbox" + class="form-check-input qa_checkbox_available" + [attr.aria-labelledby]="checkbox.name" + [value]="checkbox.id" + (change)="onCheckboxChange(checkbox.id, true)" + /> + <label class="form-check-label" [attr.id]="checkbox.name">{{ checkbox.name }}</label> + </div> + </ng-container> + </ng-container> + </div> + <div class="col-xl-2"></div> + <div class="col-xl-5 p-3 border border-radius" style="min-height: 125px"> + <h5 class="qa_assigned_roles">{{ 'userAdministration.form.headings.setRoles.assigned' | translate }}</h5> + <ng-container *ngFor="let checkbox of checkBoxes.assigned"> + <ng-container *ngIf="checkbox.name.startsWith('onap_')"> + <div class="form-check"> + <input + type="checkbox" + class="form-check-input qa_checkbox_assigned" + [attr.aria-labelledby]="checkbox.name" + [value]="checkbox.id" + (change)="onCheckboxChange(checkbox.id, false)" + [checked]="true" + /> + <label class="form-check-label" [attr.id]="checkbox.name">{{ checkbox.name }}</label> + </div> + </ng-container> + </ng-container> + </div> + </div> + </div> + </div> + </div> + + <div class="float-right"> + <ng-container *ngIf="userId === null"> + <button class="btn btn-secondary qa_submit_cancel" [routerLink]="['../', 'list']"> + {{ 'common.buttons.cancel' | translate }} + </button> + </ng-container> + <ng-container *ngIf="userId !== null"> + <button class="btn btn-secondary qa_submit_cancel" [routerLink]="['../../list']"> + {{ 'common.buttons.cancel' | translate }} + </button> + </ng-container> + <button type="submit" class="btn btn-primary qa_submit_button ml-2" (click)="onSubmit()"> + {{ 'common.buttons.save' | translate }} + </button> + </div> + </div> +</div> diff --git a/src/app/modules/user-administration/user-administration-form/user-administration-form.component.spec.ts b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.spec.ts new file mode 100644 index 0000000..def957f --- /dev/null +++ b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.spec.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserAdministrationFormComponent } from './user-administration-form.component'; + +describe('UserAdministrationFormComponent', () => { + let component: UserAdministrationFormComponent; + let fixture: ComponentFixture<UserAdministrationFormComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [UserAdministrationFormComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserAdministrationFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modules/user-administration/user-administration-form/user-administration-form.component.ts b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.ts new file mode 100644 index 0000000..7df2700 --- /dev/null +++ b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.ts @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Component, OnInit } from '@angular/core'; +import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms'; +import { + CreateUserRequest, + Role, + RoleListResponse, + RolesService, + UpdateUserRequest, + UserResponse, + UsersService, +} from 'openapi/output'; +import { AlertService } from 'src/app/modules/alerting'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { UnsubscribeService } from 'src/app/services/unsubscribe/unsubscribe.service'; +import { NON_WHITE_SPACE_PATTERN, VALIDATION_PATTERN } from 'src/app/model/validation-pattern.model'; +import { map, switchMap, take, takeUntil } from 'rxjs/operators'; +import { markAsDirtyAndValidate } from 'src/app/helpers/helpers'; +import { forkJoin, Observable, zip } from 'rxjs'; +import { ActionType, EntityType } from '../../../model/user-last-action.model'; +import { HistoryService } from '../../../services/history.service'; + +@Component({ + selector: 'app-user-administration-form', + templateUrl: './user-administration-form.component.html', + styleUrls: ['./user-administration-form.component.css'], + providers: [UnsubscribeService], +}) +export class UserAdministrationFormComponent implements OnInit { + public readonly userId: string | null; + public readonly keycloakUserForm: FormGroup; + public user: UserResponse | undefined = undefined; + + public checkBoxes: { + assigned: Role[]; + available: Role[]; + } = { + assigned: [], + available: [], + }; + + constructor( + private readonly alertService: AlertService, + private readonly route: ActivatedRoute, + private readonly userAdministrationService: UsersService, + private readonly rolesService: RolesService, + private readonly router: Router, + private readonly translateService: TranslateService, + private readonly unsubscribeService: UnsubscribeService, + private readonly historyService: HistoryService, + ) { + this.userId = this.route.snapshot.paramMap.get('userId'); + + this.keycloakUserForm = new FormGroup({ + id: new FormControl({ value: null, disabled: true }), + username: new FormControl({ value: null, disabled: this.userId !== null }, [ + Validators.required, + Validators.maxLength(50), + Validators.pattern(VALIDATION_PATTERN), + Validators.pattern(NON_WHITE_SPACE_PATTERN), + ]), + email: new FormControl(null, [Validators.email, Validators.required, Validators.pattern(VALIDATION_PATTERN)]), + firstName: new FormControl(null, [Validators.pattern(VALIDATION_PATTERN)]), + lastName: new FormControl(null, [Validators.pattern(VALIDATION_PATTERN)]), + }); + } + + ngOnInit(): void { + if (this.userId !== null) { + this.userAdministrationService + .getUser(this.userId) + .pipe(takeUntil(this.unsubscribeService.unsubscribe$)) + .subscribe(user => { + this.user = user; + this.keycloakUserForm.patchValue({ + id: user.id, + username: user.username, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + }); + }); + + zip( + this.userAdministrationService.listAvailableRoles(this.userId).pipe(map(available => available.items)), + this.userAdministrationService.listAssignedRoles(this.userId).pipe(map(assigned => assigned.items)), + ) + .pipe(takeUntil(this.unsubscribeService.unsubscribe$)) + .subscribe(([available, assigned]) => { + this.checkBoxes = { available, assigned }; + }); + } else { + this.rolesService + .listRoles() + .pipe( + takeUntil(this.unsubscribeService.unsubscribe$), + map(available => available.items), + ) + .subscribe(available => { + this.checkBoxes.available = available; + }); + } + } + + get userName(): FormControl { + return this.keycloakUserForm.get('username') as FormControl; + } + + get email(): FormControl { + return this.keycloakUserForm.get('email') as FormControl; + } + + get firstName(): FormControl { + return this.keycloakUserForm.get('firstName') as FormControl; + } + + get lastName(): FormControl { + return this.keycloakUserForm.get('lastName') as FormControl; + } + + public onSubmit(): void { + markAsDirtyAndValidate(this.keycloakUserForm); + if (this.keycloakUserForm.valid) { + const formValue = this.keycloakUserForm.getRawValue(); + if (this.userId === null) { + this.userAdministrationService + .createUser(this.createUserRequest(formValue)) + .pipe( + switchMap((data: UserResponse) => + this.historyService.createUserHistoryAction({ + type: ActionType.CREATE, + entity: EntityType.USERADMINISTRATION, + entityParams: { userName: data.username, userId: data.id }, + }), + ), + take(1), + ) + .subscribe(() => { + this.alertService.success(this.translateService.instant('userAdministration.messages.success.created'), { + keepAfterRouteChange: true, + autoClose: true, + }); + this.router.navigate(['../list'], { relativeTo: this.route }); + }); + } else { + this.updateUserData( + this.userAdministrationService.updateUser(this.userId, this.updateUserRequest(formValue)), + this.userAdministrationService.updateAssignedRoles(this.userId, undefined, this.checkBoxes.assigned), + ); + } + } + } + + public isFormControlInvalid(formControl: AbstractControl | null): boolean { + if (formControl !== null) { + return formControl && formControl?.invalid && (formControl?.dirty || formControl?.touched); + } + return false; + } + + public onCheckboxChange(roleId: string, checked: boolean): void { + if (checked) { + const checkedObj = { ...this.checkBoxes.available.find(({ id }) => id === roleId) } as Role; + this.checkBoxes.assigned.push(checkedObj); + this.checkBoxes.available = this.checkBoxes.available.filter(({ id }) => id !== roleId); + } else { + const uncheckedObj = { ...this.checkBoxes.assigned.find(({ id }) => id === roleId) } as Role; + this.checkBoxes.available.push(uncheckedObj); + this.checkBoxes.assigned = this.checkBoxes.assigned.filter(({ id }) => id !== roleId); + } + } + + private createUserRequest(formValue: any): CreateUserRequest { + return { + username: formValue.username, + email: formValue.email, + firstName: formValue.firstName, + lastName: formValue.lastName, + enabled: true, + roles: this.checkBoxes.assigned, + }; + } + + private updateUserRequest(formValue: any): UpdateUserRequest { + return { + email: formValue.email, + firstName: formValue.firstName, + lastName: formValue.lastName, + enabled: true, + }; + } + + private updateUserData(userResponse: Observable<UserResponse>, roleResponse: Observable<RoleListResponse>): void { + forkJoin([userResponse, roleResponse]) + .pipe( + switchMap(([,]) => + this.historyService.createUserHistoryAction({ + type: ActionType.EDIT, + entity: EntityType.USERADMINISTRATION, + entityParams: { userName: this.user!.username, userId: this.user!.id }, + }), + ), + take(1), + ) + .subscribe(() => { + this.alertService.success(this.translateService.instant('userAdministration.messages.success.updated'), { + keepAfterRouteChange: true, + autoClose: true, + }); + this.router.navigate(['../../list'], { relativeTo: this.route }); + }); + } +} diff --git a/src/app/modules/user-administration/user-administration-list/user-administration-list.component.css b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.css new file mode 100644 index 0000000..b8d5a0e --- /dev/null +++ b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.css @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +.btn-outline-secondary { + color: var(--dark-gray) !important; + border-color: var(--dark-gray) !important; +} +.btn-outline-secondary:hover { + color: var(--light-gray) !important; + background-color: var(--dark-gray) !important; + border-color: var(--dark-gray) !important; +} diff --git a/src/app/modules/user-administration/user-administration-list/user-administration-list.component.html b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.html new file mode 100644 index 0000000..d205ee2 --- /dev/null +++ b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.html @@ -0,0 +1,159 @@ +<!-- + ~ Copyright (c) 2022. Deutsche Telekom AG + ~ + ~ 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. + ~ + ~ SPDX-License-Identifier: Apache-2.0 + --> + + +<ng-container + *ngIf=" + { + users: result$ | async, + page: page$ | async, + pageSize: pageSize$ | async + } as vm; + else loading + " +> + <app-breadcrumb> + <app-breadcrumb-item> + <a [routerLink]="['/dashboard']">{{ 'layout.menu.items.home' | translate }}</a> + </app-breadcrumb-item> + <app-breadcrumb-item> + <span aria-current="page">{{ 'userAdministration.list.title' | translate }}</span> + </app-breadcrumb-item> + </app-breadcrumb> + <h2>{{ 'userAdministration.list.title' | translate }}</h2> + <hr /> + <div class="d-flex justify-content-between"> + <button + class="btn btn-primary qa_create_button ml-auto" + [appHasPermissions]="'users.administration.create'" + type="button" + [routerLink]="['../', 'create']" + > + {{ 'userAdministration.buttons.createUser' | translate }} + </button> + </div> + + <div class="row"> + <div class="col"> + <div class="table-responsive"> + <table class="table table-sm table-striped"> + <caption> + {{ + 'userAdministration.list.tableCaption' | translate + }} + </caption> + <thead> + <tr> + <th class="qa_user_name_header" scope="col">{{ 'userAdministration.fields.userName' | translate }}</th> + <th class="qa_first_name_header" scope="col">{{ 'userAdministration.fields.firstName' | translate }}</th> + <th class="qa_last_name_header" scope="col">{{ 'userAdministration.fields.lastName' | translate }}</th> + <th class="qa_email_header" scope="col">{{ 'userAdministration.fields.email' | translate }}</th> + <th class="qa_assigned_roles_header" scope="col"> + {{ 'userAdministration.fields.assignedRoles' | translate }} + </th> + <th class="qa_actions_header" scope="col" style="width: 11%"> + {{ 'userAdministration.fields.actions' | translate }} + </th> + </tr> + </thead> + <tbody> + <ng-container *ngIf="vm.users as users"> + <tr *ngFor="let user of users.items"> + <td>{{ user.username }}</td> + <td>{{ user.firstName }}</td> + <td>{{ user.lastName }}</td> + <td> + <a [href]="'mailto:' + user.email">{{ user.email }}</a> + </td> + <td> + <ng-container *ngFor="let role of user.realmRoles; let last = last"> + <span>{{ role }}<span *ngIf="!last">, </span> </span> + </ng-container> + </td> + <td> + <div class="d-flex" *ngIf="loggedUserId$ | async as userId"> + <ng-container *ngIf="userId === user.id; else elseBlock"> + <span + class="d-inline-block" + tabindex="0" + placement="top" + container="body" + [ngbTooltip]="'common.buttons.notPossibleDelete' | translate" + > + <button + class="btn btn-sm btn-outline-danger qa_delete_button mr-2" + type="button" + [appHasPermissions]="'users.administration.delete'" + [attr.aria-label]="'common.buttons.delete' | translate" + disabled + > + <i class="bi bi-trash" aria-hidden="true"></i> + </button> + </span> + </ng-container> + <ng-template #elseBlock> + <button + class="btn btn-sm btn-outline-danger qa_delete_button mr-2" + type="button" + placement="top" + container="body" + [appHasPermissions]="'users.administration.delete'" + [ngbTooltip]="'common.buttons.delete' | translate" + [attr.aria-label]="'common.buttons.delete' | translate" + (click)="openModal(user.id, user.username)" + > + <i class="bi bi-trash" aria-hidden="true"></i> + </button> + </ng-template> + + <button + class="btn btn-sm btn-outline-secondary qa_edit_button" + type="button" + placement="top" + container="body" + [appHasPermissions]="'users.administration.edit'" + [ngbTooltip]="'common.buttons.edit' | translate" + [routerLink]="['../', user.id, 'edit']" + [attr.aria-label]="'common.buttons.edit' | translate" + > + <i class="bi bi-pencil" aria-hidden="true"></i> + </button> + </div> + </td> + </tr> + </ng-container> + </tbody> + </table> + </div> + + <app-pagination + *ngIf="vm.users && vm.users.totalCount > 10" + [collectionSize]="vm.users.totalCount || 0" + [page]="vm.page || 1" + [pageSize]="vm.pageSize || 10" + (pageChange)="changePage($event)" + (pageSizeChange)="changePageSize($event)" + > + </app-pagination> + </div> + </div> +</ng-container> + +<ng-template #loading> + <app-table-skeleton></app-table-skeleton> +</ng-template> diff --git a/src/app/modules/user-administration/user-administration-list/user-administration-list.component.spec.ts b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.spec.ts new file mode 100644 index 0000000..db24b11 --- /dev/null +++ b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.spec.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserAdministrationListComponent } from './user-administration-list.component'; + +describe('UserAdministrationComponent', () => { + let component: UserAdministrationListComponent; + let fixture: ComponentFixture<UserAdministrationListComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [UserAdministrationListComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UserAdministrationListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modules/user-administration/user-administration-list/user-administration-list.component.ts b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.ts new file mode 100644 index 0000000..30637a1 --- /dev/null +++ b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.ts @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { Component } from '@angular/core'; +import { map, repeatWhen, shareReplay, switchMap, takeUntil, tap } from 'rxjs/operators'; +import { AlertService } from 'src/app/modules/alerting'; +import { TranslateService } from '@ngx-translate/core'; +import { UnsubscribeService } from 'src/app/services/unsubscribe/unsubscribe.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ConfirmationModalComponent } from 'src/app/components/shared/confirmation-modal/confirmation-modal.component'; +import { BehaviorSubject, combineLatest, EMPTY, Subject } from 'rxjs'; +import { UsersService } from 'openapi/output'; +import { ActionType, EntityType } from '../../../model/user-last-action.model'; +import { HistoryService } from '../../../services/history.service'; +import { AuthService } from '../../../services/auth.service'; + +@Component({ + selector: 'app-user-administration-list', + templateUrl: './user-administration-list.component.html', + styleUrls: ['./user-administration-list.component.css'], + providers: [UnsubscribeService], +}) +export class UserAdministrationListComponent { + readonly page$ = new BehaviorSubject<number>(1); + readonly pageSize$ = new BehaviorSubject<number>(10); + readonly loggedUserId$ = this.authService.loadCachedUserProfile().pipe( + takeUntil(this.unsubscribeService.unsubscribe$), + map(userInfo => userInfo!.sub)); + + private readonly reload$ = new Subject<void>(); + readonly result$ = combineLatest([this.page$, this.pageSize$]).pipe( + switchMap(([page, pageSize]) => { + return this.userAdministrationService.listUsers(page, pageSize).pipe( + map(response => { + return { + ...response, + items: response.items.map(user => ({ + ...user, + realmRoles: user.realmRoles?.filter(role => role.startsWith('onap_')), + })), + }; + }), + repeatWhen(() => this.reload$), + ); + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + constructor( + private readonly userAdministrationService: UsersService, + private readonly alertService: AlertService, + private readonly translateService: TranslateService, + private readonly modalService: NgbModal, + private readonly unsubscribeService: UnsubscribeService, + private readonly authService: AuthService, + private readonly historyService: HistoryService, + ) { + } + + changePage(page: number): void { + this.page$.next(page); + } + + changePageSize(pageSize: number): void { + this.pageSize$.next(pageSize); + } + + openModal(userId: string, userName: string): void { + // open confirmation modal for user deletion + const modalRef = this.modalService.open(ConfirmationModalComponent,{backdropClass:'backdropClass'}); + modalRef.componentInstance.okText = this.translateService.instant('common.buttons.delete'); + modalRef.componentInstance.title = this.translateService.instant('userAdministration.list.modal.delete.title'); + modalRef.componentInstance.text = this.translateService.instant('userAdministration.list.modal.delete.text', { + userName, + }); + modalRef.closed + .pipe( + takeUntil(this.unsubscribeService.unsubscribe$), + switchMap((confirm: boolean) => { + if (confirm) { + return this.userAdministrationService.deleteUser(userId).pipe( + switchMap(() => + this.historyService.createUserHistoryAction({ + type: ActionType.DELETE, + entity: EntityType.USERADMINISTRATION, + entityParams: { userName, userId }, + }), + ), + ); + } + return EMPTY; + }), + tap(() => { + this.alertService.success(this.translateService.instant('userAdministration.messages.success.deleted'), { + keepAfterRouteChange: true, + autoClose: true, + }); + }), + ) + .subscribe(() => this.reload$.next()); + } +} diff --git a/src/app/modules/user-administration/user-administration-routing.module.ts b/src/app/modules/user-administration/user-administration-routing.module.ts new file mode 100644 index 0000000..7d1a8db --- /dev/null +++ b/src/app/modules/user-administration/user-administration-routing.module.ts @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { UserAdministrationListComponent } from './user-administration-list/user-administration-list.component'; +import { AuthGuard } from '../../guards/auth.guard'; +import { HasPermissionsGuard } from '../../guards/has-permissions.guard'; +import { UserAdministrationFormComponent } from './user-administration-form/user-administration-form.component'; +import { EditUserCanActivateGuard } from '../../guards/edit-user.can-activate.guard'; + +const routes: Routes = [ + { path: '', redirectTo: 'list', pathMatch: 'full' }, + { + path: 'list', + component: UserAdministrationListComponent, + canActivate: [AuthGuard, HasPermissionsGuard], + data: { permission: 'users.administration.list' }, + }, + { + path: 'create', + component: UserAdministrationFormComponent, + canActivate: [AuthGuard, HasPermissionsGuard], + data: { permission: 'users.administration.create' }, + }, + { + path: ':userId/edit', + component: UserAdministrationFormComponent, + canActivate: [AuthGuard, HasPermissionsGuard, EditUserCanActivateGuard], + data: { permission: 'users.administration.edit' }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class UserAdministrationRoutingModule {} diff --git a/src/app/modules/user-administration/user-administration.module.ts b/src/app/modules/user-administration/user-administration.module.ts new file mode 100644 index 0000000..799d405 --- /dev/null +++ b/src/app/modules/user-administration/user-administration.module.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022. Deutsche Telekom AG + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { NgModule } from '@angular/core'; +import { UserAdministrationListComponent } from './user-administration-list/user-administration-list.component'; +import { UserAdministrationFormComponent } from './user-administration-form/user-administration-form.component'; +import { UserAdministrationRoutingModule } from './user-administration-routing.module'; +import { SharedModule } from '../../shared.module'; + +@NgModule({ + declarations: [UserAdministrationListComponent, UserAdministrationFormComponent], + imports: [UserAdministrationRoutingModule, SharedModule], +}) +export class UserAdministrationModule {} |