From 5edb50a1206d5d2b45af86cae5e38b4fb18ff40a Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Wed, 21 Aug 2024 17:58:59 +0200 Subject: [PATCH 01/14] add signals for auth.service --- AMW_angular/io/package-lock.json | 2 +- AMW_angular/io/src/app/auth/auth.service.ts | 32 ++++++++++--------- .../deployment-parameter.component.ts | 22 ++++++------- .../settings/releases/releases.component.ts | 29 +++++++++-------- .../io/src/app/settings/settings.component.ts | 24 +++++++------- .../src/app/settings/tags/tags.component.html | 2 +- .../src/app/settings/tags/tags.component.ts | 30 ++++++++--------- 7 files changed, 72 insertions(+), 69 deletions(-) diff --git a/AMW_angular/io/package-lock.json b/AMW_angular/io/package-lock.json index 621700cb2..6427c92e4 100644 --- a/AMW_angular/io/package-lock.json +++ b/AMW_angular/io/package-lock.json @@ -8112,7 +8112,7 @@ "engines": { "node": ">= 0.6" } - }, + } , "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/AMW_angular/io/src/app/auth/auth.service.ts b/AMW_angular/io/src/app/auth/auth.service.ts index 340542745..7af30b25a 100644 --- a/AMW_angular/io/src/app/auth/auth.service.ts +++ b/AMW_angular/io/src/app/auth/auth.service.ts @@ -1,9 +1,10 @@ -import { Injectable } from '@angular/core'; +import { computed, Injectable, signal } from '@angular/core'; import { BaseService } from '../base/base.service'; import { HttpClient } from '@angular/common/http'; import { Observable, startWith, Subject } from 'rxjs'; import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; import { Restriction } from '../settings/permission/restriction'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Injectable({ providedIn: 'root' }) export class AuthService extends BaseService { @@ -13,9 +14,14 @@ export class AuthService extends BaseService { switchMap(() => this.getRestrictions()), shareReplay(1), ); + #restrictions = signal(null); + restrictions = this.#restrictions.asReadonly(); constructor(private http: HttpClient) { super(); + this.restrictions$.pipe(takeUntilDestroyed()).subscribe((values) => { + this.#restrictions.set(values); + }); } refreshData() { @@ -30,23 +36,19 @@ export class AuthService extends BaseService { .pipe(catchError(this.handleError)); } - get userRestrictions() { - return this.restrictions$; + get isLoaded() { + return computed(() => !!this.restrictions()); } - getActionsForPermission(permissionName: string): Observable { - return this.restrictions$.pipe( - map((restrictions) => { - return restrictions.filter((entry) => entry.permission.name === permissionName).map((entry) => entry.action); - }), - ); + getActionsForPermission(permissionName: string): string[] { + return this.restrictions() + .filter((entry) => entry.permission.name === permissionName) + .map((entry) => entry.action); } - hasPermission(permissionName: string, action: string): Observable { - return this.getActionsForPermission(permissionName).pipe(map(values => { - return values.find(value => value === 'ALL' || value === action) !== undefined; - }) - ) + hasPermission(permissionName: string, action: string): boolean { + return ( + this.getActionsForPermission(permissionName).find((value) => value === 'ALL' || value === action) !== undefined + ); } - } diff --git a/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts b/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts index 4566d4e0f..15eba5126 100644 --- a/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts +++ b/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; @@ -20,8 +20,8 @@ type Key = { id: number; name: string }; export class DeploymentParameterComponent implements OnInit, OnDestroy { keyName = ''; paramKeys: Key[] = []; - canCreate = false; - canDelete = false; + canCreate = signal(false); + canDelete = signal(false); private destroy$ = new Subject(); constructor( @@ -44,17 +44,17 @@ export class DeploymentParameterComponent implements OnInit, OnDestroy { } private getUserPermissions() { - this.authService - .getActionsForPermission('MANAGE_DEPLOYMENT_PARAMETER') - .pipe(takeUntil(this.destroy$)) - .subscribe((value) => { - if (value.indexOf('ALL') > -1) { - this.canCreate = this.canDelete = true; + if (this.authService.isLoaded) { + this.authService.getActionsForPermission('MANAGE_DEPLOYMENT_PARAMETER').map((action) => { + if (action.indexOf('ALL') > -1) { + this.canDelete.set(true); + this.canCreate.set(true); } else { - this.canCreate = value.indexOf('CREATE') > -1; - this.canDelete = value.indexOf('DELETE') > -1; + this.canCreate.set(action.indexOf('CREATE') > -1); + this.canDelete.set(action.indexOf('DELETE') > -1); } }); + } } addKey(): void { diff --git a/AMW_angular/io/src/app/settings/releases/releases.component.ts b/AMW_angular/io/src/app/settings/releases/releases.component.ts index 56177743f..31e88887a 100644 --- a/AMW_angular/io/src/app/settings/releases/releases.component.ts +++ b/AMW_angular/io/src/app/settings/releases/releases.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, signal } from '@angular/core'; import { AsyncPipe, DatePipe } from '@angular/common'; import { LoadingIndicatorComponent } from '../../shared/elements/loading-indicator.component'; import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; @@ -48,16 +48,16 @@ export class ReleasesComponent implements OnInit { isLoading = true; - canCreate = false; - canEdit = false; - canDelete = false; + canCreate = signal(false); + canEdit = signal(false); + canDelete = signal(false); constructor( private authService: AuthService, private modalService: NgbModal, private releasesService: ReleasesService, private toastService: ToastService, - ) { } + ) {} ngOnInit(): void { this.error$.pipe(takeUntil(this.destroy$)).subscribe((msg) => { @@ -74,18 +74,19 @@ export class ReleasesComponent implements OnInit { } private getUserPermissions() { - this.authService - .getActionsForPermission('RELEASE') - .pipe(takeUntil(this.destroy$)) - .subscribe((value) => { - if (value.indexOf('ALL') > -1) { - this.canDelete = this.canEdit = this.canCreate = true; + if (this.authService.isLoaded()) { + this.authService.getActionsForPermission('RELEASE').map((action) => { + if (action.indexOf('ALL') > -1) { + this.canDelete.set(true); + this.canEdit.set(true); + this.canCreate.set(true); } else { - this.canCreate = value.indexOf('CREATE') > -1; - this.canEdit = value.indexOf('UPDATE') > -1; - this.canDelete = value.indexOf('DELETE') > -1; + this.canCreate.set(action.indexOf('CREATE') > -1); + this.canEdit.set(action.indexOf('UPDATE') > -1); + this.canDelete.set(action.indexOf('DELETE') > -1); } }); + } } private getReleases() { diff --git a/AMW_angular/io/src/app/settings/settings.component.ts b/AMW_angular/io/src/app/settings/settings.component.ts index 55a9e9465..ea701e390 100644 --- a/AMW_angular/io/src/app/settings/settings.component.ts +++ b/AMW_angular/io/src/app/settings/settings.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, signal } from '@angular/core'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { AuthService } from '../auth/auth.service'; import { PageComponent } from '../layout/page/page.component'; @@ -11,23 +11,23 @@ import { PageComponent } from '../layout/page/page.component'; imports: [RouterLink, RouterLinkActive, RouterOutlet, PageComponent], }) export class SettingsComponent implements OnInit { - canViewSettings = false - canViewPermissionsTab = false - canViewStpTab = false - canViewAppInfo = false + canViewSettings = signal(false); + canViewPermissionsTab = signal(false); + canViewStpTab = signal(false); + canViewAppInfo = signal(false); - constructor( - private authService: AuthService - ) { } + constructor(private authService: AuthService) {} ngOnInit(): void { this.getUserPermissions(); } private getUserPermissions() { - this.authService.hasPermission('SETTING_PANEL_LIST', 'ALL').subscribe(value => this.canViewSettings = value) - this.authService.hasPermission('ROLES_AND_PERMISSIONS_TAB', 'ALL').subscribe(value => this.canViewPermissionsTab = value) - this.authService.hasPermission('SHAKEDOWNTEST', 'ALL').subscribe(value => this.canViewStpTab = value) - this.authService.hasPermission('RELEASE', 'READ').subscribe(value => this.canViewAppInfo = value) + if (this.authService.isLoaded()) { + this.canViewSettings.set(this.authService.hasPermission('SETTING_PANEL_LIST', 'ALL')); + this.canViewPermissionsTab.set(this.authService.hasPermission('ROLES_AND_PERMISSIONS_TAB', 'ALL')); + this.canViewStpTab.set(this.authService.hasPermission('SHAKEDOWNTEST', 'ALL')); + this.canViewAppInfo.set(this.authService.hasPermission('RELEASE', 'READ')); + } } } diff --git a/AMW_angular/io/src/app/settings/tags/tags.component.html b/AMW_angular/io/src/app/settings/tags/tags.component.html index ec80c03b1..59aec0ad7 100644 --- a/AMW_angular/io/src/app/settings/tags/tags.component.html +++ b/AMW_angular/io/src/app/settings/tags/tags.component.html @@ -21,7 +21,7 @@

