diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c4f15f3f..0030c76a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: test and build on: push: - branches: [ master ] + branches: [ master, development ] pull_request: - branches: [ master ] + branches: [ master, development ] jobs: build_and_test: diff --git a/AMW_angular/io/.eslintrc.json b/AMW_angular/io/.eslintrc.json index d77d6de87..fd4ffce00 100644 --- a/AMW_angular/io/.eslintrc.json +++ b/AMW_angular/io/.eslintrc.json @@ -16,7 +16,7 @@ "error", { "type": "attribute", - "prefix": "amw", + "prefix": "app", "style": "camelCase" } ], @@ -24,7 +24,7 @@ "error", { "type": "element", - "prefix": "amw", + "prefix": "app", "style": "kebab-case" } ] diff --git a/AMW_angular/io/angular.json b/AMW_angular/io/angular.json index c3bffc589..fcc3ccb93 100644 --- a/AMW_angular/io/angular.json +++ b/AMW_angular/io/angular.json @@ -33,13 +33,7 @@ } ], "styles": ["src/styles.scss", "./node_modules/bootstrap-icons/font/bootstrap-icons.scss"], - "scripts": [ - "node_modules/codemirror/lib/codemirror.js", - "node_modules/codemirror/addon/mode/simple.js", - "node_modules/codemirror/addon/dialog/dialog.js", - "node_modules/codemirror/addon/search/searchcursor.js", - "node_modules/codemirror/addon/search/search.js" - ], + "scripts": [], "extractLicenses": false, "sourceMap": true, "optimization": true, diff --git a/AMW_angular/io/coding-guidelines.md b/AMW_angular/io/coding-guidelines.md index 023f9d09f..285ff09fb 100644 --- a/AMW_angular/io/coding-guidelines.md +++ b/AMW_angular/io/coding-guidelines.md @@ -1,9 +1,8 @@ # Coding Guidelines - ## Inject() over Constructor-Injections -Use `inject()` instead of constuctor-injection to make the code more explicit and obvious. +Use `inject()` instead of constructor-injection to make the code more explicit and obvious. ```typescript // use @@ -16,39 +15,40 @@ 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 +Use Signals for changing values in Components ### Signal from Observable #### GET -Make API-requests with observables and expose the state as a signal: +Make API-requests with observables and expose the state as a Signal: ```typescript -// retrive data from API using RxJS +// retrieve data from API using RxJS private users$ = this.http.get(this.userUrl); -// expose as signal +// 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) +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: +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 +// retrieve 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[]}); @@ -56,7 +56,7 @@ readOnlyUsers = toSignal(this.users$, { initialValue: [] as User[]}); ## Auth Service -The frontend provides a singelton auth-service which holds all restrictions for the current user. +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: @@ -67,10 +67,9 @@ authService = inject(AuthService); // get actions for a specific permission const actions = this.authService.getActionsForPermission('MY_PERMISSION'); -// verify role in an action and set signal +// 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 +// or directly set Signal based on a concrete permission and action value this.canViewSettings.set(this.authService.hasPermission('SETTINGS', 'READ')); - ``` diff --git a/AMW_angular/io/package-lock.json b/AMW_angular/io/package-lock.json index 67acacefa..181061ca4 100644 --- a/AMW_angular/io/package-lock.json +++ b/AMW_angular/io/package-lock.json @@ -17,13 +17,14 @@ "@angular/platform-browser": "^18.0.5", "@angular/platform-browser-dynamic": "^18.0.5", "@angular/router": "^18.0.5", - "@ctrl/ngx-codemirror": "^7.0.0", + "@codemirror/merge": "^6.0.0", + "@codemirror/theme-one-dark": "^6.0.0", "@ng-bootstrap/ng-bootstrap": "^17.0.0", "@ng-select/ng-select": "^13.3.0", "@popperjs/core": "^2.11.8", "bootstrap": "5.3.2", "bootstrap-icons": "^1.11.1", - "codemirror": "^5.65.15", + "codemirror": "^6.0.0", "date-fns": "^2.30.0", "rxjs": "^7.8.1", "tslib": "^2.6.2", @@ -2422,6 +2423,114 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz", + "integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz", + "integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz", + "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz", + "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/merge": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@codemirror/merge/-/merge-6.7.2.tgz", + "integrity": "sha512-HSzuWoV4E+F0DROOSwGZMYIDXh+y4iA64ffRADXPBbKKSwx9bsYNM4i7qN8t0mc8H0PYNBoehOvsW2Nitmnx9g==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/highlight": "^1.0.0", + "style-mod": "^4.1.0" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", + "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==", + "license": "MIT" + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.34.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.1.tgz", + "integrity": "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2455,20 +2564,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@ctrl/ngx-codemirror": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@ctrl/ngx-codemirror/-/ngx-codemirror-7.0.0.tgz", - "integrity": "sha512-qvIWtSTw/8fdXDnofBTX6LmTW9646HhawG2+Qyagf1vH40jCy0ZbHnkC20UYOVpUX+QCd1e/PQpkvWQ/1iGFzQ==", - "dependencies": { - "@types/codemirror": "^5.60.7", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/core": ">=16.0.0-0", - "@angular/forms": ">=16.0.0-0", - "codemirror": "^5.65.9" - } - }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -3640,6 +3735,30 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@listr2/prompt-adapter-inquirer": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.13.tgz", @@ -4967,14 +5086,6 @@ "@types/qs": "*" } }, - "node_modules/@types/codemirror": { - "version": "5.60.15", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", - "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==", - "dependencies": { - "@types/tern": "*" - } - }, "node_modules/@types/command-line-args": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", @@ -5072,7 +5183,8 @@ "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true }, "node_modules/@types/express": { "version": "4.17.21", @@ -5297,14 +5409,6 @@ "@types/node": "*" } }, - "node_modules/@types/tern": { - "version": "0.23.9", - "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", - "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", @@ -7854,9 +7958,19 @@ } }, "node_modules/codemirror": { - "version": "5.65.17", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.17.tgz", - "integrity": "sha512-1zOsUx3lzAOu/gnMAZkQ9kpIHcPYOc9y1Fbm2UVk5UBPkdq380nhkelG0qUwm1f7wPvTbndu9ZYlug35EwAZRQ==" + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } }, "node_modules/color-convert": { "version": "1.9.3", @@ -8292,6 +8406,12 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/critters": { "version": "0.0.24", "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.24.tgz", @@ -16723,6 +16843,12 @@ "node": ">=4" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -17676,6 +17802,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", diff --git a/AMW_angular/io/package.json b/AMW_angular/io/package.json index 6ebaa6cd9..4ec060e42 100644 --- a/AMW_angular/io/package.json +++ b/AMW_angular/io/package.json @@ -26,13 +26,14 @@ "@angular/platform-browser": "^18.0.5", "@angular/platform-browser-dynamic": "^18.0.5", "@angular/router": "^18.0.5", - "@ctrl/ngx-codemirror": "^7.0.0", + "@codemirror/merge": "^6.0.0", + "@codemirror/theme-one-dark": "^6.0.0", "@ng-bootstrap/ng-bootstrap": "^17.0.0", "@ng-select/ng-select": "^13.3.0", "@popperjs/core": "^2.11.8", "bootstrap": "5.3.2", "bootstrap-icons": "^1.11.1", - "codemirror": "^5.65.15", + "codemirror": "^6.0.0", "date-fns": "^2.30.0", "rxjs": "^7.8.1", "tslib": "^2.6.2", diff --git a/AMW_angular/io/src/app/app.component.spec.ts b/AMW_angular/io/src/app/app.component.spec.ts index 7b5a06b20..3b08a4c16 100644 --- a/AMW_angular/io/src/app/app.component.spec.ts +++ b/AMW_angular/io/src/app/app.component.spec.ts @@ -1,7 +1,6 @@ import { ChangeDetectorRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; -import { RouterTestingModule } from '@angular/router/testing'; +import { provideRouter, Router, withHashLocation } from '@angular/router'; import { of } from 'rxjs'; import { AppComponent } from './app.component'; import { NavigationComponent } from './navigation/navigation.component'; @@ -10,6 +9,7 @@ import { AppConfiguration } from './setting/app-configuration'; import { SettingService } from './setting/setting.service'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { routes } from './app.routes'; class RouterStub { navigateByUrl(url: string) { @@ -18,17 +18,23 @@ class RouterStub { } describe('App', () => { - let router: Router; let app: AppComponent; let fixture: ComponentFixture; let settingService: SettingService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [RouterTestingModule, NavigationComponent, AppComponent], - providers: [SettingService, ChangeDetectorRef, AppComponent, { provide: Router, useClass: RouterStub }, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}).compileComponents(); - router = TestBed.inject(Router); + imports: [NavigationComponent, AppComponent], + providers: [ + SettingService, + ChangeDetectorRef, + AppComponent, + { provide: Router, useClass: RouterStub }, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + provideRouter(routes, withHashLocation()), + ], + }).compileComponents(); settingService = TestBed.inject(SettingService); fixture = TestBed.createComponent(AppComponent); @@ -54,7 +60,6 @@ describe('App', () => { it('should set empty logoutUrl if config not found', () => { // given - const expectedKey: string = 'logoutUrl'; const expectedValue: string = ''; const appConf: AppConfiguration = { key: { value: 'test', env: 'TEST' }, @@ -63,6 +68,6 @@ describe('App', () => { app.ngOnInit(); - expect(app.logoutUrl).toEqual(''); + expect(app.logoutUrl).toEqual(expectedValue); }); }); diff --git a/AMW_angular/io/src/app/app.component.ts b/AMW_angular/io/src/app/app.component.ts index c72601d1c..2c0f43850 100644 --- a/AMW_angular/io/src/app/app.component.ts +++ b/AMW_angular/io/src/app/app.component.ts @@ -18,9 +18,7 @@ import { ToastContainerComponent } from './shared/elements/toast/toast-container export class AppComponent implements OnInit { logoutUrl: string; - constructor( - private settingService: SettingService - ) { } + constructor(private settingService: SettingService) {} ngOnInit(): void { this.settingService.getAllAppSettings().subscribe((r) => this.configureSettings(r)); diff --git a/AMW_angular/io/src/app/app.routes.ts b/AMW_angular/io/src/app/app.routes.ts index 3c9cc8025..11e65f3c0 100644 --- a/AMW_angular/io/src/app/app.routes.ts +++ b/AMW_angular/io/src/app/app.routes.ts @@ -1,14 +1,20 @@ import { Routes } from '@angular/router'; -import { DeploymentsComponent } from './deployments/deployments.component'; +import { appsRoutes } from './apps/apps.routes'; import { auditviewRoutes } from './auditview/auditview.routes'; import { deploymentRoutes } from './deployment/deployment-routes'; import { settingsRoutes } from './settings/settings.routes'; import { deploymentsRoutes } from './deployments/deployments.routes'; +import { serversRoute } from './servers/servers.route'; +import { resourcesRoute } from './resources/resources.route'; +import { AppsComponent } from './apps/apps.component'; export const routes: Routes = [ // default route only, the rest is done in module routing - { path: '', component: DeploymentsComponent }, + { path: '', component: AppsComponent }, + ...appsRoutes, + ...serversRoute, + ...resourcesRoute, ...settingsRoutes, ...auditviewRoutes, ...deploymentRoutes, diff --git a/AMW_angular/io/src/app/apps/app-add/app-add.component.html b/AMW_angular/io/src/app/apps/app-add/app-add.component.html new file mode 100644 index 000000000..bd499858a --- /dev/null +++ b/AMW_angular/io/src/app/apps/app-add/app-add.component.html @@ -0,0 +1,44 @@ + + + diff --git a/AMW_angular/io/src/app/apps/app-add/app-add.component.spec.ts b/AMW_angular/io/src/app/apps/app-add/app-add.component.spec.ts new file mode 100644 index 000000000..88f171596 --- /dev/null +++ b/AMW_angular/io/src/app/apps/app-add/app-add.component.spec.ts @@ -0,0 +1,15 @@ +import { AppAddComponent } from './app-add.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +describe('AppAddComponent', () => { + let component: AppAddComponent; + const activeModal = new NgbActiveModal(); + + beforeEach(async () => { + component = new AppAddComponent(activeModal); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/apps/app-add/app-add.component.ts b/AMW_angular/io/src/app/apps/app-add/app-add.component.ts new file mode 100644 index 000000000..046b0e61e --- /dev/null +++ b/AMW_angular/io/src/app/apps/app-add/app-add.component.ts @@ -0,0 +1,63 @@ +import { Component, EventEmitter, Input, Output, Signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { Release } from '../../settings/releases/release'; +import { Release as Rel } from '../../resource/release'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { Resource } from '../../resource/resource'; +import { AppCreate } from '../app-create'; +import { ModalHeaderComponent } from '../../shared/modal-header/modal-header.component'; +import { ButtonComponent } from '../../shared/button/button.component'; + +@Component({ + selector: 'app-app-add', + standalone: true, + imports: [FormsModule, NgSelectModule, ModalHeaderComponent, ButtonComponent], + templateUrl: './app-add.component.html', +}) +export class AppAddComponent { + @Input() releases: Signal; + @Input() appServerGroups: Signal; + @Output() saveApp: EventEmitter = new EventEmitter(); + + app: AppCreate = { appName: '', appReleaseId: null, appServerId: null, appServerReleaseId: null }; + appServerGroup: Resource; + appServerRelease: Rel; + + constructor(public activeModal: NgbActiveModal) { + this.activeModal = activeModal; + } + + hasInvalidGroup(): boolean { + const isInvalid = + this.appServerGroup === undefined || this.appServerGroup === null || this.appServerGroup?.releases.length === 0; + if (isInvalid) { + this.appServerRelease = undefined; + } + return isInvalid; + } + + // apps without appserver are valid too + hasInvalidFields(): boolean { + return ( + this.app.appName === '' || + this.app.appReleaseId === null || + (!this.hasInvalidGroup() && (this.appServerRelease === undefined || this.appServerRelease === null)) + ); + } + + cancel() { + this.activeModal.close(); + } + + save() { + const app: AppCreate = { + appName: this.app.appName, + appReleaseId: this.app.appReleaseId, + appServerId: this.appServerGroup?.id, + appServerReleaseId: this.appServerRelease?.id, + }; + this.saveApp.emit(app); + this.activeModal.close(); + } +} diff --git a/AMW_angular/io/src/app/apps/app-create.ts b/AMW_angular/io/src/app/apps/app-create.ts new file mode 100644 index 000000000..b9dd628d2 --- /dev/null +++ b/AMW_angular/io/src/app/apps/app-create.ts @@ -0,0 +1,6 @@ +export interface AppCreate { + appName: string; + appReleaseId: number; + appServerId: number; + appServerReleaseId: number; +} diff --git a/AMW_angular/io/src/app/apps/app-server-add/app-server-add.component.html b/AMW_angular/io/src/app/apps/app-server-add/app-server-add.component.html new file mode 100644 index 000000000..90694cb84 --- /dev/null +++ b/AMW_angular/io/src/app/apps/app-server-add/app-server-add.component.html @@ -0,0 +1,21 @@ + + + diff --git a/AMW_angular/io/src/app/apps/app-server-add/app-server-add.component.spec.ts b/AMW_angular/io/src/app/apps/app-server-add/app-server-add.component.spec.ts new file mode 100644 index 000000000..537cd92c5 --- /dev/null +++ b/AMW_angular/io/src/app/apps/app-server-add/app-server-add.component.spec.ts @@ -0,0 +1,15 @@ +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { AppServerAddComponent } from './app-server-add.component'; + +describe('AppServerAddComponent', () => { + let component: AppServerAddComponent; + const activeModal = new NgbActiveModal(); + + beforeEach(async () => { + component = new AppServerAddComponent(activeModal); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/apps/app-server-add/app-server-add.component.ts b/AMW_angular/io/src/app/apps/app-server-add/app-server-add.component.ts new file mode 100644 index 000000000..f29a1302a --- /dev/null +++ b/AMW_angular/io/src/app/apps/app-server-add/app-server-add.component.ts @@ -0,0 +1,46 @@ +import { Component, EventEmitter, Input, Output, Signal } from '@angular/core'; +import { Release } from '../../settings/releases/release'; +import { AppServer } from '../app-server'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { FormsModule } from '@angular/forms'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { ModalHeaderComponent } from '../../shared/modal-header/modal-header.component'; +import { ButtonComponent } from '../../shared/button/button.component'; + +@Component({ + selector: 'app-server-add', + standalone: true, + imports: [FormsModule, NgSelectModule, ModalHeaderComponent, ButtonComponent], + templateUrl: './app-server-add.component.html', +}) +export class AppServerAddComponent { + @Input() releases: Signal; + @Output() saveAppServer: EventEmitter = new EventEmitter(); + + appServer: AppServer = { name: '', apps: [], deletable: false, id: null, runtimeName: '', release: null }; + + constructor(public activeModal: NgbActiveModal) { + this.activeModal = activeModal; + } + + hasInvalidFields(): boolean { + return this.appServer.name === '' || this.appServer.release?.id === null || this.appServer.release?.name === ''; + } + + cancel() { + this.activeModal.close(); + } + + save() { + const appServer: AppServer = { + name: this.appServer.name, + release: this.appServer.release, + deletable: this.appServer.deletable, + id: this.appServer.id, + runtimeName: this.appServer.runtimeName, + apps: this.appServer.apps, + }; + this.saveAppServer.emit(appServer); + this.activeModal.close(); + } +} diff --git a/AMW_angular/io/src/app/apps/app-server.ts b/AMW_angular/io/src/app/apps/app-server.ts new file mode 100644 index 000000000..dd80acef9 --- /dev/null +++ b/AMW_angular/io/src/app/apps/app-server.ts @@ -0,0 +1,11 @@ +import { Release } from '../settings/releases/release'; +import { App } from './app'; + +export interface AppServer { + id: number; + name: string; + deletable: boolean; + runtimeName: string; + release: Release; + apps: App[]; +} diff --git a/AMW_angular/io/src/app/apps/app.ts b/AMW_angular/io/src/app/apps/app.ts new file mode 100644 index 000000000..a75f9c0b4 --- /dev/null +++ b/AMW_angular/io/src/app/apps/app.ts @@ -0,0 +1,7 @@ +import { Release } from '../settings/releases/release'; + +export interface App { + id: number; + name: string; + release: Release; +} diff --git a/AMW_angular/io/src/app/apps/apps-filter/apps-filter.component.html b/AMW_angular/io/src/app/apps/apps-filter/apps-filter.component.html new file mode 100644 index 000000000..f288f5266 --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps-filter/apps-filter.component.html @@ -0,0 +1,29 @@ +
+
+
+ Add Filter +
+
+
+
+ + +
+
+ + + @for (release of selection().releases; track release.id) { + {{ release.name }} + } + +
+
+ Search +
+
+
diff --git a/AMW_angular/io/src/app/apps/apps-filter/apps-filter.component.spec.ts b/AMW_angular/io/src/app/apps/apps-filter/apps-filter.component.spec.ts new file mode 100644 index 000000000..f2c34c7e2 --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps-filter/apps-filter.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AppsFilterComponent } from './apps-filter.component'; +import { InputSignal, signal } from '@angular/core'; +import { Release } from '../../settings/releases/release'; + +describe('AppsFilterComponent', () => { + let component: AppsFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppsFilterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AppsFilterComponent); + component = fixture.componentInstance; + component.releases = signal([]) as unknown as InputSignal; + component.upcoming = signal(0) as unknown as InputSignal; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/apps/apps-filter/apps-filter.component.ts b/AMW_angular/io/src/app/apps/apps-filter/apps-filter.component.ts new file mode 100644 index 000000000..6d2c0976e --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps-filter/apps-filter.component.ts @@ -0,0 +1,33 @@ +import { ChangeDetectionStrategy, Component, computed, input, output, signal } from '@angular/core'; + +import { NgSelectModule } from '@ng-select/ng-select'; +import { Release } from '../../settings/releases/release'; +import { FormsModule } from '@angular/forms'; +import { ButtonComponent } from '../../shared/button/button.component'; + +@Component({ + selector: 'app-apps-filter', + standalone: true, + imports: [FormsModule, NgSelectModule, ButtonComponent], + templateUrl: './apps-filter.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppsFilterComponent { + releases = input.required(); + upcoming = input.required(); + + selection = computed(() => { + return { + releases: this.releases(), + selected: signal(this.upcoming()), + }; + }); + + appName = signal(''); + + filterEvent = output<{ filter: string; releaseId: number }>(); + + search() { + this.filterEvent.emit({ filter: this.appName(), releaseId: this.selection().selected() }); + } +} diff --git a/AMW_angular/io/src/app/apps/apps-list/apps-list-component.ts b/AMW_angular/io/src/app/apps/apps-list/apps-list-component.ts new file mode 100644 index 000000000..f523b43ec --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps-list/apps-list-component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; +import { AsyncPipe } from '@angular/common'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; +import { App } from '../app'; + +@Component({ + selector: 'app-apps-list', + standalone: true, + imports: [AsyncPipe, PaginationComponent], + templateUrl: './apps-list.component.html', + styleUrl: './apps-list.component.scss', +}) +export class AppsListComponent { + @Input() apps: App[]; + @Input() even: boolean; +} diff --git a/AMW_angular/io/src/app/apps/apps-list/apps-list.component.html b/AMW_angular/io/src/app/apps/apps-list/apps-list.component.html new file mode 100644 index 000000000..e75302fe3 --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps-list/apps-list.component.html @@ -0,0 +1,16 @@ +@if (apps && apps.length > 0) { + + + @for (app of apps; track app) { + + + + + } + +
+ {{ app.name }} + + {{ app.release.name }} +
+} diff --git a/AMW_angular/io/src/app/apps/apps-list/apps-list.component.scss b/AMW_angular/io/src/app/apps/apps-list/apps-list.component.scss new file mode 100644 index 000000000..d6d827123 --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps-list/apps-list.component.scss @@ -0,0 +1,6 @@ +table .w-10 { + width: 10%; +} +table .w-90 { + width: 90%; +} diff --git a/AMW_angular/io/src/app/apps/apps-list/apps-list.component.spec.ts b/AMW_angular/io/src/app/apps/apps-list/apps-list.component.spec.ts new file mode 100644 index 000000000..2c95fe1fb --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps-list/apps-list.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AppsListComponent } from './apps-list-component'; + +describe('AppsListComponent', () => { + let component: AppsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppsListComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AppsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.html b/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.html new file mode 100644 index 000000000..e7bbd1e9e --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.html @@ -0,0 +1,39 @@ +
+ @if (appServers && appServers.length > 0) { + + + + + + + + + @for (appServer of appServers; track appServer; let even = $even) { + + + + + + @if (appServer.apps && appServer.apps.length > 0) { + + + + } } + +
App NameRelease
+ @if (!appServer.deletable) { +
{{ appServer.name }}
+ } @else { + {{ appServer.name }} [ + {{ appServer.runtimeName }} ] } +
+ {{ + appServer.release.name + }} +
+ +
+ } @else { +
No results found in database
+ } +
diff --git a/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.scss b/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.scss new file mode 100644 index 000000000..6d505a116 --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.scss @@ -0,0 +1,11 @@ +table .w-10 { + width: 10%; +} +table .w-90 { + width: 90%; +} + +.empty-container { + padding: 1em; + text-align: center; +} diff --git a/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.spec.ts b/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.spec.ts new file mode 100644 index 000000000..0f5284f14 --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AppsServersListComponent } from './apps-servers-list.component'; + +describe('AppsListComponent', () => { + let component: AppsServersListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppsServersListComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AppsServersListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.ts b/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.ts new file mode 100644 index 000000000..2cbe6ebae --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps-servers-list/apps-servers-list.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; +import { AsyncPipe } from '@angular/common'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; +import { AppServer } from '../app-server'; +import { AppsListComponent } from '../apps-list/apps-list-component'; + +@Component({ + selector: 'app-apps-servers-list', + standalone: true, + imports: [AsyncPipe, AppsListComponent, PaginationComponent], + templateUrl: './apps-servers-list.component.html', + styleUrl: './apps-servers-list.component.scss', +}) +export class AppsServersListComponent { + @Input() appServers: AppServer[]; +} diff --git a/AMW_angular/io/src/app/apps/apps.component.html b/AMW_angular/io/src/app/apps/apps.component.html new file mode 100644 index 000000000..c5b821a3f --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps.component.html @@ -0,0 +1,58 @@ + + +
Apps
+
+
+
+
+
+
+
+ Application servers and applications +
+
+ @if (permissions().canCreateApp) { + + + Add Application + + } @if (permissions().canCreateAppServer) { + + + Add Application Server + + } +
+
+
+ @if (permissions().canViewAppList) { +
+ + +
+ + } +
+
+
+
+
diff --git a/AMW_angular/io/src/app/apps/apps.component.spec.ts b/AMW_angular/io/src/app/apps/apps.component.spec.ts new file mode 100644 index 000000000..01d44bdf1 --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AppsComponent } from './apps.component'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; + +describe('AppsComponent', () => { + let component: AppsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppsComponent], + providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + }).compileComponents(); + + fixture = TestBed.createComponent(AppsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/apps/apps.component.ts b/AMW_angular/io/src/app/apps/apps.component.ts new file mode 100644 index 000000000..88dc877a1 --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps.component.ts @@ -0,0 +1,177 @@ +import { ChangeDetectionStrategy, Component, computed, inject, OnDestroy, OnInit, signal, Signal } from '@angular/core'; +import { BehaviorSubject, skip, Subject, take } from 'rxjs'; +import { LoadingIndicatorComponent } from '../shared/elements/loading-indicator.component'; +import { AsyncPipe } from '@angular/common'; +import { IconComponent } from '../shared/icon/icon.component'; +import { PageComponent } from '../layout/page/page.component'; +import { AppsFilterComponent } from './apps-filter/apps-filter.component'; +import { AuthService } from '../auth/auth.service'; +import { ReleasesService } from '../settings/releases/releases.service'; +import { AppsService } from './apps.service'; +import { Release } from '../settings/releases/release'; +import { takeUntil } from 'rxjs/operators'; +import { ToastService } from '../shared/elements/toast/toast.service'; +import { AppServer } from './app-server'; +import { AppsServersListComponent } from './apps-servers-list/apps-servers-list.component'; +import { PaginationComponent } from '../shared/pagination/pagination.component'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { AppServerAddComponent } from './app-server-add/app-server-add.component'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { AppAddComponent } from './app-add/app-add.component'; +import { ResourceService } from '../resource/resource.service'; +import { Resource } from '../resource/resource'; +import { AppCreate } from './app-create'; +import { ButtonComponent } from '../shared/button/button.component'; + +@Component({ + selector: 'app-apps', + standalone: true, + imports: [ + AppsFilterComponent, + AppsServersListComponent, + AsyncPipe, + IconComponent, + LoadingIndicatorComponent, + PageComponent, + PaginationComponent, + ButtonComponent, + ], + templateUrl: './apps.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppsComponent implements OnInit, OnDestroy { + private appsService = inject(AppsService); + private authService = inject(AuthService); + private modalService = inject(NgbModal); + private releaseService = inject(ReleasesService); // getCount -> getReleases(0, count) + private resourceService = inject(ResourceService); + private toastService = inject(ToastService); + + upcomingRelease: Signal = toSignal(this.releaseService.getUpcomingRelease()); + + releases: Signal = toSignal(this.releaseService.getReleases(0, 50), { initialValue: [] as Release[] }); + appServerGroups = toSignal(this.resourceService.getByType('APPLICATIONSERVER'), { + initialValue: [] as Resource[], + }); + appServers = this.appsService.apps; + count = this.appsService.count; + maxResults = this.appsService.limit; + offset = this.appsService.offset; + filter = this.appsService.filter; + releaseId = this.appsService.releaseId; + private error$ = new BehaviorSubject(''); + private destroy$ = new Subject(); + + showLoader = signal(false); + isLoading = computed(() => { + return this.appServers() === undefined || this.showLoader(); + }); + + permissions = computed(() => { + if (this.authService.restrictions().length > 0) { + return { + canCreateApp: this.authService.hasResourcePermission('RESOURCE', 'CREATE', 'APPLICATION'), + canCreateAppServer: this.authService.hasResourcePermission('RESOURCE', 'CREATE', 'APPLICATIONSERVER'), + canViewAppList: this.authService.hasPermission('APP_AND_APPSERVER_LIST', 'READ'), + }; + } else { + return { canCreateApp: false, canCreateAppServer: false, canViewAppList: false }; + } + }); + + currentPage = computed(() => Math.floor(this.offset() / this.maxResults()) + 1); + lastPage = computed(() => Math.ceil(this.count() / this.maxResults())); + + constructor() { + toObservable(this.upcomingRelease) + .pipe(takeUntil(this.destroy$), skip(1), take(1)) + .subscribe((release) => { + this.releaseId.set(release.id); + this.appsService.refreshData(); + }); + } + + ngOnInit(): void { + this.error$.pipe(takeUntil(this.destroy$)).subscribe((msg) => { + msg !== '' ? this.toastService.error(msg) : null; + }); + } + + addApp() { + const modalRef = this.modalService.open(AppAddComponent); + modalRef.componentInstance.releases = this.releases; + modalRef.componentInstance.appServerGroups = this.appServerGroups; + modalRef.componentInstance.saveApp.pipe(takeUntil(this.destroy$)).subscribe((app: AppCreate) => this.saveApp(app)); + } + + addServer() { + const modalRef = this.modalService.open(AppServerAddComponent); + modalRef.componentInstance.releases = this.releases; + modalRef.componentInstance.saveAppServer + .pipe(takeUntil(this.destroy$)) + .subscribe((appServer: AppServer) => this.saveAppServer(appServer)); + } + + saveAppServer(appServer: AppServer) { + this.showLoader.set(true); + this.appsService + .createAppServer(appServer) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => { + this.toastService.success('AppServer saved successfully.'); + }, + error: (e) => { + this.error$.next(e.toString()); + }, + complete: () => { + this.appsService.refreshData(); + }, + }); + } + + saveApp(app: AppCreate) { + this.showLoader.set(true); + this.appsService + .createApp(app) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => this.toastService.success('App saved successfully.'), + error: (e) => this.error$.next(e.toString()), + complete: () => { + this.appsService.refreshData(); + }, + }); + } + + setMaxResultsPerPage(max: number) { + this.maxResults.set(max); + this.offset.set(0); + this.appsService.refreshData(); + } + + setNewOffset(offset: number) { + this.offset.set(offset); + this.appsService.refreshData(); + } + + updateFilter(values: { filter: string; releaseId: number }) { + let update = false; + if (values.filter !== undefined && this.filter() !== values.filter) { + this.filter.set(values.filter); + update = true; + } + + if (values.releaseId > 0 && this.releaseId() !== values.releaseId) { + this.releaseId.set(values.releaseId); + update = true; + } + if (update) { + this.appsService.refreshData(); + } + } + + ngOnDestroy(): void { + this.destroy$.next(undefined); + } +} diff --git a/AMW_angular/io/src/app/apps/apps.routes.ts b/AMW_angular/io/src/app/apps/apps.routes.ts new file mode 100644 index 000000000..ce28c07bb --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps.routes.ts @@ -0,0 +1,3 @@ +import { AppsComponent } from './apps.component'; + +export const appsRoutes = [{ path: 'apps', component: AppsComponent }]; diff --git a/AMW_angular/io/src/app/apps/apps.service.ts b/AMW_angular/io/src/app/apps/apps.service.ts new file mode 100644 index 000000000..7dd56ba18 --- /dev/null +++ b/AMW_angular/io/src/app/apps/apps.service.ts @@ -0,0 +1,94 @@ +import { inject, Injectable, signal, WritableSignal } from '@angular/core'; +import { BaseService } from '../base/base.service'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable, of, startWith, Subject } from 'rxjs'; +import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; +import { AppServer } from './app-server'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { AppCreate } from './app-create'; + +@Injectable({ providedIn: 'root' }) +export class AppsService extends BaseService { + private http = inject(HttpClient); + private appsUrl = `${this.getBaseUrl()}/apps`; + + private reload$ = new Subject(); + + offset = signal(0); + limit = signal(20); + filter = signal(null); + releaseId: WritableSignal = signal(undefined); + private apps$: Observable = this.reload$.pipe( + startWith(null), + switchMap(() => this.getApps(this.offset(), this.limit(), this.filter(), this.releaseId())), + shareReplay(1), + ); + count = signal(0); + apps = toSignal(this.apps$, { initialValue: [] as AppServer[] }); + + constructor() { + super(); + } + + refreshData() { + this.reload$.next([]); + } + + private getApps(offset: number, limit: number, filter: string, releaseId: number | undefined) { + if (!releaseId) return of([]); + + let urlParams = ''; + if (offset != null) { + urlParams = `start=${offset}&`; + } + + if (limit != null) { + urlParams += `limit=${limit}&`; + } + + if (filter != null) { + urlParams += `appServerName=${filter}&`; + } + + return this.http + .get(`${this.appsUrl}?${urlParams}releaseId=${releaseId}`, { + headers: this.getHeaders(), + observe: 'response', + }) + .pipe(catchError(this.handleError)) + .pipe( + map((response: HttpResponse) => { + if (response.body.length <= 0) { + this.count.set(0); + } else { + this.count.set(Number(response.headers.get('x-total-count'))); + } + return response.body; + }), + ); + } + + createAppServer(appServer: AppServer) { + return this.http + .post(`${this.appsUrl}/appServer?appServerName=${appServer.name}&releaseId=${appServer.release.id}`, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + createApp(app: AppCreate) { + if (app.appServerId) { + return this.http + .post(`${this.appsUrl}/appWithServer`, app, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + return this.http + .post(`${this.appsUrl}?appName=${app.appName}&releaseId=${app.appReleaseId}`, app, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } +} diff --git a/AMW_angular/io/src/app/auditview/auditview.component.ts b/AMW_angular/io/src/app/auditview/auditview.component.ts index 4467cbb0c..fa101595e 100644 --- a/AMW_angular/io/src/app/auditview/auditview.component.ts +++ b/AMW_angular/io/src/app/auditview/auditview.component.ts @@ -11,7 +11,7 @@ import { AuditviewTableService } from './auditview-table/auditview-table.service import { PageComponent } from '../layout/page/page.component'; @Component({ - selector: 'amw-auditview', + selector: 'app-auditview', templateUrl: './auditview.component.html', standalone: true, providers: [AuditviewService, AuditviewTableService, DatePipe], diff --git a/AMW_angular/io/src/app/auth/auth.service.ts b/AMW_angular/io/src/app/auth/auth.service.ts index 29de855c3..ef5593823 100644 --- a/AMW_angular/io/src/app/auth/auth.service.ts +++ b/AMW_angular/io/src/app/auth/auth.service.ts @@ -1,13 +1,15 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { BaseService } from '../base/base.service'; import { HttpClient } from '@angular/common/http'; import { Observable, startWith, Subject } from 'rxjs'; import { catchError, shareReplay, switchMap } from 'rxjs/operators'; import { Restriction } from '../settings/permission/restriction'; import { toSignal } from '@angular/core/rxjs-interop'; +import { DefaultResourceType } from './defaultResourceType'; @Injectable({ providedIn: 'root' }) export class AuthService extends BaseService { + private http = inject(HttpClient); private reload$ = new Subject(); private restrictions$ = this.reload$.pipe( startWith(null), @@ -16,7 +18,7 @@ export class AuthService extends BaseService { ); restrictions = toSignal(this.restrictions$, { initialValue: [] as Restriction[] }); - constructor(private http: HttpClient) { + constructor() { super(); } @@ -43,6 +45,22 @@ export class AuthService extends BaseService { this.getActionsForPermission(permissionName).find((value) => value === 'ALL' || value === action) !== undefined ); } + + hasResourcePermission(permissionName: string, action: string, resourceType: string): boolean { + return ( + this.restrictions() + .filter((entry) => entry.permission.name === permissionName) + .filter((entry) => entry.resourceTypeName === resourceType || this.isDefaultType(entry, resourceType)) + .map((entry) => entry.action) + .find((entry) => entry === 'ALL' || entry === action) !== undefined + ); + } + + private isDefaultType(entry: Restriction, resourceType: string) { + if (entry.resourceTypeName === null && entry.resourceTypePermission === 'DEFAULT_ONLY') { + return Object.keys(DefaultResourceType).find((key) => key === resourceType); + } else return false; + } } // curried function to verify a role in an action diff --git a/AMW_angular/io/src/app/auth/defaultResourceType.ts b/AMW_angular/io/src/app/auth/defaultResourceType.ts new file mode 100644 index 000000000..703cf24b4 --- /dev/null +++ b/AMW_angular/io/src/app/auth/defaultResourceType.ts @@ -0,0 +1,6 @@ +export enum DefaultResourceType { + APPLICATIONSERVER = 'Applicationserver', + APPLICATION = 'Application', + NODE = 'Node', + RUNTIME = 'Runtime', +} diff --git a/AMW_angular/io/src/app/core/amw-constants.ts b/AMW_angular/io/src/app/core/amw-constants.ts index 12697a286..27357536b 100644 --- a/AMW_angular/io/src/app/core/amw-constants.ts +++ b/AMW_angular/io/src/app/core/amw-constants.ts @@ -1,4 +1,9 @@ export const AMW_LOGOUT_URL = 'amw.logoutUrl'; +export const ENVIRONMENT = { + AMW_VM_DETAILS_URL: 'amw.vmDetailUrl', + AMW_VM_URL_PARAM: 'amw.vmUrlParam', +}; + // used for date-fns export const DATE_TIME_FORMAT = 'dd.MM.yyyy HH:mm'; export const DATE_FORMAT = 'dd.MM.yyyy'; diff --git a/AMW_angular/io/src/app/deployment/deployment-request.ts b/AMW_angular/io/src/app/deployment/deployment-request.ts index ca202aaa6..b9afe6106 100644 --- a/AMW_angular/io/src/app/deployment/deployment-request.ts +++ b/AMW_angular/io/src/app/deployment/deployment-request.ts @@ -7,8 +7,6 @@ export interface DeploymentRequest { releaseName: string; requestOnly: boolean; simulate: boolean; - executeShakedownTest: boolean; - neighbourhoodTest: boolean; sendEmail: boolean; appsWithVersion: AppWithVersion[]; stateToDeploy: number; diff --git a/AMW_angular/io/src/app/deployment/deployment.component.html b/AMW_angular/io/src/app/deployment/deployment.component.html index 80e29f4d1..0f1f4ff78 100644 --- a/AMW_angular/io/src/app/deployment/deployment.component.html +++ b/AMW_angular/io/src/app/deployment/deployment.component.html @@ -144,9 +144,7 @@
@if (transDeploymentParameter.key && transDeploymentParameter.value) { - + }
@@ -162,9 +160,9 @@
- +
} } @@ -180,50 +178,27 @@
- @if (hasPermissionShakedownTest) { -
- - -
- } @if (doExecuteShakedownTest) { -
- - -
- }
- -
diff --git a/AMW_angular/io/src/app/deployment/deployment.component.spec.ts b/AMW_angular/io/src/app/deployment/deployment.component.spec.ts index c4f7de033..3537023ef 100644 --- a/AMW_angular/io/src/app/deployment/deployment.component.spec.ts +++ b/AMW_angular/io/src/app/deployment/deployment.component.spec.ts @@ -89,7 +89,7 @@ describe('DeploymentComponent (create deployment)', () => { }); it('should populate groupedEnvironments on ngOnInit', () => { - const environments: Environment[] = [{ id: 1, name: 'A', parent: 'DEV' } as Environment]; + const environments: Environment[] = [{ id: 1, name: 'A', parentName: 'DEV' } as Environment]; spyOn(environmentService, 'getAll').and.returnValue(of(environments)); expect(component.groupedEnvironments).toEqual({}); component.ngOnInit(); @@ -151,9 +151,9 @@ describe('DeploymentComponent (create deployment)', () => { it('should return environementGroupNames on getEnvironmentGroups()', () => { const environments: Environment[] = [ - { id: 1, name: 'A', parent: 'DEV' } as Environment, - { id: 2, name: 'B', parent: 'DEV' } as Environment, - { id: 3, name: 'P', parent: 'PROD' } as Environment, + { id: 1, name: 'A', parentName: 'DEV' } as Environment, + { id: 2, name: 'B', parentName: 'DEV' } as Environment, + { id: 3, name: 'P', parentName: 'PROD' } as Environment, ]; spyOn(environmentService, 'getAll').and.returnValue(of(environments)); component.ngOnInit(); @@ -184,14 +184,11 @@ describe('DeploymentComponent (create deployment)', () => { const appServer: Resource = { name: 'testServer', id: 3 } as Resource; component.environments = [{ id: 1 } as Environment, { id: 2, selected: true } as Environment]; component.selectedAppserver = appServer; - spyOn(resourceService, 'canCreateShakedownTest').and.returnValue(of(false)); spyOn(deploymentService, 'canDeploy').and.returnValue(of(true)); // when component.onChangeAppserver(); // then - expect(resourceService.canCreateShakedownTest).toHaveBeenCalledWith(3); expect(deploymentService.canDeploy).toHaveBeenCalledWith(3, [2]); - expect(component.hasPermissionShakedownTest).toBeFalsy(); expect(component.hasPermissionToDeploy).toBeTruthy(); }); @@ -282,7 +279,6 @@ describe('DeploymentComponent (create deployment)', () => { component.selectedAppserver = { name: 'testServer' } as Resource; component.selectedRelease = { id: 1, release: 'testRelease' } as Release; component.environments = [{ id: 2, name: 'A' } as Environment, { id: 3, name: 'B', selected: true } as Environment]; - component.doExecuteShakedownTest = true; component.appsWithVersion = [ { applicationId: 4, @@ -305,8 +301,6 @@ describe('DeploymentComponent (create deployment)', () => { releaseName: 'testRelease', simulate: false, sendEmail: false, - executeShakedownTest: component.doExecuteShakedownTest, - neighbourhoodTest: false, requestOnly: true, appsWithVersion: component.appsWithVersion, stateToDeploy: component.selectedResourceTag.tagDate, @@ -329,7 +323,6 @@ describe('DeploymentComponent (create deployment)', () => { { id: 2, name: 'A', selected: true } as Environment, { id: 3, name: 'B', selected: true } as Environment, ]; - component.doExecuteShakedownTest = true; component.simulate = true; component.appsWithVersion = [ { @@ -357,8 +350,6 @@ describe('DeploymentComponent (create deployment)', () => { releaseName: 'testRelease', simulate: component.simulate, sendEmail: false, - executeShakedownTest: component.doExecuteShakedownTest, - neighbourhoodTest: false, requestOnly: false, appsWithVersion: component.appsWithVersion, stateToDeploy: component.selectedResourceTag.tagDate, @@ -616,8 +607,6 @@ describe('DeploymentComponent (redeployment)', () => { contextIds: [2], simulate: false, sendEmail: false, - executeShakedownTest: false, - neighbourhoodTest: false, requestOnly: false, appsWithVersion, deploymentParameters, diff --git a/AMW_angular/io/src/app/deployment/deployment.component.ts b/AMW_angular/io/src/app/deployment/deployment.component.ts index 340ace6dd..2981bfa59 100644 --- a/AMW_angular/io/src/app/deployment/deployment.component.ts +++ b/AMW_angular/io/src/app/deployment/deployment.component.ts @@ -23,9 +23,10 @@ import { NgSelectModule } from '@ng-select/ng-select'; import { NotificationComponent } from '../shared/elements/notification/notification.component'; import { LoadingIndicatorComponent } from '../shared/elements/loading-indicator.component'; import { PageComponent } from '../layout/page/page.component'; +import { ButtonComponent } from '../shared/button/button.component'; @Component({ - selector: 'amw-deployment', + selector: 'app-deployment', templateUrl: './deployment.component.html', standalone: true, imports: [ @@ -36,6 +37,7 @@ import { PageComponent } from '../layout/page/page.component'; DateTimePickerComponent, IconComponent, PageComponent, + ButtonComponent, ], }) export class DeploymentComponent implements OnInit, AfterViewInit { @@ -65,7 +67,6 @@ export class DeploymentComponent implements OnInit, AfterViewInit { transDeploymentParameter: DeploymentParameter = {} as DeploymentParameter; transDeploymentParameters: DeploymentParameter[] = []; deploymentResponse: any = {}; - hasPermissionShakedownTest: boolean = false; hasPermissionToDeploy: boolean = false; hasPermissionToRequestDeployment: boolean = false; @@ -77,9 +78,6 @@ export class DeploymentComponent implements OnInit, AfterViewInit { simulate: boolean = false; requestOnly: boolean = false; doSendEmail: boolean = false; - doExecuteShakedownTest: boolean = false; - // may only be enabled if above is true - doNeighbourhoodTest: boolean = false; bestForSelectedRelease: Release = null; @@ -136,7 +134,6 @@ export class DeploymentComponent implements OnInit, AfterViewInit { onChangeAppserver() { this.resetVars(); this.loadReleases(); - this.canCreateShakedownTest(); this.canDeploy(); } @@ -301,7 +298,6 @@ export class DeploymentComponent implements OnInit, AfterViewInit { this.errorMessage = ''; this.successMessage = ''; this.isDeploymentBlocked = false; - this.hasPermissionShakedownTest = false; this.selectedRelease = null; this.bestForSelectedRelease = null; this.resourceTags = [this.defaultResourceTag]; @@ -309,19 +305,11 @@ export class DeploymentComponent implements OnInit, AfterViewInit { this.deploymentDate = null; this.simulate = false; this.doSendEmail = false; - this.doExecuteShakedownTest = false; - this.doNeighbourhoodTest = false; this.appsWithVersion = []; this.transDeploymentParameter = {} as DeploymentParameter; this.transDeploymentParameters = []; } - private canCreateShakedownTest() { - this.resourceService - .canCreateShakedownTest(this.selectedAppserver.id) - .subscribe({ next: (r) => (this.hasPermissionShakedownTest = r), error: (e) => (this.errorMessage = e) }); - } - private canDeploy() { if (this.selectedAppserver != null) { this.hasPermissionToDeploy = false; @@ -380,8 +368,6 @@ export class DeploymentComponent implements OnInit, AfterViewInit { deploymentRequest.contextIds = contextIds; deploymentRequest.simulate = this.simulate; deploymentRequest.sendEmail = this.doSendEmail; - deploymentRequest.executeShakedownTest = this.doExecuteShakedownTest; - deploymentRequest.neighbourhoodTest = this.doNeighbourhoodTest; deploymentRequest.requestOnly = this.requestOnly; deploymentRequest.appsWithVersion = this.appsWithVersion; if (!this.isRedeployment) { @@ -425,10 +411,10 @@ export class DeploymentComponent implements OnInit, AfterViewInit { private extractEnvironmentGroups() { this.environments.forEach((environment) => { - if (!this.groupedEnvironments[environment['parent']]) { - this.groupedEnvironments[environment['parent']] = []; + if (!this.groupedEnvironments[environment['parentName']]) { + this.groupedEnvironments[environment['parentName']] = []; } - this.groupedEnvironments[environment['parent']].push(environment); + this.groupedEnvironments[environment['parentName']].push(environment); }); } diff --git a/AMW_angular/io/src/app/deployment/deployment.service.spec.ts b/AMW_angular/io/src/app/deployment/deployment.service.spec.ts index 7cb47d0f5..d04712f7e 100644 --- a/AMW_angular/io/src/app/deployment/deployment.service.spec.ts +++ b/AMW_angular/io/src/app/deployment/deployment.service.spec.ts @@ -5,18 +5,16 @@ import { Deployment } from './deployment'; import { DeploymentService } from './deployment.service'; describe('DeploymentService', () => { - let httpClient: HttpClient; let httpTestingController: HttpTestingController; let service: DeploymentService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [], - providers: [DeploymentService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}); + imports: [], + providers: [DeploymentService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + }); httpTestingController = TestBed.inject(HttpTestingController); - httpClient = TestBed.inject(HttpClient); service = TestBed.inject(DeploymentService); }); @@ -87,7 +85,5 @@ describe('DeploymentService', () => { stateToDeploy: 1, sendEmailWhenDeployed: true, simulateBeforeDeployment: true, - shakedownTestsWhenDeployed: true, - neighbourhoodTest: false, }; }); diff --git a/AMW_angular/io/src/app/deployment/deployment.ts b/AMW_angular/io/src/app/deployment/deployment.ts index 15c1e4abf..97552fc88 100644 --- a/AMW_angular/io/src/app/deployment/deployment.ts +++ b/AMW_angular/io/src/app/deployment/deployment.ts @@ -33,6 +33,4 @@ export interface Deployment { stateToDeploy: number; sendEmailWhenDeployed: boolean; simulateBeforeDeployment: boolean; - shakedownTestsWhenDeployed: boolean; - neighbourhoodTest: boolean; } diff --git a/AMW_angular/io/src/app/deployment/environment.service.spec.ts b/AMW_angular/io/src/app/deployment/environment.service.spec.ts index 807c5923d..217bf732f 100644 --- a/AMW_angular/io/src/app/deployment/environment.service.spec.ts +++ b/AMW_angular/io/src/app/deployment/environment.service.spec.ts @@ -1,11 +1,10 @@ -import { HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; import { Environment } from './environment'; import { EnvironmentService } from './environment.service'; -describe('DeploymentService', () => { - let httpClient: HttpClient; +describe('EnvironmentService', () => { let httpTestingController: HttpTestingController; let service: EnvironmentService; @@ -13,28 +12,30 @@ describe('DeploymentService', () => { id: 1, name: 'env', nameAlias: 'env-alias', - parent: 'parens', + parentName: 'parent', + parentId: null, selected: true, disabled: false, }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [], - providers: [EnvironmentService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}); + imports: [], + providers: [EnvironmentService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + }); httpTestingController = TestBed.inject(HttpTestingController); - httpClient = TestBed.inject(HttpClient); service = TestBed.inject(EnvironmentService); }); afterEach(() => { + httpTestingController.expectOne('/AMW_rest/resources/environments/contexts'); httpTestingController.verify(); }); it('should be created', () => { expect(service).toBeTruthy(); + httpTestingController.expectOne('/AMW_rest/resources/environments'); }); it('should invoke the correct endpoint on getAll()', () => { @@ -42,11 +43,16 @@ describe('DeploymentService', () => { expect(environments).toEqual([environment]); }); - const req = httpTestingController.expectOne('/AMW_rest/resources/environments'); + const requests = httpTestingController.match('/AMW_rest/resources/environments'); + expect(requests.length).toBe(2); httpTestingController.expectNone('/AMW_rest/resources/environments?includingGroups=true'); - expect(req.request.method).toEqual('GET'); - req.flush([environment]); + requests.forEach((req) => { + expect(req.request.method).toEqual('GET'); + }); + + requests[0].flush([environment]); + requests[1].flush([environment]); }); it('should invoke the correct endpoint on getAllIncludingGroups ', () => { @@ -54,7 +60,7 @@ describe('DeploymentService', () => { expect(environmentIncludingGroups).toEqual([environment]); }); - httpTestingController.expectNone('/AMW_rest/resources/environments'); + httpTestingController.expectOne('/AMW_rest/resources/environments'); const req = httpTestingController.expectOne('/AMW_rest/resources/environments?includingGroups=true'); expect(req.request.method).toEqual('GET'); diff --git a/AMW_angular/io/src/app/deployment/environment.service.ts b/AMW_angular/io/src/app/deployment/environment.service.ts index 375645663..89fead359 100644 --- a/AMW_angular/io/src/app/deployment/environment.service.ts +++ b/AMW_angular/io/src/app/deployment/environment.service.ts @@ -1,12 +1,27 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Signal } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { catchError } from 'rxjs/operators'; +import { Observable, startWith, Subject } from 'rxjs'; +import { catchError, shareReplay, switchMap } from 'rxjs/operators'; import { Environment } from './environment'; import { BaseService } from '../base/base.service'; +import { toSignal } from '@angular/core/rxjs-interop'; @Injectable({ providedIn: 'root' }) export class EnvironmentService extends BaseService { + private reload$ = new Subject(); + private reloadedContexts = this.reload$.pipe( + startWith(null), + switchMap(() => this.getContexts()), + shareReplay(1), + ); + private reloadedEnvs = this.reload$.pipe( + startWith(null), + switchMap(() => this.getAll()), + shareReplay(1), + ); + contexts: Signal = toSignal(this.reloadedContexts, { initialValue: [] as Environment[] }); + envs: Signal = toSignal(this.reloadedEnvs, { initialValue: [] as Environment[] }); + constructor(private http: HttpClient) { super(); } @@ -33,4 +48,43 @@ export class EnvironmentService extends BaseService { }) .pipe(catchError(this.handleError)); } + + getContexts(): Observable { + return this.http + .get(`${this.getBaseUrl()}/environments/contexts`) + .pipe(catchError(this.handleError)); + } + + save(environment: Environment) { + if (environment.id) return this.update(environment); + return this.create(environment); + } + + private create(environment: Environment) { + return this.http + .post(`${this.getBaseUrl()}/environments/contexts`, environment, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + private update(environment: Environment) { + return this.http + .put(`${this.getBaseUrl()}/environments/contexts/${environment.id}`, environment, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + delete(id: number) { + return this.http + .delete(`${this.getBaseUrl()}/environments/contexts/${id}`, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + refreshData() { + this.reload$.next([]); + } } diff --git a/AMW_angular/io/src/app/deployment/environment.ts b/AMW_angular/io/src/app/deployment/environment.ts index ee5f7c812..8c7c002b4 100644 --- a/AMW_angular/io/src/app/deployment/environment.ts +++ b/AMW_angular/io/src/app/deployment/environment.ts @@ -2,7 +2,19 @@ export interface Environment { id: number; name: string; nameAlias: string; - parent: string; + parentName: string; + parentId: number; + selected: boolean; + disabled: boolean; +} + +export interface EnvironmentTree { + id: number; + name: string; + nameAlias: string; + parentName: string; + parentId: number; + children: EnvironmentTree[]; selected: boolean; disabled: boolean; } diff --git a/AMW_angular/io/src/app/deployments/deployment-container/deployment-container.component.ts b/AMW_angular/io/src/app/deployments/deployment-container/deployment-container.component.ts index 929afef9b..40084e819 100644 --- a/AMW_angular/io/src/app/deployments/deployment-container/deployment-container.component.ts +++ b/AMW_angular/io/src/app/deployments/deployment-container/deployment-container.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { EnvironmentService } from '../../deployment/environment.service'; import { DeploymentService } from '../../deployment/deployment.service'; @@ -12,8 +12,6 @@ import { DeploymentService } from '../../deployment/deployment.service'; imports: [RouterOutlet], }) -export class DeploymentContainerComponent implements OnInit { +export class DeploymentContainerComponent { constructor() {} - - ngOnInit(): void {} } diff --git a/AMW_angular/io/src/app/deployments/deployments-edit-modal.component.html b/AMW_angular/io/src/app/deployments/deployments-edit-modal.component.html index 921b6a50a..f928f7c44 100644 --- a/AMW_angular/io/src/app/deployments/deployments-edit-modal.component.html +++ b/AMW_angular/io/src/app/deployments/deployments-edit-modal.component.html @@ -1,13 +1,4 @@ - + - @if (hasPermissionShakedownTest) { -
- - -
- } @if (confirmationAttributes.shakedownTestsWhenDeployed) { -
- - -
- } } + } diff --git a/AMW_angular/io/src/app/deployments/deployments-edit-modal.component.ts b/AMW_angular/io/src/app/deployments/deployments-edit-modal.component.ts index e2e3f3796..8c27b8891 100644 --- a/AMW_angular/io/src/app/deployments/deployments-edit-modal.component.ts +++ b/AMW_angular/io/src/app/deployments/deployments-edit-modal.component.ts @@ -4,16 +4,18 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { DateTimeModel } from '../shared/date-time-picker/date-time.model'; import { DateTimePickerComponent } from '../shared/date-time-picker/date-time-picker.component'; import { FormsModule } from '@angular/forms'; +import { ModalHeaderComponent } from '../shared/modal-header/modal-header.component'; +import { ButtonComponent } from '../shared/button/button.component'; +import { IconComponent } from '../shared/icon/icon.component'; @Component({ - selector: 'amw-deployments-edit-modal', + selector: 'app-deployments-edit-modal', templateUrl: './deployments-edit-modal.component.html', standalone: true, - imports: [FormsModule, DateTimePickerComponent], + imports: [FormsModule, DateTimePickerComponent, ModalHeaderComponent, ButtonComponent, IconComponent], }) export class DeploymentsEditModalComponent { @Input() deployments: Deployment[] = []; - @Input() hasPermissionShakedownTest: boolean; @Output() errorMessage: EventEmitter = new EventEmitter(); @Output() doConfirmDeployment: EventEmitter = new EventEmitter(); @@ -68,8 +70,6 @@ export class DeploymentsEditModalComponent { for (const deployment of this.deployments) { deployment.sendEmailWhenDeployed = this.confirmationAttributes.sendEmailWhenDeployed; deployment.simulateBeforeDeployment = this.confirmationAttributes.simulateBeforeDeployment; - deployment.shakedownTestsWhenDeployed = this.confirmationAttributes.shakedownTestsWhenDeployed; - deployment.neighbourhoodTest = this.confirmationAttributes.neighbourhoodTest; this.doConfirmDeployment.emit(deployment); } } diff --git a/AMW_angular/io/src/app/deployments/deployments-list.component.html b/AMW_angular/io/src/app/deployments/deployments-list.component.html index d4869ebc1..fc9113092 100644 --- a/AMW_angular/io/src/app/deployments/deployments-list.component.html +++ b/AMW_angular/io/src/app/deployments/deployments-list.component.html @@ -169,19 +169,17 @@ - + - - - -
  • - Servers + Servers
  • -
    [delegationMode]="delegationMode" (cancelEdit)="cancel()" (saveRestriction)="persistRestriction()" - > + > } @if (create) { - Roles and Permissions [delegationMode]="delegationMode" (cancelEdit)="cancel()" (saveRestrictions)="createRestrictions($event)" - > + > } } @@ -97,13 +98,13 @@

    Roles and Permissions

    - + >
    } diff --git a/AMW_angular/io/src/app/settings/permission/permission.component.spec.ts b/AMW_angular/io/src/app/settings/permission/permission.component.spec.ts index 7862370e4..af4c96224 100644 --- a/AMW_angular/io/src/app/settings/permission/permission.component.spec.ts +++ b/AMW_angular/io/src/app/settings/permission/permission.component.spec.ts @@ -16,6 +16,7 @@ import { Restriction } from './restriction'; import { Tag } from './tag'; import { EnvironmentService } from '../../deployment/environment.service'; import { ResourceService } from '../../resource/resource.service'; +import { ResourceTypesService } from '../../resource/resource-types.service'; import { Environment } from 'src/app/deployment/environment'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; @@ -27,6 +28,7 @@ describe('PermissionComponent without any params (default: type Role)', () => { let permissionService: PermissionService; let environmentService: EnvironmentService; let resourceService: ResourceService; + let resourceTypesService: ResourceTypesService; const mockRoute: any = { snapshot: {} }; mockRoute.params = new Subject(); @@ -50,6 +52,7 @@ describe('PermissionComponent without any params (default: type Role)', () => { EnvironmentService, PermissionService, ResourceService, + ResourceTypesService, { provide: ActivatedRoute, useValue: mockRoute }, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), @@ -63,6 +66,7 @@ describe('PermissionComponent without any params (default: type Role)', () => { permissionService = TestBed.inject(PermissionService); environmentService = TestBed.inject(EnvironmentService); resourceService = TestBed.inject(ResourceService); + resourceTypesService = TestBed.inject(ResourceTypesService); }); it('should have default data', () => { @@ -75,12 +79,14 @@ describe('PermissionComponent without any params (default: type Role)', () => { { id: null, name: null, - parent: 'All', + parentName: 'All', selected: false, } as Environment, ]); expect(component.resourceGroups).toEqual([]); - expect(component.resourceTypes).toEqual([{ id: null, name: null }]); + expect(component.resourceTypes).toEqual([ + { id: null, name: null, hasChildren: false, children: [], isApplication: false, isDefaultResourceType: false }, + ]); expect(component.restrictionType).toEqual('role'); }); @@ -91,14 +97,14 @@ describe('PermissionComponent without any params (default: type Role)', () => { { name: 'RESOURCE_TYPE', old: false } as Permission, ]; const environments: Environment[] = [ - { id: 1, name: 'U', parent: 'Dev' } as Environment, - { id: 2, name: 'V', parent: 'Dev' } as Environment, - { id: 3, name: 'T', parent: 'Test' } as Environment, + { id: 1, name: 'U', parentName: 'Dev' } as Environment, + { id: 2, name: 'V', parentName: 'Dev' } as Environment, + { id: 3, name: 'T', parentName: 'Test' } as Environment, ]; spyOn(permissionService, 'getAllPermissionEnumValues').and.returnValue(of(permissions)); spyOn(environmentService, 'getAllIncludingGroups').and.returnValue(of(environments)); spyOn(resourceService, 'getAllResourceGroups').and.callThrough(); - spyOn(resourceService, 'getAllResourceTypes').and.callThrough(); + spyOn(resourceTypesService, 'getAllResourceTypes').and.callThrough(); // when component.ngOnInit(); mockRoute.params.next({ restrictionType: 'role' }); @@ -109,31 +115,31 @@ describe('PermissionComponent without any params (default: type Role)', () => { expect(permissionService.getAllPermissionEnumValues).toHaveBeenCalled(); expect(environmentService.getAllIncludingGroups).toHaveBeenCalled(); expect(resourceService.getAllResourceGroups).toHaveBeenCalled(); - expect(resourceService.getAllResourceTypes).toHaveBeenCalled(); + expect(resourceTypesService.getAllResourceTypes).toHaveBeenCalled(); expect(component.permissions).toEqual(permissions); expect(component.restriction).toBeNull(); expect(component.groupedEnvironments['All']).toContain({ id: null, name: null, - parent: 'All', + parentName: 'All', selected: false, } as Environment); expect(component.groupedEnvironments['Dev']).toContain({ id: 1, name: 'U', - parent: 'Dev', + parentName: 'Dev', selected: false, } as Environment); expect(component.groupedEnvironments['Dev']).toContain({ id: 2, name: 'V', - parent: 'Dev', + parentName: 'Dev', selected: false, } as Environment); expect(component.groupedEnvironments['Test']).toContain({ id: 3, name: 'T', - parent: 'Test', + parentName: 'Test', selected: false, } as Environment); }); @@ -402,6 +408,7 @@ describe('PermissionComponent with param restrictionType (type User)', () => { let permissionService: PermissionService; let environmentService: EnvironmentService; let resourceService: ResourceService; + let resourceTypesService: ResourceTypesService; const mockRoute: any = { snapshot: {} }; mockRoute.params = new Subject(); @@ -424,6 +431,7 @@ describe('PermissionComponent with param restrictionType (type User)', () => { EnvironmentService, PermissionService, ResourceService, + ResourceTypesService, { provide: ActivatedRoute, useValue: mockRoute }, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), @@ -436,6 +444,7 @@ describe('PermissionComponent with param restrictionType (type User)', () => { permissionService = TestBed.inject(PermissionService); environmentService = TestBed.inject(EnvironmentService); resourceService = TestBed.inject(ResourceService); + resourceTypesService = TestBed.inject(ResourceTypesService); }); it('should invoke some services on ngOnInt', () => { @@ -445,14 +454,14 @@ describe('PermissionComponent with param restrictionType (type User)', () => { { name: 'RESOURCE_TYPE', old: false } as Permission, ]; const environments: Environment[] = [ - { id: 1, name: 'U', parent: 'Dev' } as Environment, - { id: 2, name: 'V', parent: 'Dev' } as Environment, - { id: 3, name: 'T', parent: 'Test' } as Environment, + { id: 1, name: 'U', parentName: 'Dev' } as Environment, + { id: 2, name: 'V', parentName: 'Dev' } as Environment, + { id: 3, name: 'T', parentName: 'Test' } as Environment, ]; spyOn(permissionService, 'getAllPermissionEnumValues').and.returnValue(of(permissions)); spyOn(environmentService, 'getAllIncludingGroups').and.returnValue(of(environments)); spyOn(resourceService, 'getAllResourceGroups').and.callThrough(); - spyOn(resourceService, 'getAllResourceTypes').and.callThrough(); + spyOn(resourceTypesService, 'getAllResourceTypes').and.callThrough(); spyOn(permissionService, 'getAllUserRestrictionNames').and.callThrough(); // when component.ngOnInit(); @@ -463,7 +472,7 @@ describe('PermissionComponent with param restrictionType (type User)', () => { expect(permissionService.getAllPermissionEnumValues).toHaveBeenCalled(); expect(environmentService.getAllIncludingGroups).toHaveBeenCalled(); expect(resourceService.getAllResourceGroups).toHaveBeenCalled(); - expect(resourceService.getAllResourceTypes).toHaveBeenCalled(); + expect(resourceTypesService.getAllResourceTypes).toHaveBeenCalled(); expect(permissionService.getAllUserRestrictionNames).toHaveBeenCalled(); expect(component.permissions).toEqual(permissions); expect(component.restriction).toBeNull(); @@ -471,25 +480,25 @@ describe('PermissionComponent with param restrictionType (type User)', () => { expect(component.groupedEnvironments['All']).toContain({ id: null, name: null, - parent: 'All', + parentName: 'All', selected: false, } as Environment); expect(component.groupedEnvironments['Dev']).toContain({ id: 1, name: 'U', - parent: 'Dev', + parentName: 'Dev', selected: false, } as Environment); expect(component.groupedEnvironments['Dev']).toContain({ id: 2, name: 'V', - parent: 'Dev', + parentName: 'Dev', selected: false, } as Environment); expect(component.groupedEnvironments['Test']).toContain({ id: 3, name: 'T', - parent: 'Test', + parentName: 'Test', selected: false, } as Environment); }); @@ -502,6 +511,7 @@ describe('PermissionComponent with param actingUser (delegation mode)', () => { let permissionService: PermissionService; let environmentService: EnvironmentService; let resourceService: ResourceService; + let resourceTypesService: ResourceTypesService; const mockRoute: any = { snapshot: {} }; mockRoute.params = new Subject(); @@ -524,6 +534,7 @@ describe('PermissionComponent with param actingUser (delegation mode)', () => { EnvironmentService, PermissionService, ResourceService, + ResourceTypesService, { provide: ActivatedRoute, useValue: mockRoute }, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting(), @@ -536,6 +547,7 @@ describe('PermissionComponent with param actingUser (delegation mode)', () => { permissionService = TestBed.inject(PermissionService); environmentService = TestBed.inject(EnvironmentService); resourceService = TestBed.inject(ResourceService); + resourceTypesService = TestBed.inject(ResourceTypesService); }); it('should invoke some services on ngOnInt', () => { // given @@ -557,15 +569,15 @@ describe('PermissionComponent with param actingUser (delegation mode)', () => { { name: 'RESOURCE_TYPE', old: false, longName: 'RESOURCE_TYPE' }, ]; const environments: Environment[] = [ - { id: 1, name: 'U', parent: 'Dev' } as Environment, - { id: 2, name: 'V', parent: 'Dev' } as Environment, - { id: 3, name: 'T', parent: 'Test' } as Environment, + { id: 1, name: 'U', parentName: 'Dev' } as Environment, + { id: 2, name: 'V', parentName: 'Dev' } as Environment, + { id: 3, name: 'T', parentName: 'Test' } as Environment, ]; spyOn(permissionService, 'getAllUserRestrictionNames').and.returnValue(of(userNames)); spyOn(permissionService, 'getOwnUserAndRoleRestrictions').and.returnValue(of(restrictions)); spyOn(environmentService, 'getAllIncludingGroups').and.returnValue(of(environments)); spyOn(resourceService, 'getAllResourceGroups').and.callThrough(); - spyOn(resourceService, 'getAllResourceTypes').and.callThrough(); + spyOn(resourceTypesService, 'getAllResourceTypes').and.callThrough(); // when component.ngOnInit(); mockRoute.params.next({ actingUser: 'testUser' }); @@ -578,7 +590,7 @@ describe('PermissionComponent with param actingUser (delegation mode)', () => { expect(component.userNames).not.toContain('testUser'); expect(environmentService.getAllIncludingGroups).toHaveBeenCalled(); expect(resourceService.getAllResourceGroups).toHaveBeenCalled(); - expect(resourceService.getAllResourceTypes).toHaveBeenCalled(); + expect(resourceTypesService.getAllResourceTypes).toHaveBeenCalled(); expect(permissionService.getOwnUserAndRoleRestrictions).toHaveBeenCalled(); expect(component.assignableRestrictions).toEqual(restrictions); expect(component.assignablePermissions).toEqual(permissions); diff --git a/AMW_angular/io/src/app/settings/permission/permission.component.ts b/AMW_angular/io/src/app/settings/permission/permission.component.ts index 4b47b7c27..de99696a1 100644 --- a/AMW_angular/io/src/app/settings/permission/permission.component.ts +++ b/AMW_angular/io/src/app/settings/permission/permission.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, AfterViewInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { PermissionService } from './permission.service'; import { Restriction } from './restriction'; @@ -27,9 +27,11 @@ import { NgbNavOutlet, } from '@ng-bootstrap/ng-bootstrap'; import { LoadingIndicatorComponent } from '../../shared/elements/loading-indicator.component'; +import { ButtonComponent } from '../../shared/button/button.component'; +import { ResourceTypesService } from '../../resource/resource-types.service'; @Component({ - selector: 'amw-permission', + selector: 'app-permission', templateUrl: './permission.component.html', standalone: true, imports: [ @@ -47,20 +49,23 @@ import { LoadingIndicatorComponent } from '../../shared/elements/loading-indicat RestrictionEditComponent, RestrictionAddComponent, RestrictionListComponent, + ButtonComponent, ], }) -export class PermissionComponent implements OnInit, OnDestroy, AfterViewInit { +export class PermissionComponent implements OnInit { // loaded only once roleNames: string[] = []; userNames: string[] = []; permissions: Permission[] = []; - environments: Environment[] = [{ id: null, name: null, parent: 'All', selected: false } as Environment]; + environments: Environment[] = [{ id: null, name: null, parentName: 'All', selected: false } as Environment]; groupedEnvironments: { [key: string]: Environment[] } = { All: [], Global: [], }; resourceGroups: Resource[] = []; - resourceTypes: ResourceType[] = [{ id: null, name: null }]; + resourceTypes: ResourceType[] = [ + { id: null, name: null, hasChildren: false, children: [], isApplication: false, isDefaultResourceType: false }, + ]; defaultNavItem: string = 'Roles'; // role | user @@ -88,6 +93,7 @@ export class PermissionComponent implements OnInit, OnDestroy, AfterViewInit { private permissionService: PermissionService, private environmentService: EnvironmentService, private resourceService: ResourceService, + private resourceTypesService: ResourceTypesService, private activatedRoute: ActivatedRoute, private location: Location, ) { @@ -113,10 +119,6 @@ export class PermissionComponent implements OnInit, OnDestroy, AfterViewInit { this.getAllResourceTypes(); } - ngAfterViewInit(): void {} - - ngOnDestroy() {} - onChangeRole() { this.selectedRoleName = this.selectedRoleName.trim(); if (this.isExistingRole(this.selectedRoleName)) { @@ -141,7 +143,7 @@ export class PermissionComponent implements OnInit, OnDestroy, AfterViewInit { this.clearMessages(); if (id) { this.permissionService.removeRestriction(id).subscribe({ - next: (r) => '', + next: () => '', error: (e) => (this.errorMessage = e), complete: () => _.remove(this.assignedRestrictions, { id }), }); @@ -172,7 +174,7 @@ export class PermissionComponent implements OnInit, OnDestroy, AfterViewInit { this.isLoading = true; if (this.restriction.id != null) { this.permissionService.updateRestriction(this.restriction).subscribe({ - next: (r) => '', + next: () => '', error: (e) => (this.errorMessage = e), complete: () => { this.updatePermissions(this.restriction); @@ -202,7 +204,7 @@ export class PermissionComponent implements OnInit, OnDestroy, AfterViewInit { this.clearMessages(); this.isLoading = true; this.permissionService.createRestrictions(restrictionsCreation, this.delegationMode).subscribe({ - next: (r) => '', + next: () => '', error: (e) => (this.errorMessage = e), complete: () => { this.create = false; @@ -394,7 +396,7 @@ export class PermissionComponent implements OnInit, OnDestroy, AfterViewInit { private getAllResourceTypes() { this.isLoading = true; - this.resourceService.getAllResourceTypes().subscribe({ + this.resourceTypesService.getAllResourceTypes().subscribe({ next: (r) => (this.resourceTypes = this.resourceTypes.concat(r)), error: (e) => (this.errorMessage = e), complete: () => (this.isLoading = false), @@ -447,10 +449,10 @@ export class PermissionComponent implements OnInit, OnDestroy, AfterViewInit { private extractEnvironmentGroups() { this.environments.forEach((environment) => { environment.selected = false; - if (!this.groupedEnvironments[environment['parent']]) { - this.groupedEnvironments[environment['parent']] = []; + if (!this.groupedEnvironments[environment['parentName']]) { + this.groupedEnvironments[environment['parentName']] = []; } - this.groupedEnvironments[environment['parent']].push(environment); + this.groupedEnvironments[environment['parentName']].push(environment); }); this.isLoading = false; } diff --git a/AMW_angular/io/src/app/settings/permission/permission.service.spec.ts b/AMW_angular/io/src/app/settings/permission/permission.service.spec.ts index f664e2153..848e3b7b7 100644 --- a/AMW_angular/io/src/app/settings/permission/permission.service.spec.ts +++ b/AMW_angular/io/src/app/settings/permission/permission.service.spec.ts @@ -8,17 +8,15 @@ describe('PermissionService', () => { let service: PermissionService; let httpTestingController: HttpTestingController; - let httpClient: HttpClient; beforeEach(() => { TestBed.configureTestingModule({ - imports: [], - providers: [PermissionService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}); + imports: [], + providers: [PermissionService, provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + }); service = TestBed.inject(PermissionService); httpTestingController = TestBed.inject(HttpTestingController); - httpClient = TestBed.inject(HttpClient); }); afterEach(() => { diff --git a/AMW_angular/io/src/app/settings/permission/restriction-add.component.html b/AMW_angular/io/src/app/settings/permission/restriction-add.component.html index b34efec14..1aad0297c 100644 --- a/AMW_angular/io/src/app/settings/permission/restriction-add.component.html +++ b/AMW_angular/io/src/app/settings/permission/restriction-add.component.html @@ -123,10 +123,10 @@
    {{ group }}:
    - - + + Save + Cancel
    diff --git a/AMW_angular/io/src/app/settings/permission/restriction-add.component.spec.ts b/AMW_angular/io/src/app/settings/permission/restriction-add.component.spec.ts index 7ccc0fda2..80db05f3b 100644 --- a/AMW_angular/io/src/app/settings/permission/restriction-add.component.spec.ts +++ b/AMW_angular/io/src/app/settings/permission/restriction-add.component.spec.ts @@ -218,9 +218,30 @@ describe('RestrictionAddComponent', () => { // given restrictionComponent.delegationMode = true; restrictionComponent.resourceTypes = [ - { id: 1, name: 'APP' }, - { id: 2, name: 'AS' }, - { id: 3, name: 'FOO' }, + { + id: 1, + name: 'APP', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 2, + name: 'AS', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 3, + name: 'FOO', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, ]; restrictionComponent.selectedPermissionNames = ['NEO']; restrictionComponent.selectedContextNames = ['T', 'S']; @@ -262,9 +283,30 @@ describe('RestrictionAddComponent', () => { // given restrictionComponent.delegationMode = true; restrictionComponent.resourceTypes = [ - { id: 1, name: 'APP' }, - { id: 2, name: 'AS' }, - { id: 3, name: 'FOO' }, + { + id: 1, + name: 'APP', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 2, + name: 'AS', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 3, + name: 'FOO', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, ]; restrictionComponent.selectedPermissionNames = ['NEO']; restrictionComponent.selectedContextNames = ['T']; @@ -306,9 +348,30 @@ describe('RestrictionAddComponent', () => { // given restrictionComponent.delegationMode = true; restrictionComponent.resourceTypes = [ - { id: 1, name: 'APP' }, - { id: 2, name: 'AS' }, - { id: 3, name: 'FOO' }, + { + id: 1, + name: 'APP', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 2, + name: 'AS', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 3, + name: 'FOO', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, ]; restrictionComponent.selectedPermissionNames = ['NEO']; restrictionComponent.selectedContextNames = ['T']; @@ -488,11 +551,11 @@ describe('RestrictionAddComponent', () => { (restrictionComponent: RestrictionAddComponent) => { // given const emptyEnvironment: Environment[] = [ - { id: null, name: null, nameAlias: null, parent: 'All', selected: false, disabled: false }, + { id: null, name: null, nameAlias: null, parentName: 'All', parentId: null, selected: false, disabled: false }, ]; const devEnvironments: Environment[] = [ - { id: 1, name: 'B', nameAlias: 'Test', parent: 'Dev', selected: false, disabled: false }, - { id: 2, name: 'C', nameAlias: null, parent: 'Dev', selected: false, disabled: false }, + { id: 1, name: 'B', nameAlias: 'Test', parentName: 'Dev', parentId: 2, selected: false, disabled: false }, + { id: 2, name: 'C', nameAlias: null, parentName: 'Dev', parentId: 2, selected: false, disabled: false }, ]; restrictionComponent.groupedEnvironments = { All: emptyEnvironment, Dev: devEnvironments }; restrictionComponent.delegationMode = true; diff --git a/AMW_angular/io/src/app/settings/permission/restriction-add.component.ts b/AMW_angular/io/src/app/settings/permission/restriction-add.component.ts index 2097b4984..d516257ec 100644 --- a/AMW_angular/io/src/app/settings/permission/restriction-add.component.ts +++ b/AMW_angular/io/src/app/settings/permission/restriction-add.component.ts @@ -10,12 +10,13 @@ import { ResourceType } from 'src/app/resource/resource-type'; import { IconComponent } from '../../shared/icon/icon.component'; import { FormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; +import { ButtonComponent } from '../../shared/button/button.component'; @Component({ - selector: 'amw-restriction-add', + selector: 'app-restriction-add', templateUrl: './restriction-add.component.html', standalone: true, - imports: [NgSelectModule, FormsModule, IconComponent], + imports: [NgSelectModule, FormsModule, IconComponent, ButtonComponent], }) export class RestrictionAddComponent implements OnChanges, AfterViewChecked { actions: Action[] = [ diff --git a/AMW_angular/io/src/app/settings/permission/restriction-edit.component.html b/AMW_angular/io/src/app/settings/permission/restriction-edit.component.html index ab9ca6aa5..1a7c88e5b 100644 --- a/AMW_angular/io/src/app/settings/permission/restriction-edit.component.html +++ b/AMW_angular/io/src/app/settings/permission/restriction-edit.component.html @@ -140,11 +140,11 @@
    @if (restriction.permission) { - + + Save } - + Cancel
    diff --git a/AMW_angular/io/src/app/settings/permission/restriction-edit.component.spec.ts b/AMW_angular/io/src/app/settings/permission/restriction-edit.component.spec.ts index aad371beb..56202239f 100644 --- a/AMW_angular/io/src/app/settings/permission/restriction-edit.component.spec.ts +++ b/AMW_angular/io/src/app/settings/permission/restriction-edit.component.spec.ts @@ -23,7 +23,8 @@ describe('RestrictionEditComponent', () => { id: null, name: null, nameAlias: null, - parent: 'All', + parentName: 'All', + parentId: null, selected: false, disabled: false, }, @@ -33,7 +34,8 @@ describe('RestrictionEditComponent', () => { id: 1, name: 'B', nameAlias: 'Test', - parent: 'Dev', + parentName: 'Dev', + parentId: 2, selected: false, disabled: false, }, @@ -41,7 +43,8 @@ describe('RestrictionEditComponent', () => { id: 2, name: 'C', nameAlias: null, - parent: 'Dev', + parentName: 'Dev', + parentId: 2, selected: false, disabled: false, }, @@ -107,8 +110,22 @@ describe('RestrictionEditComponent', () => { (restrictionComponent: RestrictionEditComponent) => { // given restrictionComponent.resourceTypes = [ - { id: 1, name: 'APP' }, - { id: 2, name: 'APPSERVER' }, + { + id: 1, + name: 'APP', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 2, + name: 'APPSERVER', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, ]; restrictionComponent.restriction = { resourceTypeName: 'INVALID', @@ -123,8 +140,22 @@ describe('RestrictionEditComponent', () => { (restrictionComponent: RestrictionEditComponent) => { // given restrictionComponent.resourceTypes = [ - { id: 1, name: 'APP' }, - { id: 2, name: 'APPSERVER' }, + { + id: 1, + name: 'APP', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 2, + name: 'APPSERVER', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, ]; restrictionComponent.restriction = { resourceTypeName: 'APPSERVER', @@ -263,14 +294,14 @@ describe('RestrictionEditComponent', () => { (restrictionComponent: RestrictionEditComponent) => { // given restrictionComponent.delegationMode = true; - const emptyEnvironment: Environment[] = [{ id: null, name: null, parent: 'All' } as Environment]; + const emptyEnvironment: Environment[] = [{ id: null, name: null, parentName: 'All' } as Environment]; const devEnvironments: Environment[] = [ - { id: 1, name: 'B', parent: 'Dev' } as Environment, - { id: 2, name: 'C', parent: 'Dev' } as Environment, + { id: 1, name: 'B', parentName: 'Dev' } as Environment, + { id: 2, name: 'C', parentName: 'Dev' } as Environment, ]; const prodEnvironments: Environment[] = [ - { id: 12, name: 'P', parent: 'Dev' } as Environment, - { id: 22, name: 'S', parent: 'Dev' } as Environment, + { id: 12, name: 'P', parentName: 'Dev' } as Environment, + { id: 22, name: 'S', parentName: 'Dev' } as Environment, ]; restrictionComponent.groupedEnvironments = { All: emptyEnvironment, @@ -425,9 +456,30 @@ describe('RestrictionEditComponent', () => { // given restrictionComponent.delegationMode = true; restrictionComponent.resourceTypes = [ - { id: 1, name: 'APP' }, - { id: 2, name: 'AS' }, - { id: 3, name: 'FOO' }, + { + id: 1, + name: 'APP', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 2, + name: 'AS', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 3, + name: 'FOO', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, ]; restrictionComponent.restriction = { action: 'CREATE', @@ -469,9 +521,30 @@ describe('RestrictionEditComponent', () => { // given restrictionComponent.delegationMode = true; restrictionComponent.resourceTypes = [ - { id: 1, name: 'APP' }, - { id: 2, name: 'AS' }, - { id: 3, name: 'FOO' }, + { + id: 1, + name: 'APP', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 2, + name: 'AS', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 3, + name: 'FOO', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, ]; restrictionComponent.restriction = { action: 'CREATE', @@ -512,9 +585,30 @@ describe('RestrictionEditComponent', () => { // given restrictionComponent.delegationMode = true; restrictionComponent.resourceTypes = [ - { id: 1, name: 'APP' }, - { id: 2, name: 'AS' }, - { id: 3, name: 'FOO' }, + { + id: 1, + name: 'APP', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 2, + name: 'AS', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, + { + id: 3, + name: 'FOO', + hasChildren: false, + children: [], + isApplication: false, + isDefaultResourceType: false, + }, ]; restrictionComponent.restriction = { action: 'CREATE', diff --git a/AMW_angular/io/src/app/settings/permission/restriction-edit.component.ts b/AMW_angular/io/src/app/settings/permission/restriction-edit.component.ts index 37421ab90..492500a73 100644 --- a/AMW_angular/io/src/app/settings/permission/restriction-edit.component.ts +++ b/AMW_angular/io/src/app/settings/permission/restriction-edit.component.ts @@ -8,12 +8,13 @@ import { ResourceType } from '../../resource/resource-type'; import { IconComponent } from '../../shared/icon/icon.component'; import { FormsModule } from '@angular/forms'; import { NgClass } from '@angular/common'; +import { ButtonComponent } from '../../shared/button/button.component'; @Component({ - selector: 'amw-restriction-edit', + selector: 'app-restriction-edit', templateUrl: './restriction-edit.component.html', standalone: true, - imports: [FormsModule, NgClass, IconComponent], + imports: [FormsModule, NgClass, IconComponent, ButtonComponent], }) export class RestrictionEditComponent implements OnChanges, AfterViewChecked { actions: string[] = ['ALL', 'CREATE', 'DELETE', 'READ', 'UPDATE']; @@ -171,12 +172,12 @@ export class RestrictionEditComponent implements OnChanges, AfterViewChecked { checkUnique(env: Environment) { if (this.delegationMode) { - const index: number = this.groupedEnvironments[env.parent].indexOf(env); - const isSelected: boolean = this.groupedEnvironments[env.parent][index].selected; + const index: number = this.groupedEnvironments[env.parentName].indexOf(env); + const isSelected: boolean = this.groupedEnvironments[env.parentName][index].selected; this.deSelectAllEnvironments(); - this.groupedEnvironments[env.parent][index].selected = isSelected; + this.groupedEnvironments[env.parentName][index].selected = isSelected; if (isSelected) { - this.restriction.contextName = this.groupedEnvironments[env.parent][index].name; + this.restriction.contextName = this.groupedEnvironments[env.parentName][index].name; } } } diff --git a/AMW_angular/io/src/app/settings/permission/restriction-list.component.html b/AMW_angular/io/src/app/settings/permission/restriction-list.component.html index fcd2e41af..b2c718049 100644 --- a/AMW_angular/io/src/app/settings/permission/restriction-list.component.html +++ b/AMW_angular/io/src/app/settings/permission/restriction-list.component.html @@ -33,12 +33,12 @@ @if (!delegationMode) { - - + + + + } diff --git a/AMW_angular/io/src/app/settings/permission/restriction-list.component.ts b/AMW_angular/io/src/app/settings/permission/restriction-list.component.ts index 8be254e2b..b397e7067 100644 --- a/AMW_angular/io/src/app/settings/permission/restriction-list.component.ts +++ b/AMW_angular/io/src/app/settings/permission/restriction-list.component.ts @@ -3,12 +3,13 @@ import { Restriction } from './restriction'; import * as _ from 'lodash'; import { Resource } from '../../resource/resource'; import { IconComponent } from '../../shared/icon/icon.component'; +import { ButtonComponent } from '../../shared/button/button.component'; @Component({ - selector: 'amw-restriction-list', + selector: 'app-restriction-list', templateUrl: './restriction-list.component.html', standalone: true, - imports: [IconComponent], + imports: [IconComponent, ButtonComponent], }) export class RestrictionListComponent { @Input() delegationMode: boolean; diff --git a/AMW_angular/io/src/app/settings/property-types/property-tag.ts b/AMW_angular/io/src/app/settings/property-types/property-tag.ts new file mode 100644 index 000000000..ec150c9f7 --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-tag.ts @@ -0,0 +1,4 @@ +export interface PropertyTag { + name: string; + type: string; +} diff --git a/AMW_angular/io/src/app/settings/property-types/property-type-delete.component.html b/AMW_angular/io/src/app/settings/property-types/property-type-delete.component.html new file mode 100644 index 000000000..562d15f09 --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-type-delete.component.html @@ -0,0 +1,12 @@ + + + diff --git a/AMW_angular/io/src/app/settings/property-types/property-type-delete.component.spec.ts b/AMW_angular/io/src/app/settings/property-types/property-type-delete.component.spec.ts new file mode 100644 index 000000000..2ab62eabd --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-type-delete.component.spec.ts @@ -0,0 +1,15 @@ +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { PropertyTypeDeleteComponent } from './property-type-delete.component'; + +describe('PropertyTypeDeleteComponent', () => { + let component: PropertyTypeDeleteComponent; + const activeModal = new NgbActiveModal(); + + beforeEach(async () => { + component = new PropertyTypeDeleteComponent(activeModal); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/settings/property-types/property-type-delete.component.ts b/AMW_angular/io/src/app/settings/property-types/property-type-delete.component.ts new file mode 100644 index 000000000..3c806ee39 --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-type-delete.component.ts @@ -0,0 +1,34 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { PropertyType } from './property-type'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { FormsModule } from '@angular/forms'; +import { ModalHeaderComponent } from '../../shared/modal-header/modal-header.component'; +import { ButtonComponent } from '../../shared/button/button.component'; + +@Component({ + selector: 'app-property-type-delete', + standalone: true, + imports: [FormsModule, ModalHeaderComponent, ButtonComponent], + templateUrl: './property-type-delete.component.html', +}) +export class PropertyTypeDeleteComponent { + @Input() propertyType: PropertyType; + @Output() deletePropertyType: EventEmitter = new EventEmitter(); + + constructor(public activeModal: NgbActiveModal) { + this.activeModal = activeModal; + } + + getTitle(): string { + return 'Remove property type'; + } + + cancel() { + this.activeModal.close(); + } + + delete() { + this.deletePropertyType.emit(this.propertyType); + this.activeModal.close(); + } +} diff --git a/AMW_angular/io/src/app/settings/property-types/property-type-edit.component.html b/AMW_angular/io/src/app/settings/property-types/property-type-edit.component.html new file mode 100644 index 000000000..07b74e5c0 --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-type-edit.component.html @@ -0,0 +1,68 @@ + + + diff --git a/AMW_angular/io/src/app/settings/property-types/property-type-edit.component.spec.ts b/AMW_angular/io/src/app/settings/property-types/property-type-edit.component.spec.ts new file mode 100644 index 000000000..6a2bcb31d --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-type-edit.component.spec.ts @@ -0,0 +1,15 @@ +import { PropertyTypeEditComponent } from './property-type-edit.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +describe('ReleasesEditComponent', () => { + let component: PropertyTypeEditComponent; + const activeModal = new NgbActiveModal(); + + beforeEach(async () => { + component = new PropertyTypeEditComponent(activeModal); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/settings/property-types/property-type-edit.component.ts b/AMW_angular/io/src/app/settings/property-types/property-type-edit.component.ts new file mode 100644 index 000000000..d6b08d6ce --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-type-edit.component.ts @@ -0,0 +1,78 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { DatePickerComponent } from '../../shared/date-picker/date-picker.component'; +import { PropertyType } from './property-type'; +import { IconComponent } from '../../shared/icon/icon.component'; +import { PropertyTag } from './property-tag'; +import { ModalHeaderComponent } from '../../shared/modal-header/modal-header.component'; +import { ButtonComponent } from '../../shared/button/button.component'; + +@Component({ + selector: 'app-property-type-edit', + templateUrl: './property-type-edit.component.html', + standalone: true, + imports: [DatePickerComponent, IconComponent, FormsModule, ModalHeaderComponent, ButtonComponent], +}) +export class PropertyTypeEditComponent { + @Input() propertyType: PropertyType; + @Output() savePropertyType: EventEmitter = new EventEmitter(); + + title = 'property type'; + newTag: string = ''; + + constructor(public activeModal: NgbActiveModal) { + this.activeModal = activeModal; + } + + getTitle(): string { + return this.propertyType.id ? `Edit ${this.title}` : `Add ${this.title}`; + } + + cancel() { + this.activeModal.close(); + } + + isValidRegex() { + if (this.propertyType.validationRegex === '') { + return true; + } + try { + ''.match(this.propertyType.validationRegex); + return true; + } catch (e) { + return false; + } + } + + isValidForm() { + return this.propertyType.name !== '' && this.propertyType.validationRegex !== ''; + } + + save() { + const propertyType: PropertyType = { + name: this.propertyType.name, + id: this.propertyType.id ? this.propertyType.id : null, + validationRegex: this.propertyType.validationRegex, + encrypted: this.propertyType.encrypted, + propertyTags: this.propertyType.propertyTags, + }; + this.savePropertyType.emit(propertyType); + this.activeModal.close(); + } + + deleteTag(tag: PropertyTag) { + this.propertyType.propertyTags = this.propertyType.propertyTags.filter((value) => { + return value.type !== tag.type || value.name !== tag.name; + }); + } + + addTag() { + const tag = this.newTag.trim(); + if (tag !== '') { + this.propertyType.propertyTags.push({ name: tag, type: 'LOCAL' }); + } + this.newTag = ''; + } +} diff --git a/AMW_angular/io/src/app/settings/property-types/property-type.ts b/AMW_angular/io/src/app/settings/property-types/property-type.ts new file mode 100644 index 000000000..323325700 --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-type.ts @@ -0,0 +1,9 @@ +import { PropertyTag } from './property-tag'; + +export interface PropertyType { + id: number; + name: string; + encrypted: boolean; + validationRegex: string; + propertyTags: PropertyTag[]; +} diff --git a/AMW_angular/io/src/app/settings/property-types/property-types.component.html b/AMW_angular/io/src/app/settings/property-types/property-types.component.html new file mode 100644 index 000000000..3ce7f1b22 --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-types.component.html @@ -0,0 +1,81 @@ + +
    +

    Property Types

    +
    +
    +
    +
    + +
    + @if (canAdd()) { + Add property type + } +
    +
    +
    +
    +
    + + + + + + + + + + + + + @for (property of propertyTypes(); track property.id) { + + + @if (canDisplay()) { + + + + + + } + + } + +
    Property NameEncryptedValidationTagsEditDelete
    {{ property.name }}{{ property.encrypted ? 'Yes' : 'No' }}{{ property.validationRegex }} +
    + @for (tag of property.propertyTags; track tag.name) { + {{ tag.name }} + } +
    +
    + @if (canEditName()) { + + } + + @if (canDelete()) { + + } +
    +
    +
    +
    +
    diff --git a/AMW_angular/io/src/app/settings/property-types/property-types.component.spec.ts b/AMW_angular/io/src/app/settings/property-types/property-types.component.spec.ts new file mode 100644 index 000000000..c0a949cc2 --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-types.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PropertyTypesComponent } from './property-types.component'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; + +describe('PropertyTypesComponent', () => { + let component: PropertyTypesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PropertyTypesComponent], + providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], + }).compileComponents(); + + fixture = TestBed.createComponent(PropertyTypesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/settings/property-types/property-types.component.ts b/AMW_angular/io/src/app/settings/property-types/property-types.component.ts new file mode 100644 index 000000000..39f607638 --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-types.component.ts @@ -0,0 +1,132 @@ +import { Component, computed, inject, OnDestroy, OnInit, Signal, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AuthService } from '../../auth/auth.service'; +import { LoadingIndicatorComponent } from '../../shared/elements/loading-indicator.component'; +import { IconComponent } from '../../shared/icon/icon.component'; +import { PropertyTypesService } from './property-types.service'; +import { PropertyType } from './property-type'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ToastService } from '../../shared/elements/toast/toast.service'; +import { takeUntil } from 'rxjs/operators'; +import { PropertyTypeEditComponent } from './property-type-edit.component'; +import { Subject } from 'rxjs'; +import { PropertyTypeDeleteComponent } from './property-type-delete.component'; +import { ButtonComponent } from '../../shared/button/button.component'; + +@Component({ + selector: 'app-property-types', + standalone: true, + imports: [CommonModule, IconComponent, LoadingIndicatorComponent, ButtonComponent], + templateUrl: './property-types.component.html', +}) +export class PropertyTypesComponent implements OnInit, OnDestroy { + private authService = inject(AuthService); + private propertyTypeService = inject(PropertyTypesService); + private modalService = inject(NgbModal); + private toastService = inject(ToastService); + + private destroy$ = new Subject(); + + canAdd = signal(false); + canDelete = signal(false); + canDisplay = signal(false); + canEditName = signal(false); + canEditValidation = signal(false); + canSave = signal(false); + + propertyTypes: Signal; + error = signal(''); + handleError = computed(() => { + if (this.error() != '') { + this.toastService.error(this.error()); + } + }); + + private readonly PROPERTY_TYPE = 'Property type'; + isLoading = true; + + ngOnInit(): void { + this.getUserPermissions(); + this.getPropertyTypes(); + this.isLoading = false; + } + + ngOnDestroy(): void { + this.destroy$.next(undefined); + } + + private getUserPermissions() { + this.canAdd.set(this.authService.hasPermission('ADD_PROPTYPE', 'ALL')); + this.canDelete.set(this.authService.hasPermission('DELETE_PROPTYPE', 'ALL')); + this.canDisplay.set(this.authService.hasPermission('PROP_TYPE_NAME_VALUE', 'ALL')); + this.canEditName.set(this.authService.hasPermission('EDIT_PROP_TYPE_NAME', 'ALL')); + this.canEditValidation.set(this.authService.hasPermission('EDIT_PROP_TYPE_VALIDATION', 'ALL')); + this.canSave.set(this.authService.hasPermission('SAVE_SETTINGS_PROPTYPE', 'ALL')); + } + + private getPropertyTypes() { + this.propertyTypes = this.propertyTypeService.propertyTypes; + } + + addModal() { + const modalRef = this.modalService.open(PropertyTypeEditComponent); + modalRef.componentInstance.propertyType = { + id: 0, + name: '', + encrypted: false, + validationRegex: '', + propertyTags: [], + }; + modalRef.componentInstance.savePropertyType + .pipe(takeUntil(this.destroy$)) + .subscribe((propertyType: PropertyType) => this.save(propertyType)); + } + + editModal(propertyType: PropertyType) { + const modalRef = this.modalService.open(PropertyTypeEditComponent); + modalRef.componentInstance.propertyType = propertyType; + modalRef.componentInstance.savePropertyType + .pipe(takeUntil(this.destroy$)) + .subscribe((propertyType: PropertyType) => this.save(propertyType)); + } + + deleteModal(propertyType: PropertyType) { + const modalRef = this.modalService.open(PropertyTypeDeleteComponent); + modalRef.componentInstance.propertyType = propertyType; + modalRef.componentInstance.deletePropertyType + .pipe(takeUntil(this.destroy$)) + .subscribe((propertyType: PropertyType) => this.delete(propertyType)); + } + + save(propertyType: PropertyType) { + this.isLoading = true; + if (this.canSave() && this.canEditValidation && this.canEditName) { + this.propertyTypeService + .save(propertyType) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => this.toastService.success(`${this.PROPERTY_TYPE} saved.`), + error: (e) => this.error.set(e), + complete: () => { + this.propertyTypeService.reload(); + }, + }); + } + this.isLoading = false; + } + + delete(propertyType: PropertyType) { + this.isLoading = true; + this.propertyTypeService + .delete(propertyType.id) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => this.toastService.success(`${this.PROPERTY_TYPE} deleted.`), + error: (e) => this.error.set(e), + complete: () => { + this.propertyTypeService.reload(); + }, + }); + this.isLoading = false; + } +} diff --git a/AMW_angular/io/src/app/settings/property-types/property-types.service.ts b/AMW_angular/io/src/app/settings/property-types/property-types.service.ts new file mode 100644 index 000000000..914c3e9ad --- /dev/null +++ b/AMW_angular/io/src/app/settings/property-types/property-types.service.ts @@ -0,0 +1,64 @@ +import { inject, Injectable } from '@angular/core'; +import { BaseService } from '../../base/base.service'; +import { HttpClient } from '@angular/common/http'; +import { Observable, startWith, Subject } from 'rxjs'; +import { PropertyType } from './property-type'; +import { catchError, shareReplay, switchMap } from 'rxjs/operators'; +import { toSignal } from '@angular/core/rxjs-interop'; + +@Injectable({ providedIn: 'root' }) +export class PropertyTypesService extends BaseService { + private url = `${this.getBaseUrl()}/settings/propertyTypes`; + private http = inject(HttpClient); + private reload$ = new Subject(); + private propertyTypes$ = this.reload$.pipe( + startWith(null), + switchMap(() => this.fetchPropertyTypes()), + shareReplay(1), + ); + propertyTypes = toSignal(this.propertyTypes$, { initialValue: [] as PropertyType[] }); + + constructor() { + super(); + } + + private fetchPropertyTypes(): Observable { + return this.http.get(this.url).pipe(catchError(this.handleError)); + } + + reload() { + this.reload$.next([]); + } + + save(propertyType: PropertyType) { + if (propertyType.id) { + return this.update(propertyType); + } else { + return this.create(propertyType); + } + } + + delete(id: number) { + return this.http + .delete(`${this.url}/${id}`, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + private create(propertyType: PropertyType) { + return this.http + .post(this.url, propertyType, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + private update(propertyType: PropertyType) { + return this.http + .put(`${this.url}/${propertyType.id}`, propertyType, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } +} diff --git a/AMW_angular/io/src/app/settings/releases/release-delete.component.html b/AMW_angular/io/src/app/settings/releases/release-delete.component.html index 4e655af15..98728dece 100644 --- a/AMW_angular/io/src/app/settings/releases/release-delete.component.html +++ b/AMW_angular/io/src/app/settings/releases/release-delete.component.html @@ -1,9 +1,4 @@ - +