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