Tags

- @for (tag of tags; track tag) { + @for (tag of tags(); track tag) { {{ tag.name }} diff --git a/AMW_angular/io/src/app/settings/tags/tags.component.ts b/AMW_angular/io/src/app/settings/tags/tags.component.ts index a89cbd332..3e546e447 100644 --- a/AMW_angular/io/src/app/settings/tags/tags.component.ts +++ b/AMW_angular/io/src/app/settings/tags/tags.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; import { IconComponent } from '../../shared/icon/icon.component'; @@ -18,9 +18,9 @@ type Tag = { id: number; name: string }; }) export class TagsComponent implements OnInit, OnDestroy { tagName = ''; - tags: Tag[] = []; - canCreate = false; - canDelete = false; + tags = signal([]); + canCreate = signal(false); + canDelete = signal(false); private destroy$ = new Subject(); constructor( @@ -33,7 +33,7 @@ export class TagsComponent implements OnInit, OnDestroy { this.getUserPermissions(); this.http.get('/AMW_rest/resources/settings/tags').subscribe({ next: (data) => { - this.tags = data; + this.tags.set(data); }, }); } @@ -43,24 +43,24 @@ export class TagsComponent implements OnInit, OnDestroy { } private getUserPermissions() { - this.authService - .getActionsForPermission('MANAGE_GLOBAL_TAGS') - .pipe(takeUntil(this.destroy$)) - .subscribe((value) => { - if (value.indexOf('ALL') > -1) { - this.canCreate = this.canDelete = true; + if (this.authService.isLoaded()) { + this.authService.getActionsForPermission('MANAGE_GLOBAL_TAGS').map((action) => { + if (action.indexOf('ALL') > -1) { + this.canCreate.set(true); + this.canDelete.set(true); } else { - this.canCreate = value.indexOf('CREATE') > -1; - this.canDelete = value.indexOf('DELETE') > -1; + this.canCreate.set(action.indexOf('CREATE') > -1); + this.canDelete.set(action.indexOf('DELETE') > -1); } }); + } } addTag(): void { if (this.tagName.trim().length > 0) { this.http.post('/AMW_rest/resources/settings/tags', { name: this.tagName }).subscribe((newTag) => { this.toastService.success('Tag added.'); - this.tags.push(newTag); + this.tags.update((prevTags) => [...prevTags, newTag]); this.tagName = ''; }); } @@ -68,7 +68,7 @@ export class TagsComponent implements OnInit, OnDestroy { deleteTag(tagId: number): void { this.http.delete(`/AMW_rest/resources/settings/tags/${tagId}`).subscribe(() => { - this.tags = this.tags.filter((tag) => tag.id !== tagId); + this.tags.update((prevTags) => prevTags.filter((tag) => tag.id !== tagId)); this.toastService.success('Tag deleted.'); }); } From ae95da7c589602f2179be35b1b53d1cb9fbdd43b Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Thu, 22 Aug 2024 10:48:57 +0200 Subject: [PATCH 02/14] fix test for signals --- .../io/src/app/auth/auth.service.spec.ts | 100 ++++++++++++------ 1 file changed, 67 insertions(+), 33 deletions(-) diff --git a/AMW_angular/io/src/app/auth/auth.service.spec.ts b/AMW_angular/io/src/app/auth/auth.service.spec.ts index 41ad4ff5b..4cfd5243a 100644 --- a/AMW_angular/io/src/app/auth/auth.service.spec.ts +++ b/AMW_angular/io/src/app/auth/auth.service.spec.ts @@ -1,82 +1,116 @@ import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { AuthService } from './auth.service'; +import { provideHttpClient } from '@angular/common/http'; describe('AuthService', () => { let authService: AuthService; let httpTestingController: HttpTestingController; + let API_URL: string; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [AuthService], + imports: [], + providers: [AuthService, provideHttpClient(), provideHttpClientTesting()], }); authService = TestBed.inject(AuthService); httpTestingController = TestBed.inject(HttpTestingController); + API_URL = `${authService.getBaseUrl()}/permissions/restrictions/ownRestrictions/`; }); afterEach(() => { httpTestingController.verify(); }); - it('should return true if the user has the specified permission and action', () => { + it('should reload permissions', () => { + const req = httpTestingController.expectOne(API_URL); + expect(req.request.method).toBe('GET'); + expect(authService.isLoaded).toBeTruthy(); + authService.refreshData(); + + const req2 = httpTestingController.match(API_URL); + expect(req.request.method).toBe('GET'); + }); + + it('should return actions for a permission', () => { + const permissionName = 'examplePermission'; + const CREATE = 'CREATE'; + const READ = 'READ'; + + const req = httpTestingController.expectOne(API_URL); + expect(req.request.method).toBe('GET'); + req.flush([ + { permission: { name: permissionName }, action: READ }, + { permission: { name: permissionName }, action: CREATE }, + { permission: { name: 'secondPermission' }, action: CREATE }, + { permission: { name: 'thirdPermission' }, action: CREATE }, + ]); + expect(authService.isLoaded).toBeTruthy(); + expect(authService.getActionsForPermission(permissionName)).toEqual([READ, CREATE]); + }); + + it('should not return actions for a missing permission', () => { const permissionName = 'examplePermission'; const action = 'CREATE'; - authService.hasPermission(permissionName, action).subscribe((result) => { - expect(result).toBeTrue(); - }); + const req = httpTestingController.expectOne(API_URL); + expect(req.request.method).toBe('GET'); + req.flush([ + { permission: { name: 'firstPermiison' }, action: action }, + { permission: { name: 'secondPermission' }, action: action }, + { permission: { name: 'thirdPermission' }, action: action }, + ]); + expect(authService.isLoaded).toBeTruthy(); + expect(authService.getActionsForPermission(permissionName)).toEqual([]); + }); - const req = httpTestingController.expectOne(`${authService.getBaseUrl()}/permissions/restrictions/ownRestrictions/`); + it('should return true if the user has the specified permission and action', () => { + const permissionName = 'examplePermission'; + const action = 'CREATE'; + + const req = httpTestingController.expectOne(API_URL); expect(req.request.method).toBe('GET'); req.flush([ { permission: { name: permissionName }, action: 'READ' }, { permission: { name: permissionName }, action: action }, ]); + expect(authService.isLoaded).toBeTruthy(); + expect(authService.hasPermission(permissionName, action)).toBeTrue(); }); it('should return true if the user has the specified permission and ALL action', () => { const permissionName = 'examplePermission'; const action = 'CREATE'; - authService.hasPermission(permissionName, action).subscribe((result) => { - expect(result).toBeTrue(); - }); - - const req = httpTestingController.expectOne(`${authService.getBaseUrl()}/permissions/restrictions/ownRestrictions/`); + const req = httpTestingController.expectOne(API_URL); expect(req.request.method).toBe('GET'); - req.flush([ - { permission: { name: permissionName }, action: 'ALL' } - ]); + req.flush([{ permission: { name: permissionName }, action: 'ALL' }]); + + expect(authService.isLoaded).toBeTruthy(); + expect(authService.hasPermission(permissionName, action)).toBeTrue(); }); it('should return false if the user has the specified permission and but not action', () => { const permissionName = 'examplePermission'; const action = 'CREATE'; - authService.hasPermission(permissionName, action).subscribe((result) => { - expect(result).toBeFalse(); - }); - - const req = httpTestingController.expectOne(`${authService.getBaseUrl()}/permissions/restrictions/ownRestrictions/`); + const req = httpTestingController.expectOne(API_URL); expect(req.request.method).toBe('GET'); - req.flush([ - { permission: { name: permissionName }, action: 'READ' } - ]); + req.flush([{ permission: { name: permissionName }, action: 'READ' }]); + + expect(authService.isLoaded).toBeTruthy(); + expect(authService.hasPermission(permissionName, action)).toBeFalse(); }); it("should return false if the user doesn't have the specified permission", () => { const permissionName = 'examplePermission'; const action = 'CREATE'; - authService.hasPermission(permissionName, action).subscribe((result) => { - expect(result).toBeFalse(); - }); - - const req = httpTestingController.expectOne(`${authService.getBaseUrl()}/permissions/restrictions/ownRestrictions/`); + const req = httpTestingController.expectOne(API_URL); expect(req.request.method).toBe('GET'); - req.flush([ - { permission: { name: "otherPermission" }, action: 'READ' } - ]); + req.flush([{ permission: { name: 'otherPermission' }, action: 'READ' }]); + + expect(authService.isLoaded).toBeTruthy(); + expect(authService.hasPermission(permissionName, action)).toBeFalse(); }); -}); \ No newline at end of file +}); From ac07b640907b1343c8b75f14b09bcc74aa66c154 Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Thu, 22 Aug 2024 15:40:57 +0200 Subject: [PATCH 03/14] refactor: improve signals usage in auth service --- .../io/src/app/auth/auth.service.spec.ts | 10 ++--- AMW_angular/io/src/app/auth/auth.service.ts | 16 ++------ .../deployment-parameter.component.ts | 20 +++++----- .../settings/releases/releases.component.ts | 38 +++++++++---------- .../io/src/app/settings/settings.component.ts | 14 +++---- .../src/app/settings/tags/tags.component.ts | 20 +++++----- 6 files changed, 48 insertions(+), 70 deletions(-) diff --git a/AMW_angular/io/src/app/auth/auth.service.spec.ts b/AMW_angular/io/src/app/auth/auth.service.spec.ts index 4cfd5243a..537547e2f 100644 --- a/AMW_angular/io/src/app/auth/auth.service.spec.ts +++ b/AMW_angular/io/src/app/auth/auth.service.spec.ts @@ -25,7 +25,6 @@ describe('AuthService', () => { it('should reload permissions', () => { const req = httpTestingController.expectOne(API_URL); expect(req.request.method).toBe('GET'); - expect(authService.isLoaded).toBeTruthy(); authService.refreshData(); const req2 = httpTestingController.match(API_URL); @@ -45,7 +44,7 @@ describe('AuthService', () => { { permission: { name: 'secondPermission' }, action: CREATE }, { permission: { name: 'thirdPermission' }, action: CREATE }, ]); - expect(authService.isLoaded).toBeTruthy(); + expect(authService.getActionsForPermission(permissionName)).toEqual([READ, CREATE]); }); @@ -60,7 +59,7 @@ describe('AuthService', () => { { permission: { name: 'secondPermission' }, action: action }, { permission: { name: 'thirdPermission' }, action: action }, ]); - expect(authService.isLoaded).toBeTruthy(); + expect(authService.getActionsForPermission(permissionName)).toEqual([]); }); @@ -74,7 +73,7 @@ describe('AuthService', () => { { permission: { name: permissionName }, action: 'READ' }, { permission: { name: permissionName }, action: action }, ]); - expect(authService.isLoaded).toBeTruthy(); + expect(authService.hasPermission(permissionName, action)).toBeTrue(); }); @@ -86,7 +85,6 @@ describe('AuthService', () => { expect(req.request.method).toBe('GET'); req.flush([{ permission: { name: permissionName }, action: 'ALL' }]); - expect(authService.isLoaded).toBeTruthy(); expect(authService.hasPermission(permissionName, action)).toBeTrue(); }); @@ -98,7 +96,6 @@ describe('AuthService', () => { expect(req.request.method).toBe('GET'); req.flush([{ permission: { name: permissionName }, action: 'READ' }]); - expect(authService.isLoaded).toBeTruthy(); expect(authService.hasPermission(permissionName, action)).toBeFalse(); }); @@ -110,7 +107,6 @@ describe('AuthService', () => { expect(req.request.method).toBe('GET'); req.flush([{ permission: { name: 'otherPermission' }, action: 'READ' }]); - expect(authService.isLoaded).toBeTruthy(); expect(authService.hasPermission(permissionName, action)).toBeFalse(); }); }); diff --git a/AMW_angular/io/src/app/auth/auth.service.ts b/AMW_angular/io/src/app/auth/auth.service.ts index 7af30b25a..f7194cdec 100644 --- a/AMW_angular/io/src/app/auth/auth.service.ts +++ b/AMW_angular/io/src/app/auth/auth.service.ts @@ -1,27 +1,23 @@ -import { computed, Injectable, signal } from '@angular/core'; +import { Injectable } from '@angular/core'; import { BaseService } from '../base/base.service'; import { HttpClient } from '@angular/common/http'; import { Observable, startWith, Subject } from 'rxjs'; import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; import { Restriction } from '../settings/permission/restriction'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { toSignal } from '@angular/core/rxjs-interop'; @Injectable({ providedIn: 'root' }) export class AuthService extends BaseService { private reload$ = new Subject(); - private readonly restrictions$ = this.reload$.pipe( + private restrictions$ = this.reload$.pipe( startWith(null), switchMap(() => this.getRestrictions()), shareReplay(1), ); - #restrictions = signal(null); - restrictions = this.#restrictions.asReadonly(); + restrictions = toSignal(this.restrictions$, { initialValue: [] as Restriction[] }); constructor(private http: HttpClient) { super(); - this.restrictions$.pipe(takeUntilDestroyed()).subscribe((values) => { - this.#restrictions.set(values); - }); } refreshData() { @@ -36,10 +32,6 @@ export class AuthService extends BaseService { .pipe(catchError(this.handleError)); } - get isLoaded() { - return computed(() => !!this.restrictions()); - } - getActionsForPermission(permissionName: string): string[] { return this.restrictions() .filter((entry) => entry.permission.name === permissionName) diff --git a/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts b/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts index 15eba5126..2b7bedad2 100644 --- a/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts +++ b/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts @@ -44,17 +44,15 @@ export class DeploymentParameterComponent implements OnInit, OnDestroy { } private getUserPermissions() { - if (this.authService.isLoaded) { - this.authService.getActionsForPermission('MANAGE_DEPLOYMENT_PARAMETER').map((action) => { - if (action.indexOf('ALL') > -1) { - this.canDelete.set(true); - this.canCreate.set(true); - } else { - this.canCreate.set(action.indexOf('CREATE') > -1); - this.canDelete.set(action.indexOf('DELETE') > -1); - } - }); - } + this.authService.getActionsForPermission('MANAGE_DEPLOYMENT_PARAMETER').map((action) => { + if (action.indexOf('ALL') > -1) { + this.canDelete.set(true); + this.canCreate.set(true); + } else { + this.canCreate.set(action.indexOf('CREATE') > -1); + this.canDelete.set(action.indexOf('DELETE') > -1); + } + }); } addKey(): void { diff --git a/AMW_angular/io/src/app/settings/releases/releases.component.ts b/AMW_angular/io/src/app/settings/releases/releases.component.ts index 31e88887a..1a4f6d511 100644 --- a/AMW_angular/io/src/app/settings/releases/releases.component.ts +++ b/AMW_angular/io/src/app/settings/releases/releases.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, signal } from '@angular/core'; +import { Component, inject, OnInit, signal } from '@angular/core'; import { AsyncPipe, DatePipe } from '@angular/common'; import { LoadingIndicatorComponent } from '../../shared/elements/loading-indicator.component'; import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; @@ -29,6 +29,11 @@ import { ToastService } from '../../shared/elements/toast/toast.service'; templateUrl: './releases.component.html', }) export class ReleasesComponent implements OnInit { + private authService = inject(AuthService); + private modalService = inject(NgbModal); + private releasesService = inject(ReleasesService); + private toastService = inject(ToastService); + releases$: Observable; defaultRelease$: Observable; count$: Observable; @@ -52,13 +57,6 @@ export class ReleasesComponent implements OnInit { canEdit = signal(false); canDelete = signal(false); - constructor( - private authService: AuthService, - private modalService: NgbModal, - private releasesService: ReleasesService, - private toastService: ToastService, - ) {} - ngOnInit(): void { this.error$.pipe(takeUntil(this.destroy$)).subscribe((msg) => { msg !== '' ? this.toastService.error(msg) : null; @@ -74,19 +72,17 @@ export class ReleasesComponent implements OnInit { } private getUserPermissions() { - if (this.authService.isLoaded()) { - this.authService.getActionsForPermission('RELEASE').map((action) => { - if (action.indexOf('ALL') > -1) { - this.canDelete.set(true); - this.canEdit.set(true); - this.canCreate.set(true); - } else { - this.canCreate.set(action.indexOf('CREATE') > -1); - this.canEdit.set(action.indexOf('UPDATE') > -1); - this.canDelete.set(action.indexOf('DELETE') > -1); - } - }); - } + this.authService.getActionsForPermission('RELEASE').map((action) => { + if (action.indexOf('ALL') > -1) { + this.canDelete.set(true); + this.canEdit.set(true); + this.canCreate.set(true); + } else { + this.canCreate.set(action.indexOf('CREATE') > -1); + this.canEdit.set(action.indexOf('UPDATE') > -1); + this.canDelete.set(action.indexOf('DELETE') > -1); + } + }); } private getReleases() { diff --git a/AMW_angular/io/src/app/settings/settings.component.ts b/AMW_angular/io/src/app/settings/settings.component.ts index ea701e390..672636515 100644 --- a/AMW_angular/io/src/app/settings/settings.component.ts +++ b/AMW_angular/io/src/app/settings/settings.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, signal } from '@angular/core'; +import { Component, inject, OnInit, signal } from '@angular/core'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { AuthService } from '../auth/auth.service'; import { PageComponent } from '../layout/page/page.component'; @@ -16,18 +16,16 @@ export class SettingsComponent implements OnInit { canViewStpTab = signal(false); canViewAppInfo = signal(false); - constructor(private authService: AuthService) {} + authService = inject(AuthService); ngOnInit(): void { this.getUserPermissions(); } private getUserPermissions() { - if (this.authService.isLoaded()) { - this.canViewSettings.set(this.authService.hasPermission('SETTING_PANEL_LIST', 'ALL')); - this.canViewPermissionsTab.set(this.authService.hasPermission('ROLES_AND_PERMISSIONS_TAB', 'ALL')); - this.canViewStpTab.set(this.authService.hasPermission('SHAKEDOWNTEST', 'ALL')); - this.canViewAppInfo.set(this.authService.hasPermission('RELEASE', 'READ')); - } + this.canViewSettings.set(this.authService.hasPermission('SETTING_PANEL_LIST', 'ALL')); + this.canViewPermissionsTab.set(this.authService.hasPermission('ROLES_AND_PERMISSIONS_TAB', 'ALL')); + this.canViewStpTab.set(this.authService.hasPermission('SHAKEDOWNTEST', 'ALL')); + this.canViewAppInfo.set(this.authService.hasPermission('RELEASE', 'READ')); } } diff --git a/AMW_angular/io/src/app/settings/tags/tags.component.ts b/AMW_angular/io/src/app/settings/tags/tags.component.ts index 3e546e447..2d07b40bc 100644 --- a/AMW_angular/io/src/app/settings/tags/tags.component.ts +++ b/AMW_angular/io/src/app/settings/tags/tags.component.ts @@ -43,17 +43,15 @@ export class TagsComponent implements OnInit, OnDestroy { } private getUserPermissions() { - if (this.authService.isLoaded()) { - this.authService.getActionsForPermission('MANAGE_GLOBAL_TAGS').map((action) => { - if (action.indexOf('ALL') > -1) { - this.canCreate.set(true); - this.canDelete.set(true); - } else { - this.canCreate.set(action.indexOf('CREATE') > -1); - this.canDelete.set(action.indexOf('DELETE') > -1); - } - }); - } + this.authService.getActionsForPermission('MANAGE_GLOBAL_TAGS').map((action) => { + if (action.indexOf('ALL') > -1) { + this.canCreate.set(true); + this.canDelete.set(true); + } else { + this.canCreate.set(action.indexOf('CREATE') > -1); + this.canDelete.set(action.indexOf('DELETE') > -1); + } + }); } addTag(): void { From 635415b8c31fe86e335859adabe51e5fee574796 Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Thu, 22 Aug 2024 15:42:32 +0200 Subject: [PATCH 04/14] refactor: separate tags component to use signals and separation of concerns --- AMW_angular/io/src/app/settings/tags/tag.ts | 4 ++ .../src/app/settings/tags/tags.component.html | 2 +- .../src/app/settings/tags/tags.component.ts | 44 +++++++------------ .../io/src/app/settings/tags/tags.service.ts | 28 ++++++++++++ 4 files changed, 49 insertions(+), 29 deletions(-) create mode 100644 AMW_angular/io/src/app/settings/tags/tag.ts create mode 100644 AMW_angular/io/src/app/settings/tags/tags.service.ts diff --git a/AMW_angular/io/src/app/settings/tags/tag.ts b/AMW_angular/io/src/app/settings/tags/tag.ts new file mode 100644 index 000000000..21ec96d0e --- /dev/null +++ b/AMW_angular/io/src/app/settings/tags/tag.ts @@ -0,0 +1,4 @@ +export interface Tag { + id: number; + name: string; +} diff --git a/AMW_angular/io/src/app/settings/tags/tags.component.html b/AMW_angular/io/src/app/settings/tags/tags.component.html index 59aec0ad7..42e484b1c 100644 --- a/AMW_angular/io/src/app/settings/tags/tags.component.html +++ b/AMW_angular/io/src/app/settings/tags/tags.component.html @@ -1,5 +1,5 @@
-

Tags

+

{{ pageTitle }}

diff --git a/AMW_angular/io/src/app/settings/tags/tags.component.ts b/AMW_angular/io/src/app/settings/tags/tags.component.ts index 2d07b40bc..c3d61b932 100644 --- a/AMW_angular/io/src/app/settings/tags/tags.component.ts +++ b/AMW_angular/io/src/app/settings/tags/tags.component.ts @@ -1,13 +1,11 @@ -import { Component, OnDestroy, OnInit, signal } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { IconComponent } from '../../shared/icon/icon.component'; import { Subject } from 'rxjs'; import { AuthService } from '../../auth/auth.service'; -import { takeUntil } from 'rxjs/operators'; import { ToastService } from '../../shared/elements/toast/toast.service'; - -type Tag = { id: number; name: string }; +import { Tag } from './tag'; +import { TagsService } from './tags.service'; @Component({ selector: 'app-tags', @@ -17,25 +15,19 @@ type Tag = { id: number; name: string }; imports: [FormsModule, IconComponent], }) export class TagsComponent implements OnInit, OnDestroy { - tagName = ''; - tags = signal([]); + private tagsService = inject(TagsService); + private authService = inject(AuthService); + private toastService = inject(ToastService); + + pageTitle = 'Tags'; + tagName = signal(''); + tags = this.tagsService.tags; canCreate = signal(false); canDelete = signal(false); private destroy$ = new Subject(); - constructor( - private http: HttpClient, - private authService: AuthService, - private toastService: ToastService, - ) {} - ngOnInit(): void { this.getUserPermissions(); - this.http.get('/AMW_rest/resources/settings/tags').subscribe({ - next: (data) => { - this.tags.set(data); - }, - }); } ngOnDestroy(): void { @@ -55,19 +47,15 @@ export class TagsComponent implements OnInit, OnDestroy { } addTag(): void { - if (this.tagName.trim().length > 0) { - this.http.post('/AMW_rest/resources/settings/tags', { name: this.tagName }).subscribe((newTag) => { - this.toastService.success('Tag added.'); - this.tags.update((prevTags) => [...prevTags, newTag]); - this.tagName = ''; - }); + if (this.tagName().trim().length > 0) { + this.tagsService.add(this.tagName()); + this.toastService.success('Tag added.'); + this.tagName.set(''); } } deleteTag(tagId: number): void { - this.http.delete(`/AMW_rest/resources/settings/tags/${tagId}`).subscribe(() => { - this.tags.update((prevTags) => prevTags.filter((tag) => tag.id !== tagId)); - this.toastService.success('Tag deleted.'); - }); + this.tagsService.delete(tagId); + this.toastService.success('Tag deleted.'); } } diff --git a/AMW_angular/io/src/app/settings/tags/tags.service.ts b/AMW_angular/io/src/app/settings/tags/tags.service.ts new file mode 100644 index 000000000..156c5f4d7 --- /dev/null +++ b/AMW_angular/io/src/app/settings/tags/tags.service.ts @@ -0,0 +1,28 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Tag } from './tag'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { tap } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class TagsService { + private http = inject(HttpClient); + tagsUrl = '/AMW_rest/resources/settings/tags'; + + tags = signal([]); + private tags$ = this.http.get(this.tagsUrl).pipe(tap((tags) => this.tags.set(tags))); + // used to automatically un-/subscribe to observable + readOnlyTags = toSignal(this.tags$, { initialValue: [] as Tag[] }); + + add(tagName: string) { + this.http.post(this.tagsUrl, { name: tagName }).subscribe((newTag) => { + this.tags.update((tags) => [...tags, newTag]); + }); + } + + delete(tagId: number) { + this.http.delete(this.tagsUrl + `/${tagId}`).subscribe(() => { + this.tags.update((tags) => tags.filter((tag) => tag.id !== tagId)); + }); + } +} From 5639cfa9353e6a0c1ecaa5a63b515cdfbf3da180 Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Thu, 22 Aug 2024 15:42:49 +0200 Subject: [PATCH 05/14] doc: add coding-guidelines --- AMW_angular/io/README.md | 5 +++ AMW_angular/io/coding-guidelines.md | 54 +++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 AMW_angular/io/coding-guidelines.md diff --git a/AMW_angular/io/README.md b/AMW_angular/io/README.md index 723a2300d..0113cdfaf 100644 --- a/AMW_angular/io/README.md +++ b/AMW_angular/io/README.md @@ -21,6 +21,10 @@ npm run backend:start npm run backend:stop ``` +## Coding guidelines + +See [coding-guideline.md](./coding-guidelines.md) + ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. @@ -49,3 +53,4 @@ You can use any icon from the boostrap icon set with the custom `` com ``` ``` + diff --git a/AMW_angular/io/coding-guidelines.md b/AMW_angular/io/coding-guidelines.md new file mode 100644 index 000000000..1a72ed0ec --- /dev/null +++ b/AMW_angular/io/coding-guidelines.md @@ -0,0 +1,54 @@ + +# Coding Guidelines + +## Inject() over Constructor-Injections + +Use `inject()` instead of constuctor-injection to make the code more explicit and obvious. + +```typescript +// use +myService = inject(MyService); + +// instead of +constructor( + private myservice: MyService +) {} +``` + +## Signals + +Use signals for changing values in Components + +### Signal from Observable + +#### GET + +Make API-requests with observables and expose the state as a signal: + +```typescript +// retrive data from API using RxJS +private users$ = this.http.get(this.userUrl); + +// expose as signal +users = toSignal(this.users$, { initialValue: [] as User[]}); +``` +The observable `users$` is just used to pass the state to the *readonly* signal `users`. (The signals created from an observable are always *readonly*!) +No need for unsubscription - this is handled by `toSignal()` automatically. + +Use the signal in the component not in the template. (Separation of concerns) + +#### POST / DELETE etc. + +To update data in a signal you have to create a WritableSignal: + +```typescript +// WritableSignal +users = signal([]); +// retrive data from API using RxJS and write it in the WritableSignal +private users$ = this.http.get(this.userUrl).pipe(tap((users) => this.users.set(users))); +// only used to automatically un-/subscribe to the observable +readOnlyUsers = toSignal(this.users$, { initialValue: [] as User[]}); +``` + + + From 78fae750b386984c6d39b7f04c47e2bde4d8d3b2 Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Thu, 22 Aug 2024 16:00:50 +0200 Subject: [PATCH 06/14] chore: add pre-commit-hook for prettier --- AMW_angular/io/.husky/pre-commit | 1 + AMW_angular/io/README.md | 5 ++++- AMW_angular/io/coding-guidelines.md | 7 ++----- AMW_angular/io/package.json | 4 +++- .../io/src/app/settings/tags/tags.component.spec.ts | 6 +++--- AMW_angular/io/src/app/settings/tags/tags.service.ts | 2 +- 6 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 AMW_angular/io/.husky/pre-commit diff --git a/AMW_angular/io/.husky/pre-commit b/AMW_angular/io/.husky/pre-commit new file mode 100644 index 000000000..f21383c0e --- /dev/null +++ b/AMW_angular/io/.husky/pre-commit @@ -0,0 +1 @@ +git-format-staged -f 'prettier --ignore-unknown --stdin --stdin-filepath "{}"' . diff --git a/AMW_angular/io/README.md b/AMW_angular/io/README.md index 0113cdfaf..48767ed26 100644 --- a/AMW_angular/io/README.md +++ b/AMW_angular/io/README.md @@ -25,6 +25,10 @@ npm run backend:stop See [coding-guideline.md](./coding-guidelines.md) +## Code Formatting + +We use Prettier for code-formatting. Additionally, there is a pre-commit hook to format all staged files with husky. [More info](https://github.com/hallettj/git-format-staged) + ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. @@ -53,4 +57,3 @@ You can use any icon from the boostrap icon set with the custom `` com ``` ``` - diff --git a/AMW_angular/io/coding-guidelines.md b/AMW_angular/io/coding-guidelines.md index 1a72ed0ec..1759d91d1 100644 --- a/AMW_angular/io/coding-guidelines.md +++ b/AMW_angular/io/coding-guidelines.md @@ -1,4 +1,3 @@ - # Coding Guidelines ## Inject() over Constructor-Injections @@ -32,7 +31,8 @@ private users$ = this.http.get(this.userUrl); // expose as signal users = toSignal(this.users$, { initialValue: [] as User[]}); ``` -The observable `users$` is just used to pass the state to the *readonly* signal `users`. (The signals created from an observable are always *readonly*!) + +The observable `users$` is just used to pass the state to the _readonly_ signal `users`. (The signals created from an observable are always _readonly_!) No need for unsubscription - this is handled by `toSignal()` automatically. Use the signal in the component not in the template. (Separation of concerns) @@ -49,6 +49,3 @@ private users$ = this.http.get(this.userUrl).pipe(tap((users) => this.us // only used to automatically un-/subscribe to the observable readOnlyUsers = toSignal(this.users$, { initialValue: [] as User[]}); ``` - - - diff --git a/AMW_angular/io/package.json b/AMW_angular/io/package.json index 4608acbc2..e7617474e 100644 --- a/AMW_angular/io/package.json +++ b/AMW_angular/io/package.json @@ -12,7 +12,8 @@ "test-headless": "ng test --browsers=ChromeHeadless", "lint": "ng lint", "prettier": "npx prettier --write .", - "mavenbuild": "ng test --watch=false --browsers=ChromeHeadless && ng build --configuration production" + "mavenbuild": "ng test --watch=false --browsers=ChromeHeadless && ng build --configuration production", + "prepare": "husky" }, "private": true, "dependencies": { @@ -56,6 +57,7 @@ "eslint": "8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.0.1", + "git-format-staged": "^3.1.1", "jasmine-core": "~5.1.1", "jasmine-spec-reporter": "~7.0.0", "prettier": "3.0.3", diff --git a/AMW_angular/io/src/app/settings/tags/tags.component.spec.ts b/AMW_angular/io/src/app/settings/tags/tags.component.spec.ts index 268d589b0..32365cd88 100644 --- a/AMW_angular/io/src/app/settings/tags/tags.component.spec.ts +++ b/AMW_angular/io/src/app/settings/tags/tags.component.spec.ts @@ -10,9 +10,9 @@ describe('TagsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TagsComponent], - providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}).compileComponents(); + imports: [TagsComponent], + providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + }).compileComponents(); fixture = TestBed.createComponent(TagsComponent); component = fixture.componentInstance; diff --git a/AMW_angular/io/src/app/settings/tags/tags.service.ts b/AMW_angular/io/src/app/settings/tags/tags.service.ts index 156c5f4d7..eeeb3b5ac 100644 --- a/AMW_angular/io/src/app/settings/tags/tags.service.ts +++ b/AMW_angular/io/src/app/settings/tags/tags.service.ts @@ -1,7 +1,7 @@ import { inject, Injectable, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Tag } from './tag'; -import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { toSignal } from '@angular/core/rxjs-interop'; import { tap } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) From 6db07552e16b05eaaad2d372ca3771d901a58a28 Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Thu, 22 Aug 2024 16:03:01 +0200 Subject: [PATCH 07/14] chore: add pre-commit-hook for prettier --- AMW_angular/io/angular.json | 11 ++--------- AMW_angular/io/src/app/auth/auth.service.spec.ts | 2 +- .../deployment-parameter.component.ts | 1 - .../io/src/app/settings/tags/tags.component.ts | 1 - 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/AMW_angular/io/angular.json b/AMW_angular/io/angular.json index 26fe26355..c3bffc589 100644 --- a/AMW_angular/io/angular.json +++ b/AMW_angular/io/angular.json @@ -21,10 +21,7 @@ "base": "dist" }, "index": "src/index.html", - "polyfills": [ - "zone.js", - "@angular/localize/init" - ], + "polyfills": ["zone.js", "@angular/localize/init"], "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", @@ -99,11 +96,7 @@ "builder": "@angular-devkit/build-angular:web-test-runner", "options": { "main": "src/test.ts", - "polyfills": [ - "zone.js", - "zone.js/testing", - "@angular/localize/init" - ], + "polyfills": ["zone.js", "zone.js/testing", "@angular/localize/init"], "tsConfig": "tsconfig.spec.json", "assets": ["src/favicon.ico", "src/assets"], "styles": ["node_modules/bootstrap/dist/css/bootstrap.min.css", "src/styles.scss"] diff --git a/AMW_angular/io/src/app/auth/auth.service.spec.ts b/AMW_angular/io/src/app/auth/auth.service.spec.ts index 537547e2f..898e920e5 100644 --- a/AMW_angular/io/src/app/auth/auth.service.spec.ts +++ b/AMW_angular/io/src/app/auth/auth.service.spec.ts @@ -28,7 +28,7 @@ describe('AuthService', () => { authService.refreshData(); const req2 = httpTestingController.match(API_URL); - expect(req.request.method).toBe('GET'); + expect(req2.length).toBe(2); }); it('should return actions for a permission', () => { diff --git a/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts b/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts index 2b7bedad2..ffcf76260 100644 --- a/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts +++ b/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts @@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { IconComponent } from '../../shared/icon/icon.component'; -import { takeUntil } from 'rxjs/operators'; import { AuthService } from '../../auth/auth.service'; import { Subject } from 'rxjs'; import { ToastService } from '../../shared/elements/toast/toast.service'; diff --git a/AMW_angular/io/src/app/settings/tags/tags.component.ts b/AMW_angular/io/src/app/settings/tags/tags.component.ts index c3d61b932..d8f6bde8e 100644 --- a/AMW_angular/io/src/app/settings/tags/tags.component.ts +++ b/AMW_angular/io/src/app/settings/tags/tags.component.ts @@ -4,7 +4,6 @@ import { IconComponent } from '../../shared/icon/icon.component'; import { Subject } from 'rxjs'; import { AuthService } from '../../auth/auth.service'; import { ToastService } from '../../shared/elements/toast/toast.service'; -import { Tag } from './tag'; import { TagsService } from './tags.service'; @Component({ From c8c8c3828cc7d63bf683a453f6b109ce35b3a953 Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Thu, 22 Aug 2024 16:08:40 +0200 Subject: [PATCH 08/14] chore: add husky --- AMW_angular/io/package-lock.json | 18 ++++++++++++++++++ AMW_angular/io/package.json | 1 + 2 files changed, 19 insertions(+) diff --git a/AMW_angular/io/package-lock.json b/AMW_angular/io/package-lock.json index 6427c92e4..6e8e39651 100644 --- a/AMW_angular/io/package-lock.json +++ b/AMW_angular/io/package-lock.json @@ -48,6 +48,8 @@ "eslint": "8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.0.1", + "git-format-staged": "^3.1.1", + "husky": "^9.1.5", "jasmine-core": "~5.1.1", "jasmine-spec-reporter": "~7.0.0", "prettier": "3.0.3", @@ -10808,6 +10810,22 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.5.tgz", + "integrity": "sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", diff --git a/AMW_angular/io/package.json b/AMW_angular/io/package.json index e7617474e..a6f816f39 100644 --- a/AMW_angular/io/package.json +++ b/AMW_angular/io/package.json @@ -58,6 +58,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.0.1", "git-format-staged": "^3.1.1", + "husky": "^9.1.5", "jasmine-core": "~5.1.1", "jasmine-spec-reporter": "~7.0.0", "prettier": "3.0.3", From 9eea26260ebce13b4eb1fa501a5e23df0387a7bd Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Thu, 22 Aug 2024 21:19:26 +0200 Subject: [PATCH 09/14] fix: unit test for permission reload --- AMW_angular/io/src/app/auth/auth.service.spec.ts | 7 ++----- AMW_angular/io/src/app/auth/auth.service.ts | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/AMW_angular/io/src/app/auth/auth.service.spec.ts b/AMW_angular/io/src/app/auth/auth.service.spec.ts index 898e920e5..c313cff13 100644 --- a/AMW_angular/io/src/app/auth/auth.service.spec.ts +++ b/AMW_angular/io/src/app/auth/auth.service.spec.ts @@ -23,12 +23,9 @@ describe('AuthService', () => { }); it('should reload permissions', () => { - const req = httpTestingController.expectOne(API_URL); - expect(req.request.method).toBe('GET'); authService.refreshData(); - - const req2 = httpTestingController.match(API_URL); - expect(req2.length).toBe(2); + const requests = httpTestingController.match(API_URL); + expect(requests.length).toEqual(2); }); it('should return actions for a permission', () => { diff --git a/AMW_angular/io/src/app/auth/auth.service.ts b/AMW_angular/io/src/app/auth/auth.service.ts index f7194cdec..3eebc16d8 100644 --- a/AMW_angular/io/src/app/auth/auth.service.ts +++ b/AMW_angular/io/src/app/auth/auth.service.ts @@ -21,7 +21,7 @@ export class AuthService extends BaseService { } refreshData() { - this.getRestrictions().pipe(map((v) => this.reload$.next(v))); + this.reload$.next([]); } private getRestrictions(): Observable { From 5e81bfc4dc3abcf7a5905329a2b93edfe7deaf94 Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Thu, 22 Aug 2024 21:36:20 +0200 Subject: [PATCH 10/14] rebase master --- AMW_angular/io/package-lock.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/AMW_angular/io/package-lock.json b/AMW_angular/io/package-lock.json index 6e8e39651..67acacefa 100644 --- a/AMW_angular/io/package-lock.json +++ b/AMW_angular/io/package-lock.json @@ -8114,7 +8114,7 @@ "engines": { "node": ">= 0.6" } - } , + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -10352,6 +10352,16 @@ "node": ">= 14" } }, + "node_modules/git-format-staged": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/git-format-staged/-/git-format-staged-3.1.1.tgz", + "integrity": "sha512-P749fkktaiAchFZKR7bgdvruzhvbcIDr1uRBrS9/Wdimb7wH1Twchz9gOixj8tUaHVMuXY/ckDojfOwV6AxgPA==", + "dev": true, + "license": "MIT", + "bin": { + "git-format-staged": "git-format-staged" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", From 7bf43a80cebe276f5cfed5a24c9a04bdb5a26d9d Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Thu, 22 Aug 2024 21:56:15 +0200 Subject: [PATCH 11/14] chore: add config to web-test-runner --- AMW_angular/io/package.json | 2 +- AMW_angular/io/web-test-runner.config.mjs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 AMW_angular/io/web-test-runner.config.mjs diff --git a/AMW_angular/io/package.json b/AMW_angular/io/package.json index a6f816f39..6ebaa6cd9 100644 --- a/AMW_angular/io/package.json +++ b/AMW_angular/io/package.json @@ -9,7 +9,7 @@ "backend:stop": "docker compose -f ./../../AMW_docker/docker-compose/docker-compose.yml down", "build": "ng build", "test": "ng test", - "test-headless": "ng test --browsers=ChromeHeadless", + "test:watch": "ng test --watch", "lint": "ng lint", "prettier": "npx prettier --write .", "mavenbuild": "ng test --watch=false --browsers=ChromeHeadless && ng build --configuration production", diff --git a/AMW_angular/io/web-test-runner.config.mjs b/AMW_angular/io/web-test-runner.config.mjs new file mode 100644 index 000000000..a5c438038 --- /dev/null +++ b/AMW_angular/io/web-test-runner.config.mjs @@ -0,0 +1,8 @@ +import { chromeLauncher } from '@web/test-runner'; + +export default { + concurrency: 10, + nodeResolve: true, + watch: true, + browsers: [chromeLauncher({ launchOptions: { args: ['--no-sandbox'] } })], +}; From 8db243fc9f17c0ce7bfbbda806d7a2aa7039fda3 Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Fri, 23 Aug 2024 10:24:50 +0200 Subject: [PATCH 12/14] refactor: permission handling in components --- AMW_angular/io/src/app/auth/auth.service.ts | 6 +++++- .../deployment-parameter.component.ts | 14 ++++---------- .../app/settings/releases/releases.component.ts | 17 +++++------------ .../io/src/app/settings/tags/tags.component.ts | 14 ++++---------- 4 files changed, 18 insertions(+), 33 deletions(-) diff --git a/AMW_angular/io/src/app/auth/auth.service.ts b/AMW_angular/io/src/app/auth/auth.service.ts index 3eebc16d8..e98d58b86 100644 --- a/AMW_angular/io/src/app/auth/auth.service.ts +++ b/AMW_angular/io/src/app/auth/auth.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { BaseService } from '../base/base.service'; import { HttpClient } from '@angular/common/http'; import { Observable, startWith, Subject } from 'rxjs'; -import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; +import { catchError, shareReplay, switchMap } from 'rxjs/operators'; import { Restriction } from '../settings/permission/restriction'; import { toSignal } from '@angular/core/rxjs-interop'; @@ -44,3 +44,7 @@ export class AuthService extends BaseService { ); } } + +export function isAllowed(action: string, role: string) { + return action === 'ALL' || action === role; +} diff --git a/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts b/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts index ffcf76260..8e862d36b 100644 --- a/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts +++ b/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { IconComponent } from '../../shared/icon/icon.component'; -import { AuthService } from '../../auth/auth.service'; +import { AuthService, isAllowed } from '../../auth/auth.service'; import { Subject } from 'rxjs'; import { ToastService } from '../../shared/elements/toast/toast.service'; @@ -43,15 +43,9 @@ export class DeploymentParameterComponent implements OnInit, OnDestroy { } private getUserPermissions() { - this.authService.getActionsForPermission('MANAGE_DEPLOYMENT_PARAMETER').map((action) => { - if (action.indexOf('ALL') > -1) { - this.canDelete.set(true); - this.canCreate.set(true); - } else { - this.canCreate.set(action.indexOf('CREATE') > -1); - this.canDelete.set(action.indexOf('DELETE') > -1); - } - }); + const actions = this.authService.getActionsForPermission('MANAGE_DEPLOYMENT_PARAMETER'); + this.canCreate.set(actions.some((action) => isAllowed(action, 'CREATE'))); + this.canDelete.set(actions.some((action) => isAllowed(action, 'DELETE'))); } addKey(): void { diff --git a/AMW_angular/io/src/app/settings/releases/releases.component.ts b/AMW_angular/io/src/app/settings/releases/releases.component.ts index 1a4f6d511..2610f8707 100644 --- a/AMW_angular/io/src/app/settings/releases/releases.component.ts +++ b/AMW_angular/io/src/app/settings/releases/releases.component.ts @@ -9,7 +9,7 @@ import { ReleaseEditComponent } from './release-edit.component'; import { Release } from './release'; import { ReleasesService } from './releases.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { AuthService } from '../../auth/auth.service'; +import { AuthService, isAllowed } from '../../auth/auth.service'; import { map, takeUntil } from 'rxjs/operators'; import { ReleaseDeleteComponent } from './release-delete.component'; import { ToastService } from '../../shared/elements/toast/toast.service'; @@ -72,17 +72,10 @@ export class ReleasesComponent implements OnInit { } private getUserPermissions() { - this.authService.getActionsForPermission('RELEASE').map((action) => { - if (action.indexOf('ALL') > -1) { - this.canDelete.set(true); - this.canEdit.set(true); - this.canCreate.set(true); - } else { - this.canCreate.set(action.indexOf('CREATE') > -1); - this.canEdit.set(action.indexOf('UPDATE') > -1); - this.canDelete.set(action.indexOf('DELETE') > -1); - } - }); + const actions = this.authService.getActionsForPermission('RELEASE'); + this.canCreate.set(actions.some((action) => isAllowed(action, 'CREATE'))); + this.canEdit.set(actions.some((action) => isAllowed(action, 'UPDATE'))); + this.canDelete.set(actions.some((action) => isAllowed(action, 'DELETE'))); } private getReleases() { diff --git a/AMW_angular/io/src/app/settings/tags/tags.component.ts b/AMW_angular/io/src/app/settings/tags/tags.component.ts index d8f6bde8e..4c0631994 100644 --- a/AMW_angular/io/src/app/settings/tags/tags.component.ts +++ b/AMW_angular/io/src/app/settings/tags/tags.component.ts @@ -2,7 +2,7 @@ import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { IconComponent } from '../../shared/icon/icon.component'; import { Subject } from 'rxjs'; -import { AuthService } from '../../auth/auth.service'; +import { AuthService, isAllowed } from '../../auth/auth.service'; import { ToastService } from '../../shared/elements/toast/toast.service'; import { TagsService } from './tags.service'; @@ -34,15 +34,9 @@ export class TagsComponent implements OnInit, OnDestroy { } private getUserPermissions() { - this.authService.getActionsForPermission('MANAGE_GLOBAL_TAGS').map((action) => { - if (action.indexOf('ALL') > -1) { - this.canCreate.set(true); - this.canDelete.set(true); - } else { - this.canCreate.set(action.indexOf('CREATE') > -1); - this.canDelete.set(action.indexOf('DELETE') > -1); - } - }); + const actions = this.authService.getActionsForPermission('MANAGE_GLOBAL_TAGS'); + this.canCreate.set(actions.some((action) => isAllowed(action, 'CREATE'))); + this.canDelete.set(actions.some((action) => isAllowed(action, 'DELETE'))); } addTag(): void { From f9709887b63bd1c9259a5cfb9857126d870c1d72 Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Fri, 23 Aug 2024 11:53:47 +0200 Subject: [PATCH 13/14] refactor: currying isAllowed to clean up callers --- AMW_angular/io/src/app/auth/auth.service.ts | 7 +++++-- .../deployment-parameter/deployment-parameter.component.ts | 4 ++-- .../io/src/app/settings/releases/releases.component.ts | 6 +++--- AMW_angular/io/src/app/settings/tags/tags.component.ts | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/AMW_angular/io/src/app/auth/auth.service.ts b/AMW_angular/io/src/app/auth/auth.service.ts index e98d58b86..23b8e518f 100644 --- a/AMW_angular/io/src/app/auth/auth.service.ts +++ b/AMW_angular/io/src/app/auth/auth.service.ts @@ -45,6 +45,9 @@ export class AuthService extends BaseService { } } -export function isAllowed(action: string, role: string) { - return action === 'ALL' || action === role; +// currying function which verifies roles in a action +export function isAllowed(role: string) { + return (action: string) => { + return action === 'ALL' || action === role; + }; } diff --git a/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts b/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts index 8e862d36b..5ae6f5dfc 100644 --- a/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts +++ b/AMW_angular/io/src/app/settings/deployment-parameter/deployment-parameter.component.ts @@ -44,8 +44,8 @@ export class DeploymentParameterComponent implements OnInit, OnDestroy { private getUserPermissions() { const actions = this.authService.getActionsForPermission('MANAGE_DEPLOYMENT_PARAMETER'); - this.canCreate.set(actions.some((action) => isAllowed(action, 'CREATE'))); - this.canDelete.set(actions.some((action) => isAllowed(action, 'DELETE'))); + this.canCreate.set(actions.some(isAllowed('CREATE'))); + this.canDelete.set(actions.some(isAllowed('DELETE'))); } addKey(): void { diff --git a/AMW_angular/io/src/app/settings/releases/releases.component.ts b/AMW_angular/io/src/app/settings/releases/releases.component.ts index 2610f8707..dc8769dec 100644 --- a/AMW_angular/io/src/app/settings/releases/releases.component.ts +++ b/AMW_angular/io/src/app/settings/releases/releases.component.ts @@ -73,9 +73,9 @@ export class ReleasesComponent implements OnInit { private getUserPermissions() { const actions = this.authService.getActionsForPermission('RELEASE'); - this.canCreate.set(actions.some((action) => isAllowed(action, 'CREATE'))); - this.canEdit.set(actions.some((action) => isAllowed(action, 'UPDATE'))); - this.canDelete.set(actions.some((action) => isAllowed(action, 'DELETE'))); + this.canCreate.set(actions.some(isAllowed('CREATE'))); + this.canEdit.set(actions.some(isAllowed('UPDATE'))); + this.canDelete.set(actions.some(isAllowed('DELETE'))); } private getReleases() { diff --git a/AMW_angular/io/src/app/settings/tags/tags.component.ts b/AMW_angular/io/src/app/settings/tags/tags.component.ts index 4c0631994..014e899ac 100644 --- a/AMW_angular/io/src/app/settings/tags/tags.component.ts +++ b/AMW_angular/io/src/app/settings/tags/tags.component.ts @@ -35,8 +35,8 @@ export class TagsComponent implements OnInit, OnDestroy { private getUserPermissions() { const actions = this.authService.getActionsForPermission('MANAGE_GLOBAL_TAGS'); - this.canCreate.set(actions.some((action) => isAllowed(action, 'CREATE'))); - this.canDelete.set(actions.some((action) => isAllowed(action, 'DELETE'))); + this.canCreate.set(actions.some(isAllowed('CREATE'))); + this.canDelete.set(actions.some(isAllowed('DELETE'))); } addTag(): void { From d1d6ec4161a39a42421a7bf52c32ee81cd0d9ff3 Mon Sep 17 00:00:00 2001 From: Stephan Girod Date: Tue, 27 Aug 2024 11:34:32 +0200 Subject: [PATCH 14/14] doc: update coding-guidelines --- AMW_angular/io/coding-guidelines.md | 25 +++++++++++++++++++++ AMW_angular/io/src/app/auth/auth.service.ts | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/AMW_angular/io/coding-guidelines.md b/AMW_angular/io/coding-guidelines.md index 1759d91d1..023f9d09f 100644 --- a/AMW_angular/io/coding-guidelines.md +++ b/AMW_angular/io/coding-guidelines.md @@ -1,5 +1,6 @@ # Coding Guidelines + ## Inject() over Constructor-Injections Use `inject()` instead of constuctor-injection to make the code more explicit and obvious. @@ -14,6 +15,9 @@ constructor( ) {} ``` +## RxJS +Leverage RxJS for API calls, web sockets, and complex data flows, especially when handling multiple asynchronous events. Combine with Signals to simplify component state management. + ## Signals Use signals for changing values in Components @@ -49,3 +53,24 @@ private users$ = this.http.get(this.userUrl).pipe(tap((users) => this.us // only used to automatically un-/subscribe to the observable readOnlyUsers = toSignal(this.users$, { initialValue: [] as User[]}); ``` + +## Auth Service + +The frontend provides a singelton auth-service which holds all restrictions for the current user. + +After injecting the service in your component you can get Permissions/Actions depending on your needs: + +```typescript +// inject the service +authService = inject(AuthService); + +// get actions for a specific permission +const actions = this.authService.getActionsForPermission('MY_PERMISSION'); + +// verify role in an action and set signal +this.canCreate.set(actions.some(isAllowed('CREATE'))); + +// or directly set signal based on a concret permission and action value +this.canViewSettings.set(this.authService.hasPermission('SETTINGS', 'READ')); + +``` diff --git a/AMW_angular/io/src/app/auth/auth.service.ts b/AMW_angular/io/src/app/auth/auth.service.ts index 23b8e518f..29de855c3 100644 --- a/AMW_angular/io/src/app/auth/auth.service.ts +++ b/AMW_angular/io/src/app/auth/auth.service.ts @@ -45,7 +45,8 @@ export class AuthService extends BaseService { } } -// currying function which verifies roles in a action +// curried function to verify a role in an action +// usage example: actions.some(isAllowed("CREATE")) export function isAllowed(role: string) { return (action: string) => { return action === 'ALL' || action === role;