diff options
author | Fiete Ostkamp <Fiete.Ostkamp@telekom.de> | 2023-04-14 11:59:32 +0000 |
---|---|---|
committer | Fiete Ostkamp <Fiete.Ostkamp@telekom.de> | 2023-04-14 11:59:32 +0000 |
commit | d68841d9f75636575cd778838a8ceea5fd5aada3 (patch) | |
tree | 778c84203ed9bfa4dc1c8234e4e2cf60da6ebd8c /src/app | |
parent | 42af09588f1f839b9ab36356f02f34c89559bcfa (diff) |
Upload ui
Issue-ID: PORTAL-1084
Signed-off-by: Fiete Ostkamp <Fiete.Ostkamp@telekom.de>
Change-Id: Id0c94859a775094e67b0bb9c91ca5e776a08c068
Diffstat (limited to 'src/app')
136 files changed, 8292 insertions, 0 deletions
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts new file mode 100644 index 0000000..0f5a50e --- /dev/null +++ b/src/app/app-routing.module.ts @@ -0,0 +1,55 @@ +/* + * 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 { PageNotFoundComponent } from './components/page-not-found/page-not-found'; +import { StatusPageComponent } from './components/shared/status-page/status-page.component'; + +const routes: Routes = [ + { + path: 'dashboard', + loadChildren: () => import('./modules/dashboard/dashboard.module').then(m => m.DashboardModule), + }, + { + path: 'app-starter', + loadChildren: () => import('./modules/app-starter/app-starter.module').then(m => m.AppStarterModule), + }, + { path: 'statusPage', component: StatusPageComponent }, + { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, + { + path: 'user-administration', + loadChildren: () => + import('./modules/user-administration/user-administration.module').then(m => m.UserAdministrationModule), + }, + { + path: 'not-found', + component: PageNotFoundComponent, + }, + { + path: '**', + redirectTo: 'not-found', + }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy', onSameUrlNavigation: 'reload' })], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/src/app/app.component.css b/src/app/app.component.css new file mode 100644 index 0000000..7c4cb60 --- /dev/null +++ b/src/app/app.component.css @@ -0,0 +1,82 @@ +/* + * 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 + */ + + +*:focus { + outline: 3px dashed; +} + +.wrapper { + display: flex; + height: calc(100vh - 55px); + width: 100%; + align-items: stretch; +} + +a[data-toggle='collapse'] { + position: relative; +} + +.dropdown-toggle::after { + display: block; + position: absolute; + top: 50%; + right: 20px; + transform: translateY(-50%); +} + +.bi-list:focus, +.bi-list:hover { + outline: none; +} + +.bi-list:hover { + background-color: #cb026e; + border-radius: 2px; +} + +main.container-fluid { + overflow-x: hidden; +} + +.loading-spinner { + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 1001; + /*background-color: rgba(0, 0, 0, 0.5);*/ +} + +.large-spinner { + width: 6rem; + height: 6rem; +} + +.loading-text { + width: 90px; + position: absolute; + top: calc(50% - 15px); + left: calc(50% - 45px); + text-align: center; +} + +.spinner-border { + animation: spinner-border 1s linear infinite !important; +} diff --git a/src/app/app.component.html b/src/app/app.component.html new file mode 100644 index 0000000..327725c --- /dev/null +++ b/src/app/app.component.html @@ -0,0 +1,52 @@ +<!-- + ~ 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-header + (collapse)="collapseChanged()" + class="sticky-top" +></app-header> +<div class="wrapper"> + <aside class="h-100 pt-2" id="nav-sidebar" [attr.aria-label]="'layout.sidebar' | translate"> + <app-sidemenu [isSidebarCollapsed]="isCollapsed"></app-sidemenu> + </aside> + <main + [attr.accesskey]='ACCESS_KEY.SHORTCUT_2' + id="main" + tabindex="-1" + class="container-fluid px-5 position-relative" + [attr.aria-label]="'layout.main.mainContent' | translate" + > + <ng-container *ngIf="loading$ | async"> + <div + [style.left.px]="isCollapsed ? 60 : 250" + class="row justify-content-center align-items-center loading-spinner" + > + <div class="loading-text text-primary">{{ 'common.loading' | translate }}</div> + <div class="spinner-border text-primary large-spinner" role="status"></div> + </div> + </ng-container> + <div + class="position-absolute right-10 mt-3" + style="z-index: 999" + [attr.aria-label]="'layout.main.alerts' | translate" + > + <app-alert></app-alert> + </div> + <router-outlet class="p-2"></router-outlet> + </main> +</div> diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts new file mode 100644 index 0000000..8e7c92c --- /dev/null +++ b/src/app/app.component.spec.ts @@ -0,0 +1,64 @@ +/* + * 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 { TestBed, waitForAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AppComponent } from './app.component'; +import { AppStarterComponent } from './modules/app-starter/app-starter.component'; +import { OAuthLogger, OAuthService, UrlHelperService } from 'angular-oauth2-oidc'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { AlertModule } from './modules/alerting'; +import { SidemenuComponent } from './components/layout/sidemenu/sidemenu.component'; +import { HeaderComponent } from './components/layout/header/header/header.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { HasPermissionsDirective } from './directives/has-permissions.directive'; +import { LoadingIndicatorService } from './services/loading-indicator.service'; + +describe('AppComponent', () => { + let component: AppComponent; + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent, + AppStarterComponent, + SidemenuComponent, + HeaderComponent, + HasPermissionsDirective, + ], + imports: [RouterTestingModule, HttpClientTestingModule, AlertModule, TranslateModule], + providers: [ + AppComponent, + UrlHelperService, + OAuthService, + OAuthLogger, + LoadingIndicatorService + ], + }).compileComponents(); + component = TestBed.inject(AppComponent); + }), + ); + it('should create the app', () => { + expect(component).toBeTruthy(); + }); + + it(`should have as title 'frontend'`, () => { + expect(component.title).toEqual('frontend'); + }); +}); diff --git a/src/app/app.component.ts b/src/app/app.component.ts new file mode 100644 index 0000000..1eacc21 --- /dev/null +++ b/src/app/app.component.ts @@ -0,0 +1,50 @@ +/* + * 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, Inject } from '@angular/core'; +import { AlertService } from './modules/alerting'; +import { environment } from '../environments/environment'; +import { DOCUMENT } from '@angular/common'; +import { LoadingIndicatorService } from 'src/app/services/loading-indicator.service'; +import { KeyboardShortcuts } from './services/shortcut.service'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'], +}) +export class AppComponent { + title = 'frontend'; + isCustomStyleEnabled = environment.customStyleEnabled; + readonly loading$ = this.loadingIndicator.isVisible(); + public isCollapsed = false + public ACCESS_KEY = KeyboardShortcuts; + + constructor( + protected alertService: AlertService, + private loadingIndicator: LoadingIndicatorService, + @Inject(DOCUMENT) private document: Document, + ) { + } + + collapseChanged() { + this.isCollapsed = !this.isCollapsed; + + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts new file mode 100644 index 0000000..faeb566 --- /dev/null +++ b/src/app/app.module.ts @@ -0,0 +1,103 @@ +/* + * 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 { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { HeaderComponent } from './components/layout/header/header/header.component'; +import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; +import { AuthConfigModule } from './modules/auth/auth.config.module'; +import { HttpClientModule } from '@angular/common/http'; +import { AlertModule } from './modules/alerting'; +import { ModalContentComponent } from './components/modal/modal-content'; +import { RequestCache, RequestCacheService } from './services/cacheservice/request-cache.service'; +import { httpInterceptorProviders } from './http-interceptors/interceptors'; +import { SidemenuComponent } from './components/layout/sidemenu/sidemenu.component'; +import { ConfirmationModalComponent } from './components/shared/confirmation-modal/confirmation-modal.component'; +import { environment } from '../environments/environment'; +import { ApiModule, Configuration } from '../../openapi/output'; +import { LoadingIndicatorService } from 'src/app/services/loading-indicator.service'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { PageNotFoundComponent } from './components/page-not-found/page-not-found'; +import { RouteReuseStrategy } from '@angular/router'; +import { AppRouteReuseStrategy } from './router.strategy'; +import { StatusPageComponent } from './components/shared/status-page/status-page.component'; +import { UserAdministrationModule } from './modules/user-administration/user-administration.module'; +import { SharedModule } from './shared.module'; +import { AppStarterModule } from './modules/app-starter/app-starter.module'; +import { DashboardModule } from './modules/dashboard/dashboard.module'; +// Sentry.init({ +// dsn: 'http://726f0fcf0f55429eb1c7e613d25d2daf@10.212.1.83:9000/2', + +// // Enable performance metrics: https://docs.sentry.io/performance-monitoring/getting-started/?platform=javascript +// integrations: [new TracingIntegrations.BrowserTracing()], +// tracesSampleRate: 0.2, +// }); + +// @Injectable() +// export class SentryErrorHandler implements ErrorHandler { +// constructor() {} +// handleError(error: any) { +// Sentry.captureException(error.originalError || error); +// throw error; +// } +// } + +export function changeConfig() { + return new Configuration({ basePath: environment.backendServerUrl }); +} + +@NgModule({ + declarations: [ + AppComponent, + HeaderComponent, + ModalContentComponent, + SidemenuComponent, + PageNotFoundComponent, + ConfirmationModalComponent, + StatusPageComponent, + ], + imports: [ + BrowserModule, + AppRoutingModule, + HttpClientModule, + AuthConfigModule, + AlertModule, + AuthConfigModule, + ApiModule.forRoot(changeConfig), + OAuthModule.forRoot({ resourceServer: { sendAccessToken: false } }), + BrowserAnimationsModule, + UserAdministrationModule, + DashboardModule, + SharedModule, + AppStarterModule, + ], + // { provide: ErrorHandler, useClass: SentryErrorHandler }, + providers: [ + { provide: RequestCache, useClass: RequestCacheService }, + httpInterceptorProviders, + { provide: OAuthStorage, useValue: localStorage }, + LoadingIndicatorService, + { provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy }, + ], + bootstrap: [AppComponent], + entryComponents: [ModalContentComponent, ConfirmationModalComponent], +}) +export class AppModule {} diff --git a/src/app/components/layout/header/header/header.component.css b/src/app/components/layout/header/header/header.component.css new file mode 100644 index 0000000..21de90f --- /dev/null +++ b/src/app/components/layout/header/header/header.component.css @@ -0,0 +1,114 @@ +/* + * 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 + */ + +.navbar { + background-color: #e7e6e6; +} + +.bi-list { + font-size: 1.5rem; +} + +.dropdown-menu { + border-radius: 10px; + border: 0.5px solid #b2b2b2; + text-align: center; + box-shadow: 3px 3px 20px lightslategray; +} + +#dropdown-user, +#dropdown-settings { + text-align: left; +} + +#dropdownMenu:hover, +#dropdownMenu:focus { + background-color: #e7e6e6; +} + +.dropdown-divider { + border-color: #b2b2b2; +} + +.navbar-collapse.collapse.in { + display: inline-block; +} + +.dropdown-toggle::after { + content: initial; +} + +.navbar-toggler { + color: black; +} + +.sidebar-toggler { + background-color: transparent; + border-style: none; +} + +.btn-account { + color: black; + background-color: transparent; + border-style: none; + font-size: 21px; + line-height: 1.225; +} + +.btn:hover { + background-color: #e7e6e6; +} + +/* Make ONAP logo as large as the Telekom one */ +.brand-image { + height: 36px; +} + +.bi.bi-person-fill, +.bi.bi-caret-down-fill { + color: black; +} + +/* Add a clearly visible outline while tabbing */ +:focus-visible { + box-shadow: 0 0 0 2px black; +} +dl > * { + text-align: left; +} + +hr { + margin-top: 1.4rem; + margin-bottom: 1.4rem; + border: 0; + border-top: 1px solid; +} + +.btn { + white-space: nowrap; + display: block; +} + +.btn:hover { + color: var(--black); +} + +.btn-sm, +.btn-group-sm > .btn { + padding: 0.4rem 1.5rem 0.476rem; +} diff --git a/src/app/components/layout/header/header/header.component.html b/src/app/components/layout/header/header/header.component.html new file mode 100644 index 0000000..8e23ffd --- /dev/null +++ b/src/app/components/layout/header/header/header.component.html @@ -0,0 +1,142 @@ +<!-- + ~ 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 + --> + +<nav id="brand-bar" class="navbar navbar-light navbar-expand pl-2"> + <button + type="button" + id="sidebarCollapse" + class="sidebar-toggler" + (click)="toggleSidenav()" + [attr.aria-label]="'layout.header.sidebarToggler' | translate" + > + <i class="bi bi-list" aria-hidden="true"></i> + </button> + + <!-- Logo as Home Button --> + <a class="ml-3" [routerLink]="['/']"> + <img + class="brand-image pl-0" + id="img-logo" + src="assets/images/onap-logo.png" + alt="{{ 'layout.header.logo.onap' | translate }}" + /> + </a> + + <div class="d-flex ml-auto align-items-baseline"> + <button + class="btn btn-invisible p-2 pointer" + [attr.accesskey]="ACCESS_KEY.SHORTCUT_0" + (click)="openCanvas(content)" + [attr.aria-label]="'Help'" + [ngbTooltip]="'Help'" + > + <i aria-hidden="true" class="bi bi-question-circle black"></i> + </button> + <button + *ngIf="!isFullScreen" + class="btn btn-invisible pointer p-2 qa_btn_open_fullscreen" + [attr.aria-label]="'layout.header.button.openFullscreen' | translate" + [ngbTooltip]="'Full screen'" + (click)="openFullscreen()" + > + <i aria-hidden="true" class="bi bi-arrows-fullscreen black"></i> + </button> + <button + *ngIf="isFullScreen" + class="btn btn-invisible pointer p-2 qa_btn_cls_fullscreen" + [attr.aria-label]="'layout.header.button.closeFullscreen' | translate" + [ngbTooltip]="'Exit full screen'" + (click)="closeFullscreen()" + > + <i aria-hidden="true" class="bi bi-fullscreen-exit black"></i> + </button> + </div> + + <!-- Dropdown menu --> + <div ngbDropdown #userAccountDropdown="ngbDropdown" display="dynamic"> + <button + ngbDropdownToggle + id="dropdownMenu" + class="btn btn-account px-3 tab-focus" + [attr.aria-label]="'layout.header.button.useraccount' | translate" + aria-haspopup="true" + > + <i class="bi bi-person-fill" aria-hidden="true"></i> + <i class="bi bi-caret-down-fill" aria-hidden="true"></i> + </button> + <div class="dropdown-menu-right px-4" ngbDropdownMenu aria-labelledby="dropdownMenu" + style="z-index: 9999; min-width: 380px"> + <div class="d-flex justify-content-between align-items-center"> + <div> + <i aria-hidden="true" class="bi bi-person"></i> + {{ profile }} + </div> + <button + class="btn btn-sm btn-primary font-weight-bold" + (click)="userAccountDropdown.close(); logOut()" + [attr.aria-label]="'layout.header.button.logout' | translate" + > + {{ 'layout.header.button.logout' | translate }} + </button> + </div> + <hr /> + <dl> + <dt>{{ 'layout.header.info.name' | translate }}</dt> + <dd>{{ profile }}</dd> + <dt>{{ 'layout.header.info.mail' | translate }}</dt> + <dd>{{ email }}</dd> + </dl> + <hr /> + <button + type="button" + class="btn btn-sm btn-outline-secondary" + placement="top" + container="body" + triggers="click:blur" + [ngbTooltip]="'userAdministration.form.title.changePasswordTooltip' | translate" + > + {{ 'userAdministration.form.title.changePassword' | translate }} + </button> + </div> + </div> +</nav> + +<ng-template #content let-offcanvas> + <div class="offcanvas-header"> + <div class="d-flex"> + <h3 class="mb-0 mr-1">{{ 'layout.header.shortcuts.heading' | translate }}</h3> + </div> + <button + type="button" + class="align-self-center btn-close" + [attr.aria-label]="'common.buttons.close' | translate" + (click)="offcanvas.dismiss(content)" + ></button> + </div> + <div class="offcanvas-body"> + <p class="border-bottom pb-2" *ngFor="let shortcut of shortcuts | keyvalue"> + {{ shortcut.key }} - {{ shortcut.value }} + </p> + <div class="text-muted small"> + <p>{{ 'layout.header.shortcuts.helpText' | translate }}</p> + <div [innerHTML]="'layout.header.shortcuts.helpBrowser1' | translate"></div> + <div [innerHTML]="'layout.header.shortcuts.helpBrowser2' | translate"></div> + <div [innerHTML]="'layout.header.shortcuts.helpBrowser3' | translate"></div> + </div> + </div> +</ng-template> diff --git a/src/app/components/layout/header/header/header.component.spec.ts b/src/app/components/layout/header/header/header.component.spec.ts new file mode 100644 index 0000000..cb40e73 --- /dev/null +++ b/src/app/components/layout/header/header/header.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 { HeaderComponent } from './header.component'; + +describe('HeaderComponent', () => { + let component: HeaderComponent; + let fixture: ComponentFixture<HeaderComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [HeaderComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/layout/header/header/header.component.ts b/src/app/components/layout/header/header/header.component.ts new file mode 100644 index 0000000..f5c1a1c --- /dev/null +++ b/src/app/components/layout/header/header/header.component.ts @@ -0,0 +1,113 @@ +/* + * 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, + Output, + EventEmitter, + OnInit, + HostListener, + ElementRef, + ViewChild, + TemplateRef, +} from '@angular/core'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { FullscreenService } from 'src/app/services/fullscreen.service'; +import { environment } from 'src/environments/environment'; +import { KeyboardShortcuts, ShortcutService } from 'src/app/services/shortcut.service'; +import { NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'app-header', + templateUrl: './header.component.html', + styleUrls: ['./header.component.css'], +}) +export class HeaderComponent implements OnInit { +@Output() collapse = new EventEmitter<void>() + + /** + * + * @param {OAuthService} oauthService + */ + isOnapTheme = false; + switchToMainContent: string = ''; + isFullScreen = false; + changePasswordUrl = `${environment.keycloakEditProfile}/password`; + shortcuts: Map<KeyboardShortcuts,string> = this.shortcutService.getShortcuts(); + + public ACCESS_KEY = KeyboardShortcuts; + @ViewChild('myNavElement') myNavElement!: ElementRef; + + constructor( + private readonly fullscreenService: FullscreenService, + private readonly oauthService: OAuthService, + private offcanvasService: NgbOffcanvas, + private shortcutService: ShortcutService + ) {} + ngOnInit(): void { + this.checkScreenMode(); + } + + @HostListener('document:fullscreenchange', ['$event']) + @HostListener('document:webkitfullscreenchange', ['$event']) + @HostListener('document:mozfullscreenchange', ['$event']) + @HostListener('document:MSFullscreenChange', ['$event']) + private checkScreenMode() { + this.isFullScreen = !!document.fullscreenElement; + } + + public openFullscreen() { + this.fullscreenService.enter(); + } + public closeFullscreen() { + this.fullscreenService.leave(); + } + + public logIn() { + this.oauthService.initCodeFlow(); + } + + public logOut() { + this.oauthService.logOut(); + } + + get profile() { + const claims = Object(this.oauthService.getIdentityClaims()); + return claims.given_name ? claims.given_name : 'no Name'; + } + + get email() { + const claims = Object(this.oauthService.getIdentityClaims()); + return claims.email ? claims.email : 'no Email'; + } + + public toggleSidenav() { + this.collapse.emit(); + } + + + public openCanvas(content: TemplateRef<any>) { + const isCanvasOpened = this.offcanvasService.hasOpenOffcanvas(); + if (isCanvasOpened) { + this.offcanvasService.dismiss(); + } else { + this.offcanvasService.open(content, { ariaLabelledBy: 'Keyboard shortcuts', position: 'end' }); + } + } +} diff --git a/src/app/components/layout/sidemenu/sidemenu.component.css b/src/app/components/layout/sidemenu/sidemenu.component.css new file mode 100644 index 0000000..23831b7 --- /dev/null +++ b/src/app/components/layout/sidemenu/sidemenu.component.css @@ -0,0 +1,113 @@ +/* + * 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 + */ + + +/* Sidebar container with nav menu */ +#sidebar-container { + margin-right: -1px; + flex: 0 0 230px; + border-right-style: solid; + border-right-width: 1px; + border-right-color: #b2b2b2; +} + +/* Menu item*/ +.nav a { + height: 50px; + color: black; +} + +/* Li elements from sidemenu. Thanks to padding is :focus pseudo-class visible for keyboard users*/ +.nav li { + padding: 2px; +} + +/* Separators */ +.sidebar-separator-title { + height: 2em; +} + +/* Sidebar sizes when collapsed*/ +.sidebar-collapsed { + min-width: 60px !important; + max-width: 63px !important; + height: auto; +} + +/* Sidebar sizes when expanded*/ +.sidebar-expanded { + min-width: 250px; + max-width: 250px; + height: 100%; + display: block; + border-right-style: solid; + border-right-width: 1px; + border-right-color: #c5c5c5; +} + +/* Hide list item and grouping item title if container sidebar is collapsed */ +.sidebar-collapsed a.nav-link span, +.sidebar-collapsed li.sidebar-separator-title small { + display: none !important; +} + +/* Hide grouping item icon if container sidebar is collapsed */ +.sidebar-expanded ul li.sidebar-separator-title i, +.sidebar-expanded ul li.sidebar-separator-title img { + display: none !important; +} + +/* Show grouping item icon if container sidebar is collapsed */ +.sidebar-collapsed ul li.sidebar-separator-title i, +.sidebar-collapsed ul li.sidebar-separator-title img { + display: inline-block; +} + +/* All list items */ +.nav-link { + border-style: none; + display: block !important; + padding: 0.75rem 1rem; +} + +ul { + list-style-type: none; +} + +.active-menu-item { + background-color: var(--active-gray); + color: black; + border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; + border-left: 2px solid var(--primary) !important; +} + +.nav.inner li { + border: none; +} + +.portal-version-title, +.portal-version-number { + font-size: 13px; +} + +img { + width: 18px; + height: 19px; + vertical-align: -0.125em; +} diff --git a/src/app/components/layout/sidemenu/sidemenu.component.html b/src/app/components/layout/sidemenu/sidemenu.component.html new file mode 100644 index 0000000..fd97c50 --- /dev/null +++ b/src/app/components/layout/sidemenu/sidemenu.component.html @@ -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 + --> + + +<nav + [class.sidebar-collapsed]="isSidebarCollapsed" + id="sidebar-container" + class="sidebar-expanded overflow-auto d-flex flex-column justify-content-between" + [attr.aria-label]="'layout.menu.mainMenu' | translate" +> + <ul class="nav flex-column flex-nowrap" [attr.aria-label]="'layout.menu.menuItems' | translate"> + <li class="nav-item"> + <a #test [attr.accesskey]='ACCESS_KEY.SHORTCUT_6' class="nav-link" routerLinkActive="active-menu-item" [routerLink]="['/dashboard']"> + <i class="bi bi-house-door mr-4" aria-hidden="true"></i> + <span class="d-sm-inline qa_menu_home">{{ 'layout.menu.items.dashboard' | translate }}</span></a + > + </li> + <li class="nav-item"> + <a class="nav-link" routerLinkActive="active-menu-item" [routerLink]="['/app-starter']"> + <i class="bi bi-grid mr-4" aria-hidden="true"></i> + <span class="d-sm-inline qa_menu_app_starter">{{ 'layout.menu.items.appStarter' | translate }}</span></a + > + </li> + + <li class="nav-item" [appHasPermissions]="'users.administration.list'"> + <a class="nav-link" routerLinkActive="active-menu-item" routerLink="/user-administration"> + <i class="bi bi-people mr-4" aria-hidden="true"></i> + <span class="d-sm-inline qa_menu_users">{{ 'layout.menu.items.users' | translate }}</span></a + > + </li> + </ul> + <div [ngClass]="isSidebarCollapsed ? 'flex-column' : 'flex-row'" class="d-flex justify-content-center text-center"> + <h5 class="portal-version-title mr-1" id="portal-version">Portal Version:</h5> + <span class="portal-version-number" aria-labelledby="portal-version" (click)='test.blur()'>{{versionNumber}}</span> + </div> +</nav> diff --git a/src/app/components/layout/sidemenu/sidemenu.component.spec.ts b/src/app/components/layout/sidemenu/sidemenu.component.spec.ts new file mode 100644 index 0000000..1067c49 --- /dev/null +++ b/src/app/components/layout/sidemenu/sidemenu.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 { SidemenuComponent } from './sidemenu.component'; + +describe('SidemenuComponent', () => { + let component: SidemenuComponent; + let fixture: ComponentFixture<SidemenuComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [SidemenuComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SidemenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/layout/sidemenu/sidemenu.component.ts b/src/app/components/layout/sidemenu/sidemenu.component.ts new file mode 100644 index 0000000..1f5c123 --- /dev/null +++ b/src/app/components/layout/sidemenu/sidemenu.component.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 { Component, Injectable, Input } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import VersionJson from 'src/assets/version.json'; +import { KeyboardShortcuts } from '../../../services/shortcut.service'; + +@Injectable({ + providedIn: 'root', +}) +@Component({ + selector: 'app-sidemenu', + templateUrl: './sidemenu.component.html', + styleUrls: ['./sidemenu.component.css'], +}) +export class SidemenuComponent { + versionNumber: string; + + @Input() isSidebarCollapsed = false; + + public ACCESS_KEY = KeyboardShortcuts; + public keycloakEditProfile = environment.keycloakEditProfile; + public isKpiDashboardSubMenuCollapsed = false; + + constructor() { + this.versionNumber = VersionJson.number; + } + collapsed() { + this.isKpiDashboardSubMenuCollapsed = !this.isKpiDashboardSubMenuCollapsed; + } +} diff --git a/src/app/components/modal/modal-content.html b/src/app/components/modal/modal-content.html new file mode 100644 index 0000000..ec3b6c4 --- /dev/null +++ b/src/app/components/modal/modal-content.html @@ -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 + --> + + +<div class="modal-body m-3" style="text-align: justify"> + <h4 class="modal-title">{{'modal.error.accessDenied' | translate}}</h4> + <p><br />{{'modal.error.accessDenied' | translate}}</p> + <details> + <summary>{{'modal.error.details' | translate}}</summary> + <p>{{message}}</p> + </details> + <div style="text-align: right"> + <button class="btn btn-primary" (click)="oauthService.logOut()">Logout</button> + </div> +</div> diff --git a/src/app/components/modal/modal-content.ts b/src/app/components/modal/modal-content.ts new file mode 100644 index 0000000..01211df --- /dev/null +++ b/src/app/components/modal/modal-content.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 { Component, Input } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { OAuthService } from 'angular-oauth2-oidc'; + +@Component({ + selector: 'app-modal-content', + templateUrl: './modal-content.html', +}) +export class ModalContentComponent { + @Input() message: any; + + constructor(public activeModal: NgbActiveModal, public oauthService: OAuthService) {} +} diff --git a/src/app/components/modal/modal.service.ts b/src/app/components/modal/modal.service.ts new file mode 100644 index 0000000..083bcb1 --- /dev/null +++ b/src/app/components/modal/modal.service.ts @@ -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 + */ + + +import { Injectable } from '@angular/core'; +import { NgbModal, NgbModalConfig } from '@ng-bootstrap/ng-bootstrap'; +import { ModalContentComponent } from './modal-content'; + +@Injectable({ + providedIn: 'root', +}) +export class ModalService { + constructor(private modalService: NgbModal, private modalConfig: NgbModalConfig) { + // customize default values of modals used by this component tree + modalConfig.backdrop = 'static'; + modalConfig.keyboard = false; + } + + open(message: string) { + const modalRef = this.modalService.open(ModalContentComponent,{backdropClass:'backdropClass'}); + modalRef.componentInstance.message = message; + } +} diff --git a/src/app/components/page-not-found/page-not-found.css b/src/app/components/page-not-found/page-not-found.css new file mode 100644 index 0000000..2c10482 --- /dev/null +++ b/src/app/components/page-not-found/page-not-found.css @@ -0,0 +1,36 @@ +/* + * 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 + */ + + +.wrapper { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-40%, -50%); +} + +.woman-img { + height: 350px; +} + +.icon { + position: absolute; + top: 5px; + font-size: 30px; + color: var(--primary); +} diff --git a/src/app/components/page-not-found/page-not-found.html b/src/app/components/page-not-found/page-not-found.html new file mode 100644 index 0000000..6ad90c6 --- /dev/null +++ b/src/app/components/page-not-found/page-not-found.html @@ -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 + --> + + +<div class="container text-center wrapper"> + <img + class="woman-img mb-5" + src="assets/images/icons/standing-woman.svg" + alt="{{'pageNotFound.imgAltText' | translate}}" + /> + <i class="bi bi-x-circle-fill icon" aria-hidden="true"></i> + <h3 class="mb-3">{{'pageNotFound.text' | translate}}</h3> + <button type="button" class="btn btn-primary" routerLink="/dashboard">{{'pageNotFound.button' | translate}}</button> +</div> diff --git a/src/app/components/page-not-found/page-not-found.ts b/src/app/components/page-not-found/page-not-found.ts new file mode 100644 index 0000000..88cdc01 --- /dev/null +++ b/src/app/components/page-not-found/page-not-found.ts @@ -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 + */ + + +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-page-not-found', + templateUrl: './page-not-found.html', + styleUrls: ['./page-not-found.css'], +}) +export class PageNotFoundComponent {} diff --git a/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.html b/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.html new file mode 100644 index 0000000..72d5c79 --- /dev/null +++ b/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.html @@ -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 + --> + + +<ng-template> + <ng-content></ng-content> +</ng-template> diff --git a/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.ts b/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.ts new file mode 100644 index 0000000..5f74fff --- /dev/null +++ b/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.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 { Component, TemplateRef, ViewChild } from '@angular/core'; + +@Component({ + selector: 'app-breadcrumb-item', + templateUrl: './breadcrumb-item.component.html', +}) +export class BreadcrumbItemComponent { + @ViewChild(TemplateRef, { static: true }) + readonly template!: TemplateRef<any>; +} diff --git a/src/app/components/shared/breadcrumb/breadcrumb.component.html b/src/app/components/shared/breadcrumb/breadcrumb.component.html new file mode 100644 index 0000000..e6cdba5 --- /dev/null +++ b/src/app/components/shared/breadcrumb/breadcrumb.component.html @@ -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 + --> + + +<nav [attr.aria-label]="'layout.main.breadcrumb' | translate"> + <ol class="breadcrumb"> + <li *ngFor="let item of items; index as i" class="breadcrumb-item"> + <ng-container *ngTemplateOutlet="item.template" accessKey='3'></ng-container> + </li> + </ol> +</nav> diff --git a/src/app/components/shared/breadcrumb/breadcrumb.component.ts b/src/app/components/shared/breadcrumb/breadcrumb.component.ts new file mode 100644 index 0000000..864a9ff --- /dev/null +++ b/src/app/components/shared/breadcrumb/breadcrumb.component.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 { Component, ContentChildren, QueryList } from '@angular/core'; +import { BreadcrumbItemComponent } from '../breadcrumb-item/breadcrumb-item.component'; + +@Component({ + selector: 'app-breadcrumb', + templateUrl: './breadcrumb.component.html', +}) +export class BreadcrumbComponent { + @ContentChildren(BreadcrumbItemComponent) + readonly items!: QueryList<BreadcrumbItemComponent>; +} diff --git a/src/app/components/shared/confirmation-modal/confirmation-modal.component.html b/src/app/components/shared/confirmation-modal/confirmation-modal.component.html new file mode 100644 index 0000000..4f26c5d --- /dev/null +++ b/src/app/components/shared/confirmation-modal/confirmation-modal.component.html @@ -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 + --> + + +<div class="modal-header qa_modal_header"> + <h4 class="modal-title qa_modal_title" id="modal-title"> + <strong>{{ title }}</strong> + </h4> +</div> +<div class="modal-body qa_modal_body"> + <p> + {{ text }} + </p> +</div> +<div *ngIf='showOkBtn || showCancelBtn' class="modal-footer qa_modal_footer"> + <button *ngIf='showCancelBtn' type="button" class="btn btn-default qa_cancel_button" (click)="activeModal.close(false)"> + {{ cancelText }} + </button> + <button *ngIf='showOkBtn' type="button" class="btn btn-danger qa_apply_button" (click)="activeModal.close(true)">{{ okText }}</button> +</div> diff --git a/src/app/components/shared/confirmation-modal/confirmation-modal.component.spec.ts b/src/app/components/shared/confirmation-modal/confirmation-modal.component.spec.ts new file mode 100644 index 0000000..b7b5110 --- /dev/null +++ b/src/app/components/shared/confirmation-modal/confirmation-modal.component.spec.ts @@ -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/components/shared/confirmation-modal/confirmation-modal.component.ts b/src/app/components/shared/confirmation-modal/confirmation-modal.component.ts new file mode 100644 index 0000000..023ba5e --- /dev/null +++ b/src/app/components/shared/confirmation-modal/confirmation-modal.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 { TranslateService } from '@ngx-translate/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'app-confirmation-modal', + templateUrl: './confirmation-modal.component.html' +}) +export class ConfirmationModalComponent { + constructor(public activeModal: NgbActiveModal, private readonly translateService: TranslateService) {} + + @Input() showOkBtn = true; + @Input() showCancelBtn = true; + + @Input() + okText = this.translateService.instant('common.buttons.save'); + cancelText = this.translateService.instant('common.buttons.cancel'); + title = ''; + text = ''; +} diff --git a/src/app/components/shared/loading-spinner/loading-spinner.component.html b/src/app/components/shared/loading-spinner/loading-spinner.component.html new file mode 100644 index 0000000..f9dd6ab --- /dev/null +++ b/src/app/components/shared/loading-spinner/loading-spinner.component.html @@ -0,0 +1,21 @@ +<!-- + ~ 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="spinner-border spinner-border-sm text-light qa_alarm_spinner" role="status"> + <span class="sr-only">{{'common.loading' | translate}}</span> +</div> diff --git a/src/app/components/shared/loading-spinner/loading-spinner.component.ts b/src/app/components/shared/loading-spinner/loading-spinner.component.ts new file mode 100644 index 0000000..c99a085 --- /dev/null +++ b/src/app/components/shared/loading-spinner/loading-spinner.component.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 { Component } from '@angular/core'; + +@Component({ + selector: 'app-loading-spinner', + templateUrl: './loading-spinner.component.html', +}) +export class LoadingSpinnerComponent { +} diff --git a/src/app/components/shared/pagination/pagination.component.css b/src/app/components/shared/pagination/pagination.component.css new file mode 100644 index 0000000..b864207 --- /dev/null +++ b/src/app/components/shared/pagination/pagination.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 + */ + + +ngb-pagination::ng-deep li .page-link:focus { + box-shadow: 0 0 0 0.2rem rgb(0 123 255 / 25%); + border-color: #00a0de; +} diff --git a/src/app/components/shared/pagination/pagination.component.html b/src/app/components/shared/pagination/pagination.component.html new file mode 100644 index 0000000..ad198fc --- /dev/null +++ b/src/app/components/shared/pagination/pagination.component.html @@ -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 + --> + + +<div class="d-flex flex-wrap justify-content-between mt-3"> + <div> + <ngb-pagination + [collectionSize]="collectionSize" + [pageSize]="pageSize" + [page]="page" + (pageChange)="emitPageChange($event)" + ></ngb-pagination> + <span class="ml-2 small"> + {{ 'common.pagination.totalCount' | translate: { value: collectionSize } }} + </span> + </div> + <div> + <label for="item-select" class="sr-only"> + {{ 'common.select.description' | translate }} + </label> + <select + id="item-select" + class="custom-select" + style="width: auto" + [ngModel]="pageSize" + (ngModelChange)="emitModelChange($event)" + > + <option *ngFor="let n of itemsPerPage" [ngValue]="n"> + {{ 'common.select.itemsPerPage' | translate: { value: n } }} + </option> + </select> + </div> +</div> diff --git a/src/app/components/shared/pagination/pagination.component.spec.ts b/src/app/components/shared/pagination/pagination.component.spec.ts new file mode 100644 index 0000000..7ffe9fc --- /dev/null +++ b/src/app/components/shared/pagination/pagination.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 { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PaginationComponent } from './pagination.component'; + +describe('PaginationComponent', () => { + let component: PaginationComponent; + let fixture: ComponentFixture<PaginationComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PaginationComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PaginationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/shared/pagination/pagination.component.ts b/src/app/components/shared/pagination/pagination.component.ts new file mode 100644 index 0000000..fee827d --- /dev/null +++ b/src/app/components/shared/pagination/pagination.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, EventEmitter, Input, Output } from '@angular/core'; +import { NgbPaginationConfig } from '@ng-bootstrap/ng-bootstrap'; + +/** + * This is a wrapper component for the `ngbPagination` component of ngBootstrap ([official wiki page](https://ng-bootstrap.github.io/#/components/pagination/overview)). + * This contains the pagination element, as well as the selection element for the page size. + * + * + * Deal with both using the `pageChange` and `pageSizeChange` events, i.e in your template: + * ``` html + * <app-pagination + * [collectionSize]="..." + [pageSize]="..." + [page]="..." + * (pageChange)="changePage($event)" + * (pageSizeChange)="changePageSize($event)" + * > + * </app-pagination> + * ``` + */ +@Component({ + selector: 'app-pagination', + templateUrl: './pagination.component.html', + styleUrls: ['./pagination.component.css'], + providers: [NgbPaginationConfig], +}) +export class PaginationComponent { + /** + * This event is fired when an item in the `select`-element is changed + */ + @Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>(); + + /** + * Specify what page sizes should be selectable by the user. + */ + @Input() itemsPerPage = [10, 20, 50]; + + @Output() pageChange: EventEmitter<number> = new EventEmitter<number>(); + @Input() collectionSize = 10; + @Input() pageSize = 10; + @Input() page = 1; + + constructor(config: NgbPaginationConfig) { + config.boundaryLinks = true; + config.directionLinks = true; + config.disabled = false; + config.ellipses = false; + config.maxSize = 3; + config.pageSize = 10; + config.rotate = true; + config.size = 'sm'; + } + + /** + * Emit the currently selected page from the `ngb-Pagination` + * @param page the page that is selected + */ + emitPageChange(page: number) { + this.pageChange.emit(page); + } + + /** + * Emit the currently selected page size from the `select` + * @param size the number of items per page that is selected + */ + emitModelChange(size: number) { + this.pageSizeChange.emit(size); + } +} diff --git a/src/app/components/shared/status-page/status-page.component.css b/src/app/components/shared/status-page/status-page.component.css new file mode 100644 index 0000000..b7b5110 --- /dev/null +++ b/src/app/components/shared/status-page/status-page.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/components/shared/status-page/status-page.component.html b/src/app/components/shared/status-page/status-page.component.html new file mode 100644 index 0000000..28e65c6 --- /dev/null +++ b/src/app/components/shared/status-page/status-page.component.html @@ -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 + --> + + +<div class="d-flex justify-content-center align-items-center flex-column h-100"> + <h2 class="text-center">{{header}}</h2> + <h3>{{message}}</h3> +</div> diff --git a/src/app/components/shared/status-page/status-page.component.spec.ts b/src/app/components/shared/status-page/status-page.component.spec.ts new file mode 100644 index 0000000..f8c547e --- /dev/null +++ b/src/app/components/shared/status-page/status-page.component.spec.ts @@ -0,0 +1,44 @@ +/* + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StatusPageComponent } from './status-page.component'; + +describe('StatusPageComponent', () => { + let component: StatusPageComponent; + let fixture: ComponentFixture<StatusPageComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ StatusPageComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(StatusPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/shared/status-page/status-page.component.ts b/src/app/components/shared/status-page/status-page.component.ts new file mode 100644 index 0000000..3cbbf46 --- /dev/null +++ b/src/app/components/shared/status-page/status-page.component.ts @@ -0,0 +1,37 @@ +/* + * 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 { Router } from '@angular/router'; + +@Component({ + selector: 'app-status-page', + templateUrl: './status-page.component.html', + styleUrls: ['./status-page.component.css'], +}) +export class StatusPageComponent { + header: string; + message: string; + + constructor(private router: Router) { + const data = this.router.getCurrentNavigation(); + this.header = data?.extras?.state?.header; + this.message = data?.extras?.state?.message; + } +} diff --git a/src/app/components/shared/table-skeleton/table-skeleton.component.css b/src/app/components/shared/table-skeleton/table-skeleton.component.css new file mode 100644 index 0000000..0870694 --- /dev/null +++ b/src/app/components/shared/table-skeleton/table-skeleton.component.css @@ -0,0 +1,37 @@ +/* + * 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 + */ + + +@keyframes ghost { + from { + background-position: 0 0; + } + to { + background-position: 100vw 0; + } +} + +.line { + width: 100%; + height: 50px; + margin-top: 10px; + border-radius: 3px; + background: linear-gradient(90deg, #f0f0f0, #d8d6d6, #f0f0f0) 0 0/ 80vh 100% fixed; + background-color: var(--secondary); + animation: ghost 4000ms infinite linear; +} diff --git a/src/app/components/shared/table-skeleton/table-skeleton.component.html b/src/app/components/shared/table-skeleton/table-skeleton.component.html new file mode 100644 index 0000000..717da1f --- /dev/null +++ b/src/app/components/shared/table-skeleton/table-skeleton.component.html @@ -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 + --> + + +<div id="table-skeleton"> + <div class="line"></div> + <div class="line"></div> + <div class="line"></div> + <div class="line"></div> + <div class="line"></div> + <div class="line"></div> + <div class="line"></div> + <div class="line"></div> + <div class="line"></div> + <div class="line"></div> +</div> diff --git a/src/app/components/shared/table-skeleton/table-skeleton.component.ts b/src/app/components/shared/table-skeleton/table-skeleton.component.ts new file mode 100644 index 0000000..41638fe --- /dev/null +++ b/src/app/components/shared/table-skeleton/table-skeleton.component.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 { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'app-table-skeleton', + templateUrl: './table-skeleton.component.html', + styleUrls: ['./table-skeleton.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TableSkeletonComponent { +} diff --git a/src/app/directives/has-permissions.directive.ts b/src/app/directives/has-permissions.directive.ts new file mode 100644 index 0000000..994cee8 --- /dev/null +++ b/src/app/directives/has-permissions.directive.ts @@ -0,0 +1,57 @@ +/* + * 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 { Directive, ElementRef, Inject, Input } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { ACL_CONFIG, AclConfig } from '../modules/auth/injection-tokens'; +import { AuthService } from '../services/auth.service'; +import { takeUntil } from 'rxjs/operators'; +import { UnsubscribeService } from '../services/unsubscribe/unsubscribe.service'; + +@Directive({ + selector: '[appHasPermissions]', + providers: [UnsubscribeService] +}) +export class HasPermissionsDirective { + @Input() appHasPermissions = ''; + constructor( + private el: ElementRef, + readonly httpClient: HttpClient, + readonly authService: AuthService, + @Inject(ACL_CONFIG) readonly acl: AclConfig, + private unsubscribeService: UnsubscribeService + ) { + // for unknown reasons this must be wrapped in set timeout, otherwise appHasPermissions is sometimes empty string + setTimeout(() => { + this.el.nativeElement.style.display = 'none'; + this.authService + .loadCachedUserProfile() + .pipe(takeUntil(this.unsubscribeService.unsubscribe$)) + .subscribe(userProfile => { + const intersectionOfRoles = Object.keys(acl).filter(value => userProfile?.roles.includes(value)); + for (const role of intersectionOfRoles) { + if (acl[role].includes(this.appHasPermissions)) { + this.el.nativeElement.style.display = 'initial'; + return; + } + } + }) + }, 0); + } +} diff --git a/src/app/directives/hide-empty.directive.ts b/src/app/directives/hide-empty.directive.ts new file mode 100644 index 0000000..c6ee8a6 --- /dev/null +++ b/src/app/directives/hide-empty.directive.ts @@ -0,0 +1,65 @@ +/* + * 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 { AfterViewChecked, Directive, ElementRef, Input, OnChanges, SimpleChanges } from '@angular/core'; + +//This directive we are using in switch functionality at attribute pane. We can hide attributes without value or show all the attributes. +// We are using this directive in the html file in the element (div) that wraps all the app-detail-rows. + +@Directive({ + selector: '[appHideEmptyDetailRow]', +}) +export class HideEmptyDetailRowDirective implements OnChanges, AfterViewChecked { + @Input() appHideEmptyDetailRow!: boolean; + constructor(private el: ElementRef) {} + + ngOnChanges(changes: SimpleChanges) { + if (changes.appHideEmptyDetailRow && this.el.nativeElement.textContent) { + this.setVisibility(this.el); + } + } + + ngAfterViewChecked() { + if (this.appHideEmptyDetailRow && this.el.nativeElement.textContent) { + this.setVisibility(this.el); + } + } + + public setVisibility(ref: ElementRef) { + if (this.appHideEmptyDetailRow) { + const detailRows = ref.nativeElement.querySelectorAll('app-detail-row'); + detailRows.forEach((item: any) => { + const span = item.querySelector('span'); + if(!span){ + return; + } + if (span.textContent === '' || span.textContent === '-') { + item.style.display = 'none'; + } else { + item.style.display = 'block'; + } + }); + } else { + const detailRows = ref.nativeElement.querySelectorAll('app-detail-row'); + detailRows.forEach((item: any) => { + item.style.display = 'block'; + }); + } + } +} diff --git a/src/app/errorhandling/global-error-handler.ts b/src/app/errorhandling/global-error-handler.ts new file mode 100644 index 0000000..0886232 --- /dev/null +++ b/src/app/errorhandling/global-error-handler.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 { ErrorHandler, Injectable, Injector } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { AlertService } from '../modules/alerting'; + +/** + * This class is intended for global error handling + */ +// See: https://pusher.com/tutorials/error-handling-angular-part-1 +@Injectable() +export class GlobalErrorHandler implements ErrorHandler { + constructor(private injector: Injector, private alertService: AlertService) {} + + handleError(error: Error | HttpErrorResponse) { + this.alertService.error(error.message); + } +} diff --git a/src/app/guards/auth.guard.ts b/src/app/guards/auth.guard.ts new file mode 100644 index 0000000..54ede0f --- /dev/null +++ b/src/app/guards/auth.guard.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 { Injectable } from '@angular/core'; +import { CanActivate, UrlTree } from '@angular/router'; +import { Observable } from 'rxjs'; +import { AuthService } from '../services/auth.service'; + +/** + * grants permissions based on the `AuthService` + */ +@Injectable({ + providedIn: 'root', +}) +export class AuthGuard implements CanActivate { + roles: string = ''; + + constructor(private authService: AuthService) {} + + canActivate(): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { + return this.authService.hasPermissions(); + } +} diff --git a/src/app/guards/edit-user.can-activate.guard.ts b/src/app/guards/edit-user.can-activate.guard.ts new file mode 100644 index 0000000..81fc36e --- /dev/null +++ b/src/app/guards/edit-user.can-activate.guard.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 { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router'; +import { Observable, of } from 'rxjs'; +import { UsersService } from '../../../openapi/output'; +import { catchError, map } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +@Injectable({ + providedIn: 'root', +}) +export class EditUserCanActivateGuard implements CanActivate { + constructor(private usersService: UsersService, private router: Router, private translateService: TranslateService) {} + canActivate(route: ActivatedRouteSnapshot): Observable<boolean> { + const userId = route.paramMap.get('userId'); + if (userId) { + return this.usersService.getUser(userId).pipe( + catchError(() => { + this.router.navigate(['/statusPage'], { + state: { + header: this.translateService.instant('userAdministration.messages.warnings.userDeleted.header'), + message: this.translateService.instant('userAdministration.messages.warnings.userDeleted.message'), + }, + }); + return of(false); + }), + map(() => { + return true; + }), + ); + } + return of(false); + } +} diff --git a/src/app/guards/has-permissions.guard.ts b/src/app/guards/has-permissions.guard.ts new file mode 100644 index 0000000..cc04673 --- /dev/null +++ b/src/app/guards/has-permissions.guard.ts @@ -0,0 +1,72 @@ +/* + * 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 { ActivatedRouteSnapshot, CanActivate, Router, UrlTree } from '@angular/router'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { ACL_CONFIG, AclConfig } from '../modules/auth/injection-tokens'; +import { AuthService } from '../services/auth.service'; +import { TranslateService } from '@ngx-translate/core'; +import { map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root', +}) +export class HasPermissionsGuard implements CanActivate { + constructor( + private readonly authService: AuthService, + private readonly httpClient: HttpClient, + private readonly router: Router, + private readonly translateService: TranslateService, + @Inject(ACL_CONFIG) readonly acl: AclConfig, + ) {} + + canActivate( + next: ActivatedRouteSnapshot, + ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { + return this.authService.loadCachedUserProfile().pipe( + map(userProfile => { + // filter out the keys (the onap_ roles) that the user does not have + const intersectionOfRoles = Object.keys(this.acl).filter(role => userProfile?.roles.includes(role)); + return this.hasPermissions(next.data.permission, intersectionOfRoles); + })); + } + + /** + * Check if a user has a given permission. + * @param permission the permission, as defined in the acl.json + * @param roles the roles that the user possesses + * @returns true if the user has the needed permission + */ + private hasPermissions(permission: string, roles: string[]) { + for (const role of roles) { + if (this.acl[role].includes(permission)) { + return true; + } + } + this.router.navigate(['/statusPage'], { + state: { + header: this.translateService.instant('common.noPermissions.noPermissions'), + message: this.translateService.instant('common.noPermissions.support'), + }, + }); + return false; + } +} diff --git a/src/app/guards/pending-changes.guard.ts b/src/app/guards/pending-changes.guard.ts new file mode 100644 index 0000000..625a7b1 --- /dev/null +++ b/src/app/guards/pending-changes.guard.ts @@ -0,0 +1,42 @@ +/* + * 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 { CanDeactivate } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; + +export interface ComponentCanDeactivate { + canDeactivate: () => boolean; +} + +@Injectable() +export class PendingChangesGuard implements CanDeactivate<ComponentCanDeactivate> { + constructor(public translateService: TranslateService) {} + + canDeactivate(component: ComponentCanDeactivate): boolean | Observable<boolean> { + // if there are no pending changes, just allow deactivation; else confirm first + return component.canDeactivate() + ? true + : // NOTE: this warning message will only be shown when navigating elsewhere within your angular app; + // when navigating away from your angular app, the browser will show a generic warning message + // see http://stackoverflow.com/a/42207299/7307355 + confirm(this.translateService.instant('serviceModels.warningMessage.warning')); + } +} diff --git a/src/app/helpers/filter-helpers.ts b/src/app/helpers/filter-helpers.ts new file mode 100644 index 0000000..cc9f13e --- /dev/null +++ b/src/app/helpers/filter-helpers.ts @@ -0,0 +1,256 @@ +/* + * 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 { Filter } from 'src/app/helpers/listing'; + +/** + * The JSONPath Filter Operators that are used for comparison (subset of [these](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-SQLJSON-FILTER-EX-TABLE)). + * For instance: + * ``` + * $.id == 1 + * $.type like_regex "type_(a|b)" + * ``` + */ +export enum FilterOperator { + EQUAL = ' == ', + NOT_EQUAL = ' != ', + LIKE_REGEX = ' like_regex ', + OR = ' || ', + EQUAL_IN = ' IN ' +} + +/** + * Turns a Filter that is not formatted into a valid JSONPath one + * @param filter the filter that is not in JSONPath format + * @param string pass string: true if you want a return type string + * @returns a Filter with formatted: true or a string when parameter string: true + */ +export function createJsonPathFilter(filter: Filter): Filter; +export function createJsonPathFilter(filter: Filter, returnsString: true): string; +export function createJsonPathFilter(filter: Filter, returnsString = false): Filter | string { + // like_regex only works with strings + if (filter.operator === FilterOperator.LIKE_REGEX) { + if (returnsString) { + return createFilter(addFieldPrefix(filter.parameter), filter.operator, toSearchRegex(filter.value.toString())); + } + return { + parameter: addFieldPrefix(filter.parameter), + operator: filter.operator, + value: toSearchRegex(filter.value.toString()), + }; + } + if (typeof filter.value === 'number') { + if (returnsString) { + return createFilter(addFieldPrefix(filter.parameter), filter.operator, filter.value.toString()); + } + return { + parameter: addFieldPrefix(filter.parameter), + operator: filter.operator, + value: filter.value, + }; + } + if (returnsString) { + return createFilter(addFieldPrefix(filter.parameter), filter.operator, quote(filter.value)); + } + return { + parameter: addFieldPrefix(filter.parameter), + operator: filter.operator, + value: quote(filter.value), + }; +} + +/** + * Concatenates the provided input into a string. + * @param parameter the field to filter + * @param value the value it should match + * @param operator the operator for comparison (`==`,`!=`,`like_regex`,...) + * @param forApi + * @returns a concatenated string + */ +export function createFilter(parameter: string, operator: FilterOperator, value: string | string[], forApi= false): string { + if (forApi && operator === FilterOperator.EQUAL_IN) { + const valueArray = Array.isArray(value) ? value : value.split(','); + return addParentheses(createOrCondition(composeFilterFromArray(valueArray,parameter,FilterOperator.EQUAL))); + } + return `${parameter}${operator}${value}`; +} +/** + * Create array of composed filter + * @param value array of strings e.g array of IDs for alarms + * @param parameter the field to filter e.g $.id + * @param operator the operator for comparison + * @returns a string[] + */ +function composeFilterFromArray(value: string[], parameter: string, operator: FilterOperator): string[] { + return value + .reduce((acc: string[], curr) => [...acc, `${parameter}${operator}${curr}`], []) +} +/** + * Join array of string using '||' operator + * @param value string[] + * @returns a string + */ +function createOrCondition(value: string[]) { + return value.join(FilterOperator.OR) +} +/** + * Wraps a string in parentheses `"string"` -> `("string")` + * @param value string to wrap in quotes + * @returns a string wrapped in quotes + */ +function addParentheses(value: string) { + return ` ( ${value} ) ` +} +/** + * Wraps a string in quotes `"string"` -> `"'string'"` + * @param value string to wrap in quotes + * @returns a string wrapped in quotes + */ +export function quote(value: string): string { + return `"${value}"`; +} + +/** + * Add a `$.` prefix to the provided field + * @param value the field to match against + * @returns the field with a `$.` prefix + */ +export function addFieldPrefix(value: string): string { + return `$.${value}`; +} + +/** + * Concatenate a list of categories into a regex expression for alternative matches. + * I.e `[a,b,c]` -> `"^(a|b|c)$"` + * @param categories the alternative values that should be matched + * @returns a regex expression of alternative matches + */ +export function toCategoricalRegex(categories: string[]): string { + return `"^(${categories.join('|')})$"`; +} + +/** + * Extracts the list of categories from a given regular expression. + * I.e `"^(a|b|c)$"` -> `[a,b,c]` + * @param regex a regex expression of alternative matches + * @returns the categories from the regex + */ +export function fromCategoricalRegex(regex: string): string[] { + let strippedRegex = regex.slice(3, regex.length - 3); + return strippedRegex.split('|'); +} + +/** + * Turns the given searchTerm into a case insensitive value to be used with like_regex + * I.e `term` -> `"term" flag "i"` + * @param searchTerm the search term + * @returns the term in the format to be used with like_regex + */ +export function toSearchRegex(searchTerm: string): string { + return `"${searchTerm}" + flag "i"`; +} + +/** + * Strips the value of a like_regex filter off its formatting. + * I.e `"value" flag "i"` -> `value` + * @param regex the value for the `like_regex` operation + * @returns the value without quotes and flags + */ +export function fromSearchRegex(regex: string): string { + const rawValue = regex?.split(' ')[0]; + if (rawValue.charAt(0) != '"' || rawValue.charAt(rawValue.length - 1) != '"') { + throw new Error('Error while extracting the value from the url. Is it in the correct format?'); + } + return rawValue.substring(1, rawValue.length - 1); +} + +/** + * Join the individual filters with `&&` into a long expression + * @param filters the array of filter strings + * @returns a long filter string chained by `&&`'s + */ +export function composeFilter(filters: string[]): string { + return filters.join('&&'); +} + +/** + * Turn the provided string of filters into a Map of filters + * @param filter the filter string + * @returns a Map of filters + */ +export function parseFilterFromUrl(filter: string | null): Map<string, Filter> { + if (!filter) { + return new Map<string, Filter>(); + } + const filters = filter.split('&&'); + return splitFilterByOperator(filters); +} + +/** + * Parse a list of strings in filter format into a Map of Filters + * @param filters list of strings in format `['']` + * @returns a map of Filters + */ +function splitFilterByOperator(filters: string[]): Map<string, Filter> { + const mappedFilters = new Map<string, Filter>(); + filters + .map(filter => { + if (filter.includes(FilterOperator.EQUAL_IN)) { + const [parameter, value] = filter.split(FilterOperator.EQUAL_IN); + return { parameter, value, operator: FilterOperator.EQUAL_IN }; + } + if (filter.includes(FilterOperator.EQUAL)) { + const [parameter, value] = filter.split(FilterOperator.EQUAL); + return { parameter, value, operator: FilterOperator.EQUAL }; + } + if (filter.includes(FilterOperator.NOT_EQUAL)) { + const [parameter, value] = filter.split(FilterOperator.NOT_EQUAL); + return { parameter, value, operator: FilterOperator.NOT_EQUAL }; + } + if (filter.includes(FilterOperator.LIKE_REGEX)) { + const [parameter, value] = filter.split(FilterOperator.LIKE_REGEX); + return { parameter, value, operator: FilterOperator.LIKE_REGEX }; + } + throw new Error('Unsupported operator'); + }) + .forEach(item => mappedFilters.set(item.parameter, item)); + return mappedFilters; +} + +/** + * Transforms the map of filters back into a JSONPATH filter string + * @param filters + * @param forApi + * @returns a string containing all filter expressions or undefined if size of the Filter Map is 0 + */ +export function filterBuilder(filters: Map<string, Filter>, forApi=false): string | undefined { + if (filters.size === 0) { + return undefined; + } + const filtersString: string[] = []; + for (const [, filter] of filters.entries()) { + filtersString.push( + createFilter( + filter.parameter, + filter.operator, + filter.value.toString(), + forApi), + ); + } + return composeFilter(filtersString); +} diff --git a/src/app/helpers/form-validators.ts b/src/app/helpers/form-validators.ts new file mode 100644 index 0000000..6cd3acd --- /dev/null +++ b/src/app/helpers/form-validators.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 { AbstractControl, ValidationErrors } from '@angular/forms'; + +export class WhiteSpaceValidator { + static noWhiteSpace(control: AbstractControl): ValidationErrors | null { + if (control.value !== null) { + if ((control.value as string).indexOf(' ') >= 0) { + return { noWhiteSpace: true }; + } + return null; + } + return null; + } +} diff --git a/src/app/helpers/helpers.ts b/src/app/helpers/helpers.ts new file mode 100644 index 0000000..7c03dbd --- /dev/null +++ b/src/app/helpers/helpers.ts @@ -0,0 +1,76 @@ +/* + * 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 { FormArray, FormGroup } from '@angular/forms'; + +export function isNotUndefined<T>(val: T | undefined): val is T { + return val !== undefined; +} + +export function isNotNull<T>(val: T | null): val is T { + return val !== null; +} + +export function markAsDirtyAndValidate(formGroup: FormGroup): void { + Object.values(formGroup.controls).forEach(control => { + control.markAsDirty(); + control.updateValueAndValidity(); + }); +} + +export function isNullOrUndefined(val: any): boolean { + return val === null || val === undefined; +} + +export function isNotNullOrUndefined(val: any): boolean { + return val !== null && val !== undefined; +} + +export function isNullOrUndefinedOrEmptyString(val: any): boolean { + return val === null || val === undefined || val === ''; +} + +export function isEmptyArray(array: any[]):boolean { + return !array.length +} + +export function getRandomNumber(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +export function areFormControlsValid(form: FormGroup): boolean { + const formControls = Object.keys(form.controls) + .map(key => form.controls[key]) + .filter(control => !(control instanceof FormArray)); + return formControls.find(control => control.invalid && (control.dirty || control.touched)) === undefined; +} + +export function isString(value: any): boolean { + return typeof value === 'string' || value instanceof String; +} + +export function resetSelectDefaultValue(cssSelector: string): void { + setTimeout(() => { + const element = document.querySelector(cssSelector); + if (element) { + //@ts-ignore + document.querySelector(cssSelector)?.selectedIndex = -1; + } + }, 0); +} diff --git a/src/app/helpers/listing.ts b/src/app/helpers/listing.ts new file mode 100644 index 0000000..66d1ae0 --- /dev/null +++ b/src/app/helpers/listing.ts @@ -0,0 +1,90 @@ +/* + * 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 { Router } from '@angular/router'; +import { filterBuilder, FilterOperator } from 'src/app/helpers/filter-helpers'; + +/** + * A JSONPath filter. One or more `Filter`s can be passed to the `filter=` param of the alarm API. + */ +export interface Filter { + /** + * The field to compare against, i.e `$.type` + */ + parameter: string; + /** + * The JSONPath Filter Operators that are used for comparison (subset of [these](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-SQLJSON-FILTER-EX-TABLE)). + * For instance `==` and `like_regex`: + * ``` + * $.id == 1 + * $.type like_regex "type_(a|b)" + * ``` + */ + operator: FilterOperator; + /** + * The value or pattern that the field should have, i.e `"type_(a|b)"` + */ + value: string | number; +} + +/** + * This represents one filter expression for one column. + * Use `undefined` to signal removal of the filter. + * + * Note that parameter needs to be the complete param (i.e `$.column`). + * That is necessary because this will be the key when parsing the filter expression into a map. + */ +export type ColumnFilter = { + parameter: string; + filter: Filter | undefined; +}; + +export function changePage(router: Router, page: number): void { + router.navigate([], { queryParams: { page }, queryParamsHandling: 'merge' }); +} + +export function changePageSize(router: Router, pageSize: number): void { + router.navigate([], { + queryParams: { page: 1, pageSize }, + queryParamsHandling: 'merge', + }); +} + +export function changeFilter(router: Router, value: string | undefined): void { + router.navigate([], { + queryParams: { filter: value ? value.trim() : undefined, page: 1 }, + queryParamsHandling: 'merge', + }); +} + +/** + * Format a JSONPath filter expression into the format required by the angular router. + * Add or update this filter param in the router. + * @param router the angular router + * @param filters the filter expression in JSONPath format + * @param path path for new router link + * @param params other queryParams + */ +export function changeFiltersInRouter(router: Router, filters: Map<string, Filter>, params?: { [key: string]: any }, path?: any[]): void { + const composedFilter = filters.size > 0 ? filterBuilder(filters) : undefined; + router.navigate(path ?? [], { + queryParams: { filter: composedFilter, ...params, page: 1 }, + queryParamsHandling: 'merge', + }); +} diff --git a/src/app/helpers/sorting-helpers.ts b/src/app/helpers/sorting-helpers.ts new file mode 100644 index 0000000..94ec9ef --- /dev/null +++ b/src/app/helpers/sorting-helpers.ts @@ -0,0 +1,59 @@ +/* + * 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 { Sort, SortDirection } from '../components/shared/sort/sort.component'; +import { Router } from '@angular/router'; + +export function parseSortStringToSort(str: string): Sort { + return str.startsWith('-') + ? { parameter: str.split('-')[1], direction: SortDirection.DESC } + : { parameter: str, direction: SortDirection.ASC }; +} +export function applyNewSorting(router: Router, sort: Sort) { + if (sort.direction === SortDirection.NONE) { + router.navigate([], { + queryParams: { page: 1, sort: null }, + queryParamsHandling: 'merge', + }); + } else { + router.navigate([], { + queryParams: { page: 1, sort: sort.direction === SortDirection.ASC ? sort.parameter : '-' + sort.parameter }, + queryParamsHandling: 'merge', + }); + } +} +export function parseSortToSortString(sort: Sort): string { + if (sort.direction === SortDirection.ASC) { + return sort.parameter; + } + if (sort.direction === SortDirection.DESC) { + return '-' + sort.parameter; + } + return ''; +} + +export function parseSortToSortJsonString(sort: Sort): string { + if (sort.direction === SortDirection.ASC) { + return '$.' + sort.parameter; + } + if (sort.direction === SortDirection.DESC) { + return '-$.' + sort.parameter; + } + return ''; +} diff --git a/src/app/http-interceptors/auth.interceptor.ts b/src/app/http-interceptors/auth.interceptor.ts new file mode 100644 index 0000000..fc13e70 --- /dev/null +++ b/src/app/http-interceptors/auth.interceptor.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 { Injectable } from '@angular/core'; +import { OAuthStorage } from 'angular-oauth2-oidc'; +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; + +import { Observable, throwError } from 'rxjs'; +import { environment } from '../../environments/environment'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + constructor(private authStorage: OAuthStorage) {} + + public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { + const url = req.url.toLowerCase(); + if (!url.startsWith(environment.backendServerUrl) && !url.startsWith(environment.loggingUrl)) { + return next.handle(req); + } + + const accessToken = this.authStorage.getItem('access_token'); + const idToken = this.authStorage.getItem('id_token'); + if (accessToken === null || idToken === null) { + return throwError(new Error('Missing access or ID token')); + } + const headers = req.headers + .set('Authorization', `Bearer ${accessToken}`) + .set('X-Auth-Identity', `Bearer ${idToken}`); + req = req.clone({ headers }); + + return next.handle(req); + } +} diff --git a/src/app/http-interceptors/caching-interceptor.ts b/src/app/http-interceptors/caching-interceptor.ts new file mode 100644 index 0000000..df13376 --- /dev/null +++ b/src/app/http-interceptors/caching-interceptor.ts @@ -0,0 +1,94 @@ +/* + * 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 { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { startWith, tap } from 'rxjs/operators'; + +import { RequestCache } from '../services/cacheservice/request-cache.service'; +import { urlTileApi } from '../services/tileservice/tiles.service'; + +/** + * Interceptor that returns HttpResponses from cache and updates cache if outdated + * This applies to GET requests from specified urls (specified in this class) + */ +@Injectable() +// https://angular.io/guide/http#using-interceptors-for-caching +// https://github.com/angular/angular/blob/master/aio/content/examples/http/src/app/http-interceptors/caching-interceptor.ts +export class CachingInterceptor implements HttpInterceptor { + private cachedUrls: Array<string> = [ + urlTileApi, + window.location.origin + '/keycloak/auth/realms/ONAP/protocol/openid-connect/userinfo', + ]; + + // cache is the cache service that supports get() and put() + constructor(private cache: RequestCache) {} + + intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { + // continue if not cacheable. + if (!this.isCacheable(request)) { + return next.handle(request); + } + + const cachedResponse = this.cache.get(request); + + // when a custom 'x-refresh' header is set, refresh the cash + // (see https://github.com/angular/angular/blob/master/aio/content/examples/http/src/app/package-search/package-search.service.ts) + if (request.headers.get('x-refresh')) { + const results$ = this.getUpdatedResponse(request, next, this.cache); + return cachedResponse ? results$.pipe(startWith(cachedResponse)) : results$; + } + // cache-or-fetch + return cachedResponse ? of(cachedResponse) : this.getUpdatedResponse(request, next, this.cache); + } + + /** + * Checks if the request is cacheable. + * This is true if the request is a http GET and the url is defined in this method + * (-> urlTileApi) + */ + private isCacheable(req: HttpRequest<any>): boolean { + // Only GET requests are cacheable + return ( + req.method === 'GET' && + // Only the tile api is cacheable in this app + -1 < this.cachedUrls.indexOf(req.url) + ); + } + + /** + * Get server response observable by sending request to `next()`. + * Will add the response to the cache on the way out. + */ + private getUpdatedResponse( + req: HttpRequest<any>, + next: HttpHandler, + cache: RequestCache, + ): Observable<HttpEvent<any>> { + return next.handle(req.clone()).pipe( + tap(event => { + // There may be other events besides the response. + if (event instanceof HttpResponse) { + cache.put(req, event); // Update the cache. + } + }), + ); + } +} diff --git a/src/app/http-interceptors/http-error.interceptor.ts b/src/app/http-interceptors/http-error.interceptor.ts new file mode 100644 index 0000000..61d55e0 --- /dev/null +++ b/src/app/http-interceptors/http-error.interceptor.ts @@ -0,0 +1,142 @@ +/* + * 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 { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, throwError, TimeoutError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { AlertService } from '../modules/alerting'; +import { TranslateService } from '@ngx-translate/core'; +import { Problem } from '../../../openapi/output'; +import { Router } from '@angular/router'; +import { DEFAULT_TIMEOUT } from './timeout.interceptor'; + +/** + * This class adds global handling of http-request related errors + */ + +export enum RequestMethod { + DELETE = 'DELETE', + POST = 'POST', + GET = 'GET', +} + +interface ProblemDetail { + errorDetail: Problem; + requestId?: string; + urlTree: string[]; +} +@Injectable() +export class HttpErrorInterceptor implements HttpInterceptor { + errorDetail!: Problem; + constructor(private alertService: AlertService, private translateService: TranslateService, private router: Router) {} + + intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { + return next.handle(request).pipe( + catchError(rsp => { + const urlTree = this.router.url.split('/'); + this.errorDetail = this.createErrorDetail(rsp); + const requestId = request.headers.get('x-request-id') || undefined; + const detail: ProblemDetail = { + errorDetail: this.errorDetail, + requestId, + urlTree, + }; + + if (request.url.includes('onap_logging')) { + this.alertService.warn(this.translateService.instant('common.block.logging'), { + id: 'onap_logging', + }); + return throwError(this.errorDetail); + } + if (request.method === RequestMethod.POST && request.url.includes('keycloak')) { + this.alertService.error(this.translateService.instant('common.block.authorization'), { + id: 'keycloak', + }); + return throwError(this.errorDetail); + } + if (request.url.includes('preferences')) { + this.getPreferenceMessage(request, detail); + return throwError(this.errorDetail); + } + if (request.url.includes('actions')) { + this.getActionMessage(request, detail); + return throwError(this.errorDetail); + } + + switch (urlTree[1].split('?')[0]) { + case 'user-administration': + this.getUserAdministrationMessage(request, detail, urlTree); + break; + case 'dashboard': + this.getErrorMessage('dashboard', detail); + break; + case 'app-starter': + this.getErrorMessage('appStarter', detail); + break; + default: + this.getErrorMessage('defaultMessage', detail); + break; + } + return throwError(this.errorDetail); + }), + ); + } + + private getUserAdministrationMessage(request: HttpRequest<any>, detail: ProblemDetail, urlTree: string[]) { + if (request.method === RequestMethod.DELETE) { + return this.getErrorMessage('userAdministration.delete', detail); + } + if (urlTree.includes('create')) { + return this.getErrorMessage('userAdministration.create', detail); + } + if (urlTree.includes('edit')) { + return this.getErrorMessage('userAdministration.edit', detail); + } + } + + private getActionMessage(request: HttpRequest<any>, detail: ProblemDetail) { + if (request.method === RequestMethod.POST) { + return this.getErrorMessage('saveAction', detail); + } + this.getErrorMessage('loadAction', detail); + } + + private getPreferenceMessage(request: HttpRequest<any>, detail: ProblemDetail) { + if (request.method === RequestMethod.POST) { + return this.getErrorMessage('savePreferences', detail); + } + this.getErrorMessage('loadPreferences', detail); + } + + private createErrorDetail(response: any): Problem { + if (response instanceof TimeoutError) { + return { + detail: this.translateService.instant('common.block.timeout', { value: DEFAULT_TIMEOUT / 1000 }), + title: response.name, + status: 408, + }; + } + return response.error ? response.error : response; + } + + private getErrorMessage(type: string, detail: ProblemDetail) { + this.alertService.error(`${this.translateService.instant('common.block.' + type)}`, detail); + } +} diff --git a/src/app/http-interceptors/interceptors.ts b/src/app/http-interceptors/interceptors.ts new file mode 100644 index 0000000..5fd0a97 --- /dev/null +++ b/src/app/http-interceptors/interceptors.ts @@ -0,0 +1,42 @@ +/* + * 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 + */ + + +/* "Barrel" of Http Interceptors */ +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { CachingInterceptor } from './caching-interceptor'; +import { HttpErrorInterceptor } from './http-error.interceptor'; +import { MockInterceptor } from './mock.interceptor'; +import { LoadingIndicatorInterceptor } from 'src/app/http-interceptors/loading-indicator.interceptor'; +import { RequestidInterceptor } from './requestid.interceptor'; +import { TimeoutInterceptor } from './timeout.interceptor'; +import { LoggingInterceptor } from './logging.interceptor'; + +/** Http interceptor providers in outside-in order + * Gathers all the interceptor providers into an httpInterceptorProviders array + */ +// https://angular.io/guide/http#provide-the-interceptor +export const httpInterceptorProviders = [ + { provide: HTTP_INTERCEPTORS, useClass: MockInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: TimeoutInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: LoadingIndicatorInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: RequestidInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: HttpErrorInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }, +]; diff --git a/src/app/http-interceptors/loading-indicator.interceptor.ts b/src/app/http-interceptors/loading-indicator.interceptor.ts new file mode 100644 index 0000000..d48d04f --- /dev/null +++ b/src/app/http-interceptors/loading-indicator.interceptor.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 { Injectable } from '@angular/core'; +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { finalize } from 'rxjs/operators'; +import { LoadingIndicatorService } from 'src/app/services/loading-indicator.service'; + +@Injectable() +export class LoadingIndicatorInterceptor implements HttpInterceptor { + readonly excludedUrls = ['preferences', 'actions']; + + constructor(private readonly loadingIndicator: LoadingIndicatorService) {} + + intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { + if (this.excludedUrls.some(url => req.url.includes(url))) { + return next.handle(req); + } + + this.loadingIndicator.show(); + return next.handle(req).pipe(finalize(() => this.loadingIndicator.hide())); + + } +} diff --git a/src/app/http-interceptors/logging.interceptor.ts b/src/app/http-interceptors/logging.interceptor.ts new file mode 100644 index 0000000..8975868 --- /dev/null +++ b/src/app/http-interceptors/logging.interceptor.ts @@ -0,0 +1,64 @@ +/* + * 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 { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; +import { take, tap } from 'rxjs/operators'; +import { LoggingService } from '../services/logging.service'; + +@Injectable() +export class LoggingInterceptor implements HttpInterceptor { + constructor(private readonly loggingService: LoggingService) {} + + intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { + if (environment.loggingEnabled) { + if ( + request.url.toLowerCase().startsWith(environment.backendServerUrl) && + !request.url.toLowerCase().startsWith(environment.loggingUrl) + ) { + const requestMessage = `Portal-ui - request - X-Request-Id <${request.headers.get('x-request-id')}> <${ + request.method + }> <${request.url}>`; + this.loggingService + .writeLog(`'@timestamp': ${new Date().toISOString()}, message: ${requestMessage}`) + .pipe(take(1)) + .subscribe(); + + return next.handle(request).pipe( + tap((event: HttpEvent<any>) => { + if (event instanceof HttpResponse) { + const requestId = event.headers.get('x-request-id'); + const responseMessage = `Portal-ui - response - X-Request-Id <${requestId ?? 'Not set'}> <${ + event.status + }>`; + this.loggingService + .writeLog(`'@timestamp': ${new Date().toISOString()}, message: ${responseMessage}`) + .pipe(take(1)) + .subscribe(); + } + }), + ); + } + return next.handle(request); + } + return next.handle(request); + } +} diff --git a/src/app/http-interceptors/mock.interceptor.ts b/src/app/http-interceptors/mock.interceptor.ts new file mode 100644 index 0000000..6fcb122 --- /dev/null +++ b/src/app/http-interceptors/mock.interceptor.ts @@ -0,0 +1,50 @@ +/* + * 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 { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +/** + * This Interceptor reroutes URLs that are defined in the `matchers` array to [Wiremock](https://wiremock.org/) endpoints. + * This is useful, when the real api is not yet available. + */ +@Injectable() +export class MockInterceptor implements HttpInterceptor { + // list of all available RegExp URL matchers + // !! do not forget to remove matcher when API implementation is ready !! + private readonly matchers: RegExp[] = [ + // for example: + // /tiles/ + ]; + + intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { + let newRequest = request; + + if (this.matchers.some(matcher => request.url.match(matcher))) { + console.warn(`MockInterceptor is enabled for URL: '${request.url}'`); + // intentional usage of .replace instead of .replaceAll to replace only first instance + newRequest = request.clone({ + url: request.url.replace('/api/', '/mock-api/'), + }); + } + + return next.handle(newRequest); + } +} diff --git a/src/app/http-interceptors/requestid.interceptor.ts b/src/app/http-interceptors/requestid.interceptor.ts new file mode 100644 index 0000000..6c3c912 --- /dev/null +++ b/src/app/http-interceptors/requestid.interceptor.ts @@ -0,0 +1,37 @@ +/* + * 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 { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { v4 as uuid } from 'uuid'; + +@Injectable() +export class RequestidInterceptor implements HttpInterceptor { + intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { + // this is skipping everything that is not /api (like /keycloak) + if (!request.url.toLowerCase().startsWith(environment.backendServerUrl)) { + return next.handle(request); + } + + request = request.clone({ setHeaders: { 'X-Request-Id': uuid() } }); + return next.handle(request); + } +} diff --git a/src/app/http-interceptors/timeout.interceptor.ts b/src/app/http-interceptors/timeout.interceptor.ts new file mode 100644 index 0000000..e320b79 --- /dev/null +++ b/src/app/http-interceptors/timeout.interceptor.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 { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor +} from '@angular/common/http'; +import { Observable, throwError, TimeoutError } from 'rxjs'; +import { catchError, timeout } from 'rxjs/operators'; +import { AlertService } from '../modules/alerting'; +import { TranslateService } from '@ngx-translate/core'; + +//60 seconds +export const DEFAULT_TIMEOUT = 60000; + +@Injectable() +export class TimeoutInterceptor implements HttpInterceptor { + + constructor(private alertService: AlertService, private translateService: TranslateService) {} + intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>>{ + + return next.handle(req).pipe( + timeout(DEFAULT_TIMEOUT), + catchError(err => { + if (err instanceof TimeoutError) { + this.alertService.error(this.translateService.instant('common.messages.timeout')) + } + return throwError(err); + }) + ); + } +} diff --git a/src/app/model/dashboard.model.ts b/src/app/model/dashboard.model.ts new file mode 100644 index 0000000..2f501c3 --- /dev/null +++ b/src/app/model/dashboard.model.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 + */ + + +export enum DashboardApplications { + USER_LAST_ACTION_TILE = 'USER_LAST_ACTION_TILE', +} +export interface DashboardTileSettings { + type: DashboardApplications; + displayed: boolean; +} diff --git a/src/app/model/environment.model.ts b/src/app/model/environment.model.ts new file mode 100644 index 0000000..5bfb615 --- /dev/null +++ b/src/app/model/environment.model.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 + */ + + +export interface Environment { + customStyleEnabled: boolean; + backendServerUrl: string; + hostname: string; + keycloakEditProfile: string; + production: boolean; + keycloak: KeycloakEnvironment; + dateTimeFormat: string; + loggingUrl: string; + loggingEnabled: boolean + supportUrlLink: string + +} + +export interface KeycloakEnvironment { + issuer: string; + redirectUri: string; + clientId: string; + responseType: string; + scope: string; + requireHttps: boolean; + showDebugInformation: boolean; + disableAtHashCheck: boolean; + skipIssuerCheck: boolean; + strictDiscoveryDocumentValidation: boolean; +} diff --git a/src/app/model/tile.ts b/src/app/model/tile.ts new file mode 100644 index 0000000..c23482e --- /dev/null +++ b/src/app/model/tile.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 + */ + + +export interface TilesListResponse { + items: Tile[]; +} +export interface Tile { + id: number; + title: string; + imageUrl: string; + imageAltText: string; + description: string; + redirectUrl: string; + headers?: string; + groups: Group[]; + roles: Role[]; +} + +export enum Group { + ADMIN = 'ADMIN', + DEVELOPER = 'DEVELOPER', + OPERATOR = 'OPERATOR', +} + +export enum Role { + ONAP_OPERATOR = 'ONAP_OPERATOR', + ONAP_DESIGNER = 'ONAP_DESIGNER', + ONAP_ADMIN = 'ONAP_ADMIN', +} + + diff --git a/src/app/model/user-last-action.model.ts b/src/app/model/user-last-action.model.ts new file mode 100644 index 0000000..8941f18 --- /dev/null +++ b/src/app/model/user-last-action.model.ts @@ -0,0 +1,75 @@ +/* + * 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 enum ActionFilter { + ALL = 'ALL', + SEARCH = 'SEARCH', + ACTION = 'ACTION', +} + +export enum ActionType { + SEARCH = 'SEARCH', + VIEW = 'VIEW', + EDIT = 'EDIT', + DEPLOY = 'DEPLOY', + DELETE = 'DELETE', + CREATE = 'CREATE', + CLEAR = 'CLEAR', + ACK = 'ACK', + UNACK = 'UNACK', +} + +export enum ActionInterval { + LAST1H = '1H', + LAST4H = '4H', + LAST1D = '1D', + ALL = 'ALL', +} + +export enum EntityType { + USERADMINISTRATION = 'USERADMINISTRATION', +} + +export interface EntityUserHistoryActionModel { + userId: string; + userName: string; +} + +export interface CreateActionModel<T> { + type: ActionType; + entity: EntityType; + entityParams: T; +} + +export interface ActionRowModel<T> { + actionCreatedAt: string; + type: ActionType; + entity: EntityType; + entityParams: T; +} + +export interface ActionModel { + actionCreatedAt: string; + type: ActionType; + entity: EntityType; + entityParams: EntityTypeModel; +} + +export type EntityTypeModel = + | EntityUserHistoryActionModel diff --git a/src/app/model/user-preferences.model.ts b/src/app/model/user-preferences.model.ts new file mode 100644 index 0000000..61f9718 --- /dev/null +++ b/src/app/model/user-preferences.model.ts @@ -0,0 +1,87 @@ +/* + * 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 { DashboardApplications } from './dashboard.model'; +import { ActionFilter, ActionInterval } from './user-last-action.model'; + +export const STATE_KEYS = { + DASHBOARD: 'dashboard', + APPS: 'apps', + TILES: 'availableTiles', + USER_ACTIONS: 'lastUserAction', + FILTER_TYPE:'filterType', + INTERVAL: 'interval' +}; + + +export interface DashboardModel { + apps: DashboardAppsModel; +} + +export interface DashboardAppsModel { + availableTiles: DashboardTileSettings[]; + lastUserAction: LastUserActionSettings; +} + +export interface UserPreferencesModel { + dashboard: DashboardModel; +} + +export interface UpdateUserPreferenceModel { + dashboard?: { + apps?: { + availableTiles?: DashboardTileSettings[]; + lastUserAction?: LastUserActionSettings; + }; + }; +} + +export interface DashboardTileSettings { + type: DashboardApplications; + displayed: boolean; +} + +export interface LastUserActionSettings { + interval: ActionInterval; + filterType: ActionFilter; +} + +const availableDashboardApps: DashboardTileSettings[] = [ + { + type: DashboardApplications.USER_LAST_ACTION_TILE, + displayed: true, + }, +]; + +export const defaultLastUserActionSettings: LastUserActionSettings = { + interval: ActionInterval.LAST1H, + filterType: ActionFilter.ALL, +}; + +export const defaultUserSettings: UserPreferencesModel = { + dashboard: { + apps: { + availableTiles: availableDashboardApps, + lastUserAction: defaultLastUserActionSettings, + }, + }, +}; + +export const DASHBOARD_SECTION = 'dashboard'; diff --git a/src/app/model/validation-pattern.model.ts b/src/app/model/validation-pattern.model.ts new file mode 100644 index 0000000..d611d44 --- /dev/null +++ b/src/app/model/validation-pattern.model.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 + */ + + +export const VALIDATION_PATTERN = "[\\w,/!=§#@€:µ.+?' \\-\\u00C0-\\u017F]*"; +export const NON_WHITE_SPACE_PATTERN = new RegExp('\\S'); + + //Info from team Euler --> predefined regexp in SO service instance name is: +// public static final String VALID_INSTANCE_NAME_FORMAT = "^[a-zA-Z][a-zA-Z0-9._-]*$"; +// thanks to that we will avoid the error during model deployment +export const VALID_INSTANCE_NAME_FORMAT_PATTERN = "^[a-zA-Z][a-zA-Z0-9._-]*$"; 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 {} diff --git a/src/app/package.json b/src/app/package.json new file mode 100644 index 0000000..1706abd --- /dev/null +++ b/src/app/package.json @@ -0,0 +1,6 @@ +{
+ "name": "frontend",
+ "private": true,
+ "description": "This is a special package.json file that is not used by package managers. It is however used to tell the tools and bundlers whether the code under this directory is free of code with non-local side-effect. Any code that does have non-local side-effects can't be well optimized (tree-shaken) and will result in unnecessary increased payload size. It should be safe to set this option to 'false' for new applications, but existing code bases could be broken when built with the production config if the application code does contain non-local side-effects that the application depends on.",
+ "sideEffects": false
+}
diff --git a/src/app/pipes/colon.pipe.ts b/src/app/pipes/colon.pipe.ts new file mode 100644 index 0000000..fe0b50b --- /dev/null +++ b/src/app/pipes/colon.pipe.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 { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'colon', +}) +export class ColonPipe implements PipeTransform { + transform(value: string): string { + return `${value}: `; + } +} diff --git a/src/app/pipes/guard-type.pipe.ts b/src/app/pipes/guard-type.pipe.ts new file mode 100644 index 0000000..0c2b847 --- /dev/null +++ b/src/app/pipes/guard-type.pipe.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 { Pipe, PipeTransform } from '@angular/core'; + +export type TypeGuard<A, B extends A> = (a: A) => a is B; + +// https://github.com/angular/angular/issues/34522#issuecomment-762973301 +@Pipe({ + name: 'guardType', +}) +export class GuardTypePipe implements PipeTransform { + transform<A, B extends A>(value: A, typeGuard: TypeGuard<A, B>): B | undefined { + return typeGuard(value) ? value : undefined; + } +} diff --git a/src/app/pipes/has-permission.pipe.ts b/src/app/pipes/has-permission.pipe.ts new file mode 100644 index 0000000..a22b034 --- /dev/null +++ b/src/app/pipes/has-permission.pipe.ts @@ -0,0 +1,58 @@ +/* + * 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, Pipe, PipeTransform } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { ACL_CONFIG, AclConfig } from '../modules/auth/injection-tokens'; +import { AuthService } from '../services/auth.service'; +import { Observable } from 'rxjs'; +import { map, take } from 'rxjs/operators'; + +/* + hasPermission pipe returns Promise<boolean | void> value based on the authentication file (acl.json) and user's role. + Using the pipe we are able to show/hide the elements in the app for specific user role. + Input parameter is the string from the acl.json + USAGE: *ngIf="'dashboard.tile.KPI_DASHBOARD_TILE' | hasPermission | async" +*/ +@Pipe({ + name: 'hasPermission', +}) +export class HasPermissionPipe implements PipeTransform { + constructor( + readonly httpClient: HttpClient, + readonly authService: AuthService, + @Inject(ACL_CONFIG) readonly acl: AclConfig, + ) {} + + transform(value: string): Observable<boolean | void> { + return this.authService + .loadCachedUserProfile() + .pipe( + take(1), + map((userProfile) => { + const intersectionOfRoles = Object.keys(this.acl).filter(role => userProfile?.roles.includes(role)); + for (const role of intersectionOfRoles) { + if (this.acl[role].includes(value)) { + return true; + } + } + return false; + })) + } +} diff --git a/src/app/pipes/in.pipe.ts b/src/app/pipes/in.pipe.ts new file mode 100644 index 0000000..df6e8cc --- /dev/null +++ b/src/app/pipes/in.pipe.ts @@ -0,0 +1,36 @@ +/* + * 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 { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'in', +}) +export class InPipe implements PipeTransform { + transform(value: string, array: string[] | Set<any>): boolean { + if (array instanceof Array) { + return array.includes(value); + } + if (array instanceof Set) { + return array.has(value); + } else { + throw new Error('unsupported type'); + } + } +} diff --git a/src/app/pipes/is-today.pipe.ts b/src/app/pipes/is-today.pipe.ts new file mode 100644 index 0000000..1d2f17a --- /dev/null +++ b/src/app/pipes/is-today.pipe.ts @@ -0,0 +1,37 @@ +/* + * 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 { Pipe, PipeTransform } from '@angular/core'; + +// return true if given parameter is today else return false + +@Pipe({ + name: 'isToday', +}) +export class IsTodayPipe implements PipeTransform { + transform(value: string | Date | number): boolean { + const date = new Date(value); + const today = new Date(); + return ( + date.getDate() == today.getDate() && + date.getMonth() == today.getMonth() && + date.getFullYear() == today.getFullYear() + ); + } +} diff --git a/src/app/pipes/map.pipe.ts b/src/app/pipes/map.pipe.ts new file mode 100644 index 0000000..e2c88a9 --- /dev/null +++ b/src/app/pipes/map.pipe.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 { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'map', + pure: false +}) +/* + MapPipe allows us to run a function in a template + for example, you need to filter out some elements from the array before displaying these elements. You can call a function for getting filtered array + directly from template, but this function will be triggered everytime when user interacts with page. + MapPipe allows you to call a function through this pipe, so function will be called only when necessary. + Usage: + we have function in .ts file that's called filterZeroValues + in template we can use this function: + *ngFor="let item in elements | map : filterZeroValues" + where the first parameter of map pipe is function to be called, and other parameters will be passed as arguments to this function + Important note: as you can see from implementation, elements array will be passed to your function as a first argument +*/ +export class MapPipe implements PipeTransform { + + transform<T, R>( + thisArg: T, + project: (t:T, ...others: any[]) => R, + ...args: any[] + ): R { + return project(thisArg, ...args); + } + +} diff --git a/src/app/pipes/translate-mock.pipe.ts b/src/app/pipes/translate-mock.pipe.ts new file mode 100644 index 0000000..80c68a7 --- /dev/null +++ b/src/app/pipes/translate-mock.pipe.ts @@ -0,0 +1,42 @@ +/* + * 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 { Pipe, PipeTransform } from "@angular/core"; +/** + * This class can be used to mock the `translate` pipe in jasmine test cases. + * + * Usage: + * ``` typescript + * TestBed.configureTestingModule({ + declarations: [TranslatePipeMock,...], + providers: [{ provide: TranslatePipe, useClass: TranslatePipeMock },...] +}).compileComponents(); +* ``` +*/ +// Courtesy of: https://github.com/ngx-translate/core/issues/636#issuecomment-451137902 +@Pipe({ + name: 'translate' +}) +export class TranslatePipeMock implements PipeTransform { + public name = 'translate'; + + public transform(query: string): any { + return query; + } +} diff --git a/src/app/router.strategy.ts b/src/app/router.strategy.ts new file mode 100644 index 0000000..c80682e --- /dev/null +++ b/src/app/router.strategy.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 {ActivatedRouteSnapshot, DetachedRouteHandle, BaseRouteReuseStrategy} from '@angular/router'; + +export class AppRouteReuseStrategy implements BaseRouteReuseStrategy { + public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { + if(future.data.reuseComponent) { + return false + } + return (future.routeConfig === curr.routeConfig); + } + + retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { + return true; + } + + shouldAttach(route: ActivatedRouteSnapshot): boolean { + return false; + } + + shouldDetach(route: ActivatedRouteSnapshot): boolean { + return false; + } + + store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void; + store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void; + store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle | null): void { + //this is intentional + } + +} diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts new file mode 100644 index 0000000..8988196 --- /dev/null +++ b/src/app/services/auth.service.ts @@ -0,0 +1,94 @@ +/* + * 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 { OAuthService, UserInfo } from 'angular-oauth2-oidc'; +import { catchError, filter, map } from 'rxjs/operators'; +import { NavigationStart, Router } from '@angular/router'; +import { AlertService } from '../modules/alerting'; +import { TranslateService } from '@ngx-translate/core'; +import { BehaviorSubject, EMPTY, Observable, of } from 'rxjs'; + +/** + * Provides check for roles and token + */ +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + userProfile$ = new BehaviorSubject<UserInfo | undefined>(undefined); + constructor(private readonly oauthService: OAuthService, private router: Router, private alertService: AlertService, private translateService: TranslateService) { + //renew cache every page reload + router.events.pipe( + filter((e): e is NavigationStart => e instanceof NavigationStart) + ).subscribe(() => { + try { + return this.loadUserProfile().then(userInfo => this.userProfile$.next(userInfo)).catch(e => {throw e}) + }catch (e) { + this.alertService.error(this.translateService.instant('common.messages.keycloakAccessTokenNotValid'), {id: "keycloak", keepAfterRouteChange: true}) + return Promise.resolve(null); + } + }); + } + + userProfileCache: UserInfo | undefined = undefined; + + /** + * Convenience method of `hasValidAccessToken()` and `hasSufficientRoles()` + * Asynchronous because the needed UserInfo is fetched from Keycloak + */ + hasPermissions(): Observable<boolean> { + if (this.hasValidAccessToken()) { + return this.hasSufficientRoles(); + } + return of(false); + } + + /** + * This answers: 'What if the user has an account, but without any permissions?' + * Asynchronous because the needed UserInfo is fetched from Keycloak + */ + hasSufficientRoles(): Observable<boolean> { + return this.loadCachedUserProfile().pipe(map(info => info?.roles.join(',') !== 'offline_access')); + } + + loadCachedUserProfile(): Observable<UserInfo | undefined> { + return this.userProfile$.pipe( + filter(userProfile => userProfile !== undefined), + catchError(err => { + console.error(err); + return EMPTY + })); + } + + /** + * Wrapper for `hasValidAccessToken` from OAuthService + */ + hasValidAccessToken(): boolean { + return this.oauthService.hasValidAccessToken(); + } + /* + * Private method = should not be used outside of this class, because it triggers additional request for userprofile + * */ + private loadUserProfile():Promise<UserInfo> { + // in version 12.2 loadUserProfile() Promise returns data of type object instead of UserInfo + //@ts-ignore + return this.oauthService.loadUserProfile().then(userInfo => userInfo.info as UserInfo) + } +} diff --git a/src/app/services/authconfig.service.ts b/src/app/services/authconfig.service.ts new file mode 100644 index 0000000..5ced9a1 --- /dev/null +++ b/src/app/services/authconfig.service.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 { Injectable } from '@angular/core'; +import { AuthConfig, NullValidationHandler, OAuthService } from 'angular-oauth2-oidc'; + +@Injectable() +export class AuthConfigService { + constructor(private readonly oauthService: OAuthService, private readonly authConfig: AuthConfig) {} + + async initAuth(): Promise<any> { + return new Promise<void>((resolveFn, rejectFn) => { + // setup oauthService + this.oauthService.configure(this.authConfig); + this.oauthService.tokenValidationHandler = new NullValidationHandler(); + + this.oauthService.loadDiscoveryDocumentAndLogin().then(isLoggedIn => { + if (isLoggedIn) { + this.oauthService.setupAutomaticSilentRefresh(); + resolveFn(); + } else { + this.oauthService.initImplicitFlow(); + rejectFn(); + } + }).catch(() => { + //@ts-ignore + window.location.href = './keycloak-error.html' + }); + }); + } +} diff --git a/src/app/services/cacheservice/request-cache.service.spec.ts b/src/app/services/cacheservice/request-cache.service.spec.ts new file mode 100644 index 0000000..c9f931e --- /dev/null +++ b/src/app/services/cacheservice/request-cache.service.spec.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 { TestBed } from '@angular/core/testing'; + +import { RequestCacheService } from './request-cache.service'; + +describe('RequestCacheService', () => { + let service: RequestCacheService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(RequestCacheService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/cacheservice/request-cache.service.ts b/src/app/services/cacheservice/request-cache.service.ts new file mode 100644 index 0000000..3d2047f --- /dev/null +++ b/src/app/services/cacheservice/request-cache.service.ts @@ -0,0 +1,89 @@ +/* + * 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 { HttpRequest, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +const maxAge = 60000; + +@Injectable({ + providedIn: 'root', +}) +// https://github.com/angular/angular/blob/master/aio/content/examples/http/src/app/request-cache.service.ts +export class RequestCacheService implements RequestCache { + // The cache is a Map of (most importantly but not exclusively) HttpResponses + cache = new Map<string, RequestCacheEntry>(); + + /** + * Get an http request from cache + * @param request the http request that should be retrieved from cache + */ + get(request: HttpRequest<any>): HttpResponse<any> | undefined { + const requestUrl = request.urlWithParams; + const cachedResponse = this.cache.get(requestUrl); + + if (!cachedResponse) { + return undefined; + } + + const isExpired = cachedResponse.lastRead < Date.now() - maxAge; + + + return isExpired ? undefined : cachedResponse.response; + } + + /** + * Put a http response for a given request url (taken from the request object) in the cache + * @param request the http request that should be associated with the http response + * @param response the http response that should be stored in cache + */ + put(request: HttpRequest<any>, response: HttpResponse<any>): void { + const requestUrl = request.urlWithParams; + + // Map a request url to an object + const newEntry = { requestUrl, response, lastRead: Date.now() }; + this.cache.set(requestUrl, newEntry); + + // Remove expired entries from the cache + const expired = Date.now() - maxAge; + this.cache.forEach(entry => { + if (entry.lastRead < expired) { + this.cache.delete(entry.requestUrl); + } + }); + } +} + +/** + * Service that manages the cache. + * `get()` HttpResponses from cache and `put()` responses into the cache + */ +export abstract class RequestCache { + abstract get(request: HttpRequest<any>): HttpResponse<any> | undefined; + abstract put(request: HttpRequest<any>, response: HttpResponse<any>): void; +} + +/** + * Wrapper Object that stores the HttpResponse together with the `requestUrl` of the request and the `lastRead` time it was cached + */ +export interface RequestCacheEntry { + requestUrl: string; + response: HttpResponse<any>; + lastRead: number; +} diff --git a/src/app/services/fullscreen.service.ts b/src/app/services/fullscreen.service.ts new file mode 100644 index 0000000..91ceec9 --- /dev/null +++ b/src/app/services/fullscreen.service.ts @@ -0,0 +1,58 @@ +/* + * 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'; + +@Injectable({ + providedIn: 'root', +}) +export class FullscreenService { + private doc = <FullScreenDocument>document; + + enter() { + const el = this.doc.documentElement; + if (el.requestFullscreen) el.requestFullscreen(); + else if (el.msRequestFullscreen) el.msRequestFullscreen(); + else if (el.mozRequestFullScreen) el.mozRequestFullScreen(); + else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen(); + } + + leave() { + if (this.doc.exitFullscreen) this.doc.exitFullscreen(); + else if (this.doc.msExitFullscreen) this.doc.msExitFullscreen(); + else if (this.doc.mozCancelFullScreen) this.doc.mozCancelFullScreen(); + else if (this.doc.webkitExitFullscreen) this.doc.webkitExitFullscreen(); + } +} + +interface FullScreenDocument extends HTMLDocument { + documentElement: FullScreenDocumentElement; + mozFullScreenElement?: Element; + msFullscreenElement?: Element; + webkitFullscreenElement?: Element; + msExitFullscreen?: () => void; + mozCancelFullScreen?: () => void; + webkitExitFullscreen?: () => void; +} + +interface FullScreenDocumentElement extends HTMLElement { + msRequestFullscreen?: () => void; + mozRequestFullScreen?: () => void; + webkitRequestFullscreen?: () => void; +} diff --git a/src/app/services/history.service.ts b/src/app/services/history.service.ts new file mode 100644 index 0000000..2924f39 --- /dev/null +++ b/src/app/services/history.service.ts @@ -0,0 +1,67 @@ +/* + * 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 { + ActionType, + CreateActionModel, + EntityType, + EntityUserHistoryActionModel, +} from '../model/user-last-action.model'; +import { ActionsListResponse, ActionsResponse, ActionsService } from '../../../openapi/output'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class HistoryService { + constructor(private readonly actionService: ActionsService, private readonly oauthService: OAuthService) {} + + public getUserActions(interval: number | undefined): Observable<ActionsListResponse> { + const userId = Object(this.oauthService.getIdentityClaims()).sub; + return this.actionService.getActions(userId, 1, 1000, interval); + } + + public createUserHistoryAction(action: CreateActionModel<EntityUserHistoryActionModel>): Observable<ActionsResponse> { + let mappedAction = { + type: action.type, + entity: action.entity, + entityParams: { + userName: action.entityParams.userName, + userId: action.entityParams.userId, + }, + }; + return this.createAction(mappedAction); + } + + private createAction(action: { + type: ActionType; + entity: EntityType; + entityParams: { [key: string]: string | undefined}; + }): Observable<ActionsResponse> { + const userId = Object(this.oauthService.getIdentityClaims()).sub; + const actionCreatedAt = new Date().toISOString(); + return this.actionService.createAction(userId, undefined, { + userId, + actionCreatedAt, + action, + }); + } +} diff --git a/src/app/services/loading-indicator.service.ts b/src/app/services/loading-indicator.service.ts new file mode 100644 index 0000000..727edd4 --- /dev/null +++ b/src/app/services/loading-indicator.service.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 { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +@Injectable() +export class LoadingIndicatorService { + private isVisible$ = new BehaviorSubject<boolean>(false); + private timeDelay = 500; + private counter = 0; + + show(): void { + this.counter++; + if (this.counter > 0) { + setTimeout(() => this.isVisible$.next(true), 0); + } + } + + hide(): void { + this.counter--; + if (this.counter === 0) { + setTimeout(() => this.isVisible$.next(false), 0); + } + } + + isVisible(): Observable<boolean> { + return this.isVisible$.pipe(debounceTime(this.timeDelay)); + } +} diff --git a/src/app/services/logging.service.ts b/src/app/services/logging.service.ts new file mode 100644 index 0000000..5250fda --- /dev/null +++ b/src/app/services/logging.service.ts @@ -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 + */ + + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '../../environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class LoggingService { + constructor(private readonly httpClient: HttpClient) {} + + writeLog(message: string): Observable<string> { + return this.httpClient.post(environment.loggingUrl, message, { responseType: 'text' }); + } +} diff --git a/src/app/services/shortcut.service.ts b/src/app/services/shortcut.service.ts new file mode 100644 index 0000000..750ab5b --- /dev/null +++ b/src/app/services/shortcut.service.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 { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +export enum KeyboardShortcuts { + SHORTCUT_0 = '0', + SHORTCUT_1 = '1', + SHORTCUT_2 = '2', + SHORTCUT_4 = '4', + SHORTCUT_6 = '6', +} + +@Injectable({ + providedIn: 'root' +}) + +export class ShortcutService { + + private shortcuts = new Map<KeyboardShortcuts, string>([ + [KeyboardShortcuts.SHORTCUT_0, this.translateService.instant('layout.header.shortcuts.details')], + [KeyboardShortcuts.SHORTCUT_1, this.translateService.instant('layout.header.shortcuts.home')], + [KeyboardShortcuts.SHORTCUT_2, this.translateService.instant('layout.header.shortcuts.main')], + [KeyboardShortcuts.SHORTCUT_4, this.translateService.instant('layout.header.shortcuts.search')], + [KeyboardShortcuts.SHORTCUT_6, this.translateService.instant('layout.header.shortcuts.menu')], + ]); + + constructor(private translateService: TranslateService) { } + + public getShortcuts(): Map<KeyboardShortcuts,string> { + return this.shortcuts; + } +} diff --git a/src/app/services/tileservice/tiles.service.spec.ts b/src/app/services/tileservice/tiles.service.spec.ts new file mode 100644 index 0000000..f7f4369 --- /dev/null +++ b/src/app/services/tileservice/tiles.service.spec.ts @@ -0,0 +1,488 @@ +/* + * 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 { TestBed, waitForAsync } from '@angular/core/testing'; +import { Tile } from '../../model/tile'; +import { TilesService } from './tiles.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { environment } from '../../../environments/environment'; +import { OAuthLogger, OAuthService, UrlHelperService } from 'angular-oauth2-oidc'; + +// https://dev.to/coly010/unit-testing-angular-services-1anm + +/** + * describe sets up the Test Suite for the TileService + */ +describe('TilesService', () => { + /** + * let service declares a Test Suite-scoped variable where we will store a reference to our service + */ + let service: TilesService; + let mockTile: Tile; + let httpmock: HttpTestingController; + let errmsg: string; + + const backendServerUrlTest = environment.backendServerUrl + '/tiles'; + /** + * 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. Finally it is injecting the TilesService + * and placing a reference to it in the service variable defined earlier. + */ + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [TilesService, OAuthService, UrlHelperService, OAuthLogger], + }); + service = TestBed.inject(TilesService); + httpmock = TestBed.inject(HttpTestingController); + mockTile = { + id: 1, + title: 'NewTile1', + description: 'New Tile for frontend test', + imageUrl: 'https://www.onap.org/wp-content/uploads/sites/20/2017/02/logo_onap_2017.png', + imageAltText: 'Onap Image', + redirectUrl: 'www.onap.org', + headers: 'This is a header', + groups: [], + roles: [], + }; + // responseTile = { + // id: 2, + // title: 'NewTile1', + // description: 'New Tile for frontend test', + // imageUrl: 'https://www.onap.org/wp-content/uploads/sites/20/2017/02/logo_onap_2017.png', + // imageAltText: 'Onap Image', + // redirectUrl: 'www.onap.org', + // headers: 'This is a header', + // groups: [], + // roles: [], + // }; + }); + + /** + * After every test, assert that there are no more pending requests. + */ + afterEach(() => { + httpmock.verify(); + }); + + /** + * the it() function creates a new test with the title 'should be created' + * This test is expecting the service varibale to truthy, in otherwords, + * it should have been instantiated correctly by the Angular TestBed. + */ + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + /** + * TileService method tests begin + * Testing getTiles + */ + describe('#getTiles', () => { + // let expectedTiles: Tile[]; + + beforeEach(() => { + // expectedTiles = [mockTile, responseTile]; + }); + + /** + * testing method getTiles() to get all existing tiles + */ + /* + it('should return expected tiles (called once)', (done: DoneFn) => { + service.getTiles().subscribe(response => { + expect(response).toEqual(expectedTiles, 'should return expected tiles'); + // done() to be called after asynchronous calls (https://angular.io/guide/testing-services) + done(); + }); + + // TileService should have made one request to GET tiles from expected URL + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('GET'); + + // Respond with the expected mock tiles + req.flush(expectedTiles); + }); +*/ + /** + * TODO: Maybe it makes sense to inform the user that no tiles are displayed + * testing method getTiles() in case there are no tiles in the database + */ + /* + it('should be OK returning no tiles', (done: DoneFn) => { + service.getTiles().subscribe(response => { + expect(response.length).toEqual(0, 'should have empty tiles array'); + done(); + }); + + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('GET'); + + req.flush([]); // Respond with no tile + }); +*/ + /** + * testing method getTiles() in case the backend responds with 404 Not Found + * This service reports the error but finds a way to let the app keep going. + */ + + /* + it('should handle 404 error', (done: DoneFn) => { + errmsg = '404 error'; + + service.getTiles().subscribe( + response => fail('should fail with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(404); + expect(err.error).toEqual(errmsg); + }, + ); + + // Make an HTTP Get Request + // service.getTiles().then( + // response => fail('should have failed with the 404 error'), + // (err: HttpErrorResponse) => { + // expect(err.status).toEqual(404); + // expect(err.error).toEqual(errmsg); + // } + // ); + + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('GET'); + + // respond with a 404 and the error message in the body --> TODO Frontend GUI must react correctly + req.flush(errmsg, { status: 404, statusText: 'Not Found' }); + }); + */ + + /** + * testing getTiles() when method is called multiple times + * TODO: expect cached results + */ + /* + it('should return expected tiles (called multiple times)', () => { + service.getTiles().subscribe(); + service.getTiles().subscribe(); + service.getTiles().subscribe(response => { + expect(response).toEqual(expectedTiles, 'should return expected tiles'); + }); + + const req = httpmock.match(backendServerUrlTest); + expect(req.length).toEqual(3, 'calls to getTiles()'); + + // Respond to each request with different mock tile results + req[0].flush([]); + req[1].flush([mockTile]); + req[2].flush(expectedTiles); + });*/ + }); + + /** + * Tests for getTileByID() + */ + describe('#getTileByID', () => { + /** + * testing method getTilesById() to return the specific tile with right id + */ + it('should return expected tile by id', () => { + service.getTileById(mockTile.id).then(response => { + expect(response).toEqual(mockTile, 'should return expected tile'); + }); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('GET'); + + // Respond with the mock tiles + req.flush(mockTile); + }); + + /** + * testing method getTileByID() in case the backend responds with 404 Not Found and the tile does not exist + */ + it( + 'getTileById(): should handle 404 error', + waitForAsync(() => { + errmsg = '404 error'; + // Make an HTTP Get Request + service.getTileById(mockTile.id).then( + () => fail('should have failed with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(404); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('GET'); + + req.flush(errmsg, { status: 404, statusText: 'Not Found' }); + }), + ); + }); + /** + * Tests for update an existing tile + */ + describe('#updateTiles', () => { + /** + * testing method updateTiles() + */ + it('should update a tile and return it', () => { + mockTile.title = 'Update title'; + + service.updateTiles(mockTile).then(response => { + expect(response.title).toEqual('Update title', 'should return tile'); + }); + // TileService should have made one request to PUT + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('PUT'); + expect(req.request.body).toEqual(mockTile); + + req.flush(mockTile); + }); + + /** + * testing method updateTiles() in case the backend responds with 404 Not Found and the tile does not exist + */ + it( + 'updateTiles(): should handle 404 error', + waitForAsync(() => { + errmsg = '404 error'; + // Make an HTTP Get Request + service.updateTiles(mockTile).then( + () => fail('should have failed with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(404); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('PUT'); + + req.flush(errmsg, { status: 404, statusText: 'Not Found' }); + }), + ); + + /** + * testing method updateTiles() in case the backend responds with 401 Unauthorized + */ + it( + 'updateTiles(): should handle 401 error', + waitForAsync(() => { + errmsg = '401 error'; + // Make an HTTP Get Request + service.updateTiles(mockTile).then( + () => fail('should have failed with the 401 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(401); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('PUT'); + + req.flush(errmsg, { status: 401, statusText: 'Not Found' }); + }), + ); + + /** + * testing method updateTiles() in case the backend responds with 403 Forbidden + */ + it( + 'updateTiles(): should handle 403 error', + waitForAsync(() => { + errmsg = '403 error'; + // Make an HTTP Get Request + service.updateTiles(mockTile).then( + () => fail('should have failed with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(403); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('PUT'); + + req.flush(errmsg, { status: 403, statusText: 'Not Found' }); + }), + ); + }); + /* + * Test save a new tile + */ + describe('#saveTiles', () => { + /* + * testing saveTiles() to save a new tile + */ + it( + 'should save a tile correctly (mocked http post request)', + waitForAsync(() => { + service.saveTiles(mockTile).then(response => { + expect(response.id).toBe(1); + expect(response.title).toBe('NewTile1'); + expect(response.redirectUrl).toBe('www.onap.org'); + expect(response.imageAltText).toBe('Onap Image'); + expect(response.imageUrl).toBe('https://www.onap.org/wp-content/uploads/sites/20/2017/02/logo_onap_2017.png'); + expect(response.description).toBe('New Tile for frontend test'); + expect(response.headers).toBe('This is a header'); + }); + /* + * Checking that there ist just one request and check the type of request + * 'flush'/ respond with mock data, run then-block in line 64 and check the except commands + */ + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('POST'); + req.flush(mockTile); + }), + ); + /** + * testing method saveTiles() in case the backend answers with an 401 responds + */ + it( + 'saveTiles(): should handle 401 error', + waitForAsync(() => { + errmsg = '401 error'; + // Make an HTTP Get Request + service.saveTiles(mockTile).then( + () => fail('should have failed with the 401 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(401); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('POST'); + + req.flush(errmsg, { status: 401, statusText: 'Not Found' }); + }), + ); + + it( + 'saveTiles(): should handle 403 error', + waitForAsync(() => { + errmsg = '403 error'; + // Make an HTTP Get Request + service.saveTiles(mockTile).then( + () => fail('should have failed with the 401 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(403); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest); + expect(req.request.method).toEqual('POST'); + + req.flush(errmsg, { status: 403, statusText: 'Forbidden' }); + }), + ); + }); + /** + * testing delete a tile + */ + describe('#deleteTiles', () => { + /** + * testing method deleteTile() + */ + it( + 'should delete a tile correctly (mocked http delete request)', + waitForAsync(() => { + service.deleteTile(mockTile).then(response => { + expect(response).toBeDefined(); + }); + const req = httpmock.expectOne(environment.backendServerUrl + '/tiles/' + mockTile.id); + expect(req.request.method).toEqual('DELETE'); + req.flush({}); + }), + ); + + /** + * testing method deleteTiles() in case the backend responds with 404 Not Found and the tile does not exist + */ + it( + 'deleteTiles(): should handle 404 error', + waitForAsync(() => { + errmsg = '404 error'; + // Make an HTTP Get Request + service.deleteTile(mockTile).then( + () => fail('should have failed with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(404); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('DELETE'); + + req.flush(errmsg, { status: 404, statusText: 'Not Found' }); + }), + ); + + /** + * testing method deleteTiles() in case the backend responds with 401 Unauthorized + */ + it( + 'deleteTiles(): should handle 401 error', + waitForAsync(() => { + errmsg = '401 error'; + // Make an HTTP Get Request + service.deleteTile(mockTile).then( + () => fail('should have failed with the 401 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(401); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('DELETE'); + + req.flush(errmsg, { status: 401, statusText: 'Unauthorized' }); + }), + ); + + /** + * testing method deleteTiles() in case the backend responds with 403 Forbidden + */ + it( + 'deleteTiles(): should handle 403 error', + waitForAsync(() => { + errmsg = '403 error'; + // Make an HTTP Get Request + service.deleteTile(mockTile).then( + () => fail('should have failed with the 404 error'), + (err: HttpErrorResponse) => { + expect(err.status).toEqual(403); + expect(err.error).toEqual(errmsg); + }, + ); + + const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id); + expect(req.request.method).toEqual('DELETE'); + + req.flush(errmsg, { status: 403, statusText: 'Forbidden' }); + }), + ); + }); +}); diff --git a/src/app/services/tileservice/tiles.service.ts b/src/app/services/tileservice/tiles.service.ts new file mode 100644 index 0000000..167e42a --- /dev/null +++ b/src/app/services/tileservice/tiles.service.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 { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; +import { Tile, TilesListResponse } from '../../model/tile'; +import { map } from 'rxjs/operators'; + +export const urlTileApi = environment.backendServerUrl + '/tiles'; + +@Injectable({ + providedIn: 'root', +}) +// Tutorial on the http client: https://angular.io/tutorial/toh-pt6#get-heroes-with-httpclient +export class TilesService { + constructor(private httpClient: HttpClient) {} + /** + * GET tiles from the server + */ + getTiles(refresh = false): Observable<Tile[]> { + if (refresh) { + const headers = new HttpHeaders({ 'x-refresh': 'true' }); + return this.httpClient + .get<TilesListResponse>(urlTileApi, { headers }) + .pipe(map(tilesListResponse => tilesListResponse.items)); + } + + return this.httpClient.get<TilesListResponse>(urlTileApi).pipe(map(tilesListResponse => tilesListResponse.items)); + } + + /** + * GET tile by id + * @param id to get specific tile + */ + getTileById(id: number): Promise<Tile> { + return this.httpClient.get<Tile>(urlTileApi + '/' + id).toPromise(); + } + + /** + * POST: add a new tile to the database + * @param tile + * @returns the new saved tile + */ + saveTiles(tile: Tile): Promise<Tile> { + const options = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }), + }; + return this.httpClient.post<Tile>(urlTileApi, tile, options).toPromise(); + } + + /** + * PUT: update the tile on the server + * @returns the updated hero + * @param tile + */ + updateTiles(tile: Tile): Promise<Tile> { + const options = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }), + }; + return this.httpClient.put<Tile>(urlTileApi + '/' + tile.id, tile, options).toPromise(); + } + + /** + * DELETE: delete the tile from the server + * @param tile to delete + */ + deleteTile(tile: Tile): Promise<void> { + return this.httpClient.delete<void>(urlTileApi + '/' + tile.id).toPromise(); + } +} diff --git a/src/app/services/unsubscribe/unsubscribe.service.ts b/src/app/services/unsubscribe/unsubscribe.service.ts new file mode 100644 index 0000000..b27f6d8 --- /dev/null +++ b/src/app/services/unsubscribe/unsubscribe.service.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 { Injectable, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Injectable() +export class UnsubscribeService implements OnDestroy { + private readonly sub$ = new Subject<void>(); + public readonly unsubscribe$ = this.sub$.asObservable(); + + ngOnDestroy(): void { + this.sub$.next(); + this.sub$.complete(); + } +} diff --git a/src/app/services/user-settings.service.ts b/src/app/services/user-settings.service.ts new file mode 100644 index 0000000..cbaa992 --- /dev/null +++ b/src/app/services/user-settings.service.ts @@ -0,0 +1,111 @@ +/* + * 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 { distinctUntilChanged, map, pluck, switchMap, take } from 'rxjs/operators'; +import { PreferencesResponse, PreferencesService } from '../../../openapi/output'; +import { + DashboardAppsModel, + DashboardModel, + DashboardTileSettings, + defaultUserSettings, + LastUserActionSettings, STATE_KEYS, + UpdateUserPreferenceModel, + UserPreferencesModel, +} from '../model/user-preferences.model'; +import { BehaviorSubject, Observable, pipe, UnaryFunction } from 'rxjs'; +import { mergeWith as _mergeWith, isObject as _isObject } from 'lodash'; +import { isString } from '../helpers/helpers'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class UserSettingsService { + private userSettings: UserPreferencesModel = defaultUserSettings; + private preferencesTracker$ = new BehaviorSubject<UserPreferencesModel>(this.userSettings); + + constructor(private preferencesService: PreferencesService) { + this.getPreferences(); + } + + getPreferences$(): Observable<UserPreferencesModel> { + return this.preferencesTracker$.asObservable(); + } + + selectDashboard = () => + this.getPreferences$().pipe(selectDistinctState<UserPreferencesModel, DashboardModel>(STATE_KEYS.DASHBOARD)); + selectDashboardApps = () => + this.selectDashboard().pipe(selectDistinctState<DashboardModel, DashboardAppsModel>(STATE_KEYS.APPS)); + selectDashboardAvailableTiles = () => + this.selectDashboardApps().pipe(selectDistinctState<DashboardAppsModel, DashboardTileSettings[]>(STATE_KEYS.TILES)); + selectLastUserAction = () => + this.selectDashboardApps().pipe( + selectDistinctState<DashboardAppsModel, LastUserActionSettings>(STATE_KEYS.USER_ACTIONS), + ); + + getPreferences(): void { + this.preferencesService + .getPreferences() + .pipe( + map(preferences => { + return _mergeWith({}, defaultUserSettings, preferences.properties, (objValue, srcValue) => { + if ( + (Array.isArray(srcValue) && !srcValue.some(_isObject)) || + isString(srcValue) || + typeof srcValue === 'boolean' || + Number.isInteger(srcValue) + ) { + return srcValue; + } + }) as UserPreferencesModel; + }), + ) + .subscribe(userPreferences => { + this.preferencesTracker$.next(userPreferences); + }); + } + + updatePreferences(preferences: UpdateUserPreferenceModel): Observable<PreferencesResponse> { + return this.getPreferences$().pipe( + take(1), + switchMap(data => { + const properties = _mergeWith({}, data, preferences, (objValue, srcValue) => { + if ( + Array.isArray(srcValue) || + isString(srcValue) || + typeof srcValue === 'boolean' || + Number.isInteger(srcValue) + ) { + return srcValue; + } + }) as UserPreferencesModel; + this.preferencesTracker$.next(properties); + return this.preferencesService.savePreferences({ properties }); + }), + ); + } + + removePreferences(): Observable<PreferencesResponse> { + return this.preferencesService.updatePreferences({ properties: {} }); + } +} + +export function selectDistinctState<T, I>(key: string): UnaryFunction<Observable<T>, Observable<I>> { + return pipe(pluck<T, I>(key), distinctUntilChanged<I>()); +} diff --git a/src/app/shared.module.ts b/src/app/shared.module.ts new file mode 100644 index 0000000..eef81c0 --- /dev/null +++ b/src/app/shared.module.ts @@ -0,0 +1,78 @@ +/* + * 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 { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TableSkeletonComponent } from './components/shared/table-skeleton/table-skeleton.component'; +import { PaginationComponent } from './components/shared/pagination/pagination.component'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { BreadcrumbComponent } from './components/shared/breadcrumb/breadcrumb.component'; +import { BreadcrumbItemComponent } from './components/shared/breadcrumb-item/breadcrumb-item.component'; +import { NgModule } from '@angular/core'; +import { HasPermissionPipe } from './pipes/has-permission.pipe'; +import { HasPermissionsDirective } from './directives/has-permissions.directive'; +import { ColonPipe } from './pipes/colon.pipe'; +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { I18nModule } from './modules/i18n/i18n.module'; +import { AlertModule } from './modules/alerting'; +import { TranslateModule } from '@ngx-translate/core'; +import { LoadingSpinnerComponent } from './components/shared/loading-spinner/loading-spinner.component'; +import { InPipe } from 'src/app/pipes/in.pipe'; +import { IsTodayPipe } from 'src/app/pipes/is-today.pipe'; +import { MapPipe } from 'src/app/pipes/map.pipe'; + +@NgModule({ + imports: [CommonModule, NgbModule, I18nModule, FormsModule, ReactiveFormsModule, AlertModule, TranslateModule], + declarations: [ + HasPermissionPipe, + ColonPipe, + InPipe, + IsTodayPipe, + MapPipe, + HasPermissionsDirective, + TableSkeletonComponent, + PaginationComponent, + BreadcrumbComponent, + BreadcrumbItemComponent, + LoadingSpinnerComponent, + ], + exports: [ + CommonModule, + FormsModule, + NgbModule, + FormsModule, + ReactiveFormsModule, + DragDropModule, + I18nModule, + FormsModule, + ReactiveFormsModule, + AlertModule, + HasPermissionPipe, + ColonPipe, + InPipe, + IsTodayPipe, + MapPipe, + HasPermissionsDirective, + PaginationComponent, + TableSkeletonComponent, + BreadcrumbComponent, + BreadcrumbItemComponent, + LoadingSpinnerComponent, + ], +}) +export class SharedModule {} diff --git a/src/app/tilesmock.ts b/src/app/tilesmock.ts new file mode 100644 index 0000000..fc5aa40 --- /dev/null +++ b/src/app/tilesmock.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 { Tile } from './model/tile'; + +export const TILESMOCK: Tile[] = [ + { + id: 1, + title: 'tile1', + image_url: 'tile1.url', + image_alt_text: 'tile1', + description: 'tile1 desc', + redirect_url: 'redirect_url', + headers: 'header tile1', + }, + { + id: 2, + title: 'tile2', + image_url: 'tile2.url', + image_alt_text: 'tile2', + description: 'tile2 desc', + redirect_url: 'redirect_url', + headers: 'header tile2', + }, +]; |