aboutsummaryrefslogtreecommitdiffstats
path: root/src/app/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/services')
-rw-r--r--src/app/services/auth.service.ts94
-rw-r--r--src/app/services/authconfig.service.ts47
-rw-r--r--src/app/services/cacheservice/request-cache.service.spec.ts35
-rw-r--r--src/app/services/cacheservice/request-cache.service.ts89
-rw-r--r--src/app/services/fullscreen.service.ts58
-rw-r--r--src/app/services/history.service.ts67
-rw-r--r--src/app/services/loading-indicator.service.ts47
-rw-r--r--src/app/services/logging.service.ts34
-rw-r--r--src/app/services/shortcut.service.ts49
-rw-r--r--src/app/services/tileservice/tiles.service.spec.ts488
-rw-r--r--src/app/services/tileservice/tiles.service.ts88
-rw-r--r--src/app/services/unsubscribe/unsubscribe.service.ts32
-rw-r--r--src/app/services/user-settings.service.ts111
13 files changed, 1239 insertions, 0 deletions
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>());
+}