diff --git a/README.md b/README.md index ff1cc8df..f7e84034 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ currently `cobbler-frontend` though. Serving will only work if you have built ou ## Code scaffolding -Run `ng generate component component-name` to generate a new component. You can also use +Run `ng generate component component-name --project projectName` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build diff --git a/package-lock.json b/package-lock.json index 4c209e47..2217a6a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@angular/platform-browser": "^17.3.12", "@angular/platform-browser-dynamic": "^17.3.12", "@angular/router": "^17.3.12", - "rxjs": "~6.6.6", + "rxjs": "^7.4.0", "stream": "0.0.2", "timers": "^0.1.1", "tslib": "^2.6.3", @@ -95,16 +95,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/architect/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/@angular-devkit/build-angular": { "version": "17.3.11", "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.11.tgz", @@ -651,16 +641,6 @@ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -783,16 +763,6 @@ "webpack-dev-server": "^4.0.0" } }, - "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/@angular-devkit/core": { "version": "17.3.11", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", @@ -821,16 +791,6 @@ } } }, - "node_modules/@angular-devkit/core/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/@angular-devkit/schematics": { "version": "17.3.11", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", @@ -850,16 +810,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/schematics/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/@angular-eslint/builder": { "version": "17.5.3", "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-17.5.3.tgz", @@ -3773,16 +3723,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@compodoc/compodoc/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/@compodoc/compodoc/node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", @@ -12686,16 +12626,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/inquirer/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -15313,16 +15243,6 @@ "node": ">=8" } }, - "node_modules/ng-packagr/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/ng-packagr/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -17923,23 +17843,13 @@ } }, "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "license": "Apache-2.0", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" + "tslib": "^2.1.0" } }, - "node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", diff --git a/package.json b/package.json index bffdaf46..c7975fc7 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "^17.3.12", "@angular/platform-browser-dynamic": "^17.3.12", "@angular/router": "^17.3.12", - "rxjs": "~6.6.6", + "rxjs": "^7.4.0", "stream": "0.0.2", "timers": "^0.1.1", "tslib": "^2.6.3", diff --git a/projects/cobbler-frontend/src/app/login/login.component.html b/projects/cobbler-frontend/src/app/login/login.component.html index 64f55fd7..3dbae6f8 100644 --- a/projects/cobbler-frontend/src/app/login/login.component.html +++ b/projects/cobbler-frontend/src/app/login/login.component.html @@ -6,14 +6,25 @@ >

Cobbler

-
+
Server + + @for (item of (config | async).cobblerUrls; track item) { + {{ item }} + } + @if (server.touched && server.invalid) { ⚠ {{ errMsgServer() }} } diff --git a/projects/cobbler-frontend/src/app/login/login.component.ts b/projects/cobbler-frontend/src/app/login/login.component.ts index 82b096b7..743a762c 100644 --- a/projects/cobbler-frontend/src/app/login/login.component.ts +++ b/projects/cobbler-frontend/src/app/login/login.component.ts @@ -1,21 +1,29 @@ -import { Component, Inject, OnDestroy, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + inject, + Inject, + OnDestroy, + signal, +} from '@angular/core'; import { AbstractControl, + FormBuilder, ReactiveFormsModule, - UntypedFormControl, - UntypedFormGroup, ValidationErrors, Validators, } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { Router } from '@angular/router'; import { COBBLER_URL, CobblerApiService } from 'cobbler-api'; +import { AppConfigService, AppConfig } from '../services/app-config.service'; -import { CommonModule } from '@angular/common'; +import { AsyncPipe, CommonModule } from '@angular/common'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { AuthGuardService } from '../services/auth-guard.service'; import { UserService } from '../services/user.service'; -import { merge, Subscription } from 'rxjs'; +import { merge, Observable, Subscription } from 'rxjs'; import { distinctUntilChanged } from 'rxjs/operators'; import { MatButtonModule } from '@angular/material/button'; @@ -30,7 +38,10 @@ import { MatButtonModule } from '@angular/material/button'; MatFormFieldModule, MatInputModule, MatButtonModule, + MatAutocompleteModule, + AsyncPipe, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class LogInFormComponent implements OnDestroy { subs = new Subscription(); @@ -38,18 +49,14 @@ export class LogInFormComponent implements OnDestroy { errMsgUser = signal(''); errMsgPassword = signal(''); + private readonly _formBuilder = inject(FormBuilder); server_prefilled: string; message = null; - login_form = new UntypedFormGroup({ - server: new UntypedFormControl('', [ - Validators.required, - LogInFormComponent.urlValidator, - ]), - username: new UntypedFormControl('', [ - Validators.required, - Validators.minLength(2), - ]), - password: new UntypedFormControl('', Validators.required), + config: Observable; + login_form = this._formBuilder.group({ + server: ['', [Validators.required, LogInFormComponent.urlValidator]], + username: ['', [Validators.required, Validators.minLength(2)]], + password: ['', Validators.required], }); private static urlValidator({ @@ -69,22 +76,26 @@ export class LogInFormComponent implements OnDestroy { private guard: AuthGuardService, @Inject(COBBLER_URL) url: URL, private cobblerApiService: CobblerApiService, + private configService: AppConfigService, ) { + this.configService.loadConfig(); + this.config = configService.AppConfig$; + // The injection token has a default value and as such is always set. this.server_prefilled = url.toString(); - this.login_form.get('server').setValue(this.server_prefilled); + this.login_form.controls.server.setValue(this.server_prefilled); this.subs.add( merge( - this.login_form.controls['server'].statusChanges, - this.login_form.controls['server'].valueChanges, + this.login_form.controls.server.statusChanges, + this.login_form.controls.server.valueChanges, ) .pipe(distinctUntilChanged()) .subscribe(() => this.updateErrServer()), ); this.subs.add( merge( - this.login_form.controls['username'].statusChanges, - this.login_form.controls['username'].valueChanges, + this.login_form.controls.username.statusChanges, + this.login_form.controls.username.valueChanges, ) .pipe(distinctUntilChanged()) .subscribe(() => { @@ -93,8 +104,8 @@ export class LogInFormComponent implements OnDestroy { ); this.subs.add( merge( - this.login_form.controls['password'].statusChanges, - this.login_form.controls['password'].valueChanges, + this.login_form.controls.password.statusChanges, + this.login_form.controls.password.valueChanges, ) .pipe(distinctUntilChanged()) .subscribe(() => this.updateErrPassword()), @@ -126,8 +137,8 @@ export class LogInFormComponent implements OnDestroy { this.cobblerApiService.reconfigureService(new URL(formData.server)); this.subs.add( - this.cobblerApiService.login(user, pass).subscribe( - (data) => { + this.cobblerApiService.login(user, pass).subscribe({ + next: (data) => { this.authO.changeAuthorizedState(true); // sets username in session storage this.authO.username = user; @@ -137,10 +148,10 @@ export class LogInFormComponent implements OnDestroy { this.guard.setBool(true); this.router.navigate(['/manage']); }, - () => + error: () => (this.message = 'Server, Username or Password did not Validate. Please try again.'), - ), + }), ); } diff --git a/projects/cobbler-frontend/src/app/navbar/navbar.component.html b/projects/cobbler-frontend/src/app/navbar/navbar.component.html index 592b942a..36ed3032 100644 --- a/projects/cobbler-frontend/src/app/navbar/navbar.component.html +++ b/projects/cobbler-frontend/src/app/navbar/navbar.component.html @@ -20,7 +20,12 @@
- Version: {{ cobbler_version }} +
+ + Server: {{ cobbler_server }}
+ Version: {{ cobbler_version }} +
+
@if (!islogged) { (); cobbler_version: String = 'Unknown'; + cobbler_server: String = 'localhost'; islogged: boolean = false; subscription: Subscription; @@ -51,6 +52,10 @@ export class NavbarComponent implements OnDestroy { ), ); + if (authO.server) { + this.cobbler_server = authO.server.match('http[s]*://([^/]*)').pop(); + } + this.subscription = this.authO.authorized .pipe(takeUntil(this.ngUnsubscribe)) .subscribe((value) => { @@ -60,18 +65,23 @@ export class NavbarComponent implements OnDestroy { this.islogged = false; } }); - cobblerApiService - .extended_version() - .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe( - (value) => { - this.cobbler_version = value.version; - }, - (error) => { - this.cobbler_version = 'Error'; - this._snackBar.open(error.message, 'Close'); - }, - ); + + // should not call version unless user has authenticated + // as it could try to hit an invalid / incorrect URL + if (this.islogged) { + cobblerApiService + .extended_version() + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe( + (value) => { + this.cobbler_version = value.version; + }, + (error) => { + this.cobbler_version = 'Error'; + this._snackBar.open(error.message, 'Close'); + }, + ); + } } ngOnDestroy(): void { diff --git a/projects/cobbler-frontend/src/app/services/app-config.service.spec.ts b/projects/cobbler-frontend/src/app/services/app-config.service.spec.ts new file mode 100644 index 00000000..5952c0eb --- /dev/null +++ b/projects/cobbler-frontend/src/app/services/app-config.service.spec.ts @@ -0,0 +1,19 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; + +import { AppConfigService } from './app-config.service'; + +describe('AppConfigService', () => { + let service: AppConfigService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + service = TestBed.inject(AppConfigService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/projects/cobbler-frontend/src/app/services/app-config.service.ts b/projects/cobbler-frontend/src/app/services/app-config.service.ts new file mode 100644 index 00000000..9bbda2f7 --- /dev/null +++ b/projects/cobbler-frontend/src/app/services/app-config.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { retry } from 'rxjs/operators'; + +export interface AppConfig { + cobblerUrls: string[]; +} + +const EMPTY_CONFIG: AppConfig = { + cobblerUrls: [], +}; + +@Injectable({ + providedIn: 'root', +}) +export class AppConfigService { + private configUrl = 'assets/configs/app-config.json'; + + public AppConfig: BehaviorSubject = new BehaviorSubject( + EMPTY_CONFIG, + ); + public AppConfig$: Observable = this.AppConfig.asObservable(); + + constructor(private http: HttpClient) {} + + loadConfig(): void { + // Need to subscribe but APP_INITIALIZE does not take type: subscription; + // use Promise instead + this.retrieveConfig().subscribe({ + next: (res) => { + this.AppConfig.next(res); + }, + error: (err: HttpErrorResponse) => { + if (err.status === 404) { + console.info("Couldn't load config at " + err.url); + } + }, + }); + } + + retrieveConfig() { + return this.http.get(this.configUrl).pipe( + retry(2), // retry a failed request up to 3 times + ); + } +} diff --git a/projects/cobbler-frontend/src/assets/configs/app-config.json b/projects/cobbler-frontend/src/assets/configs/app-config.json new file mode 100644 index 00000000..5614c9cf --- /dev/null +++ b/projects/cobbler-frontend/src/assets/configs/app-config.json @@ -0,0 +1,3 @@ +{ + "cobblerUrls": ["http://localhost/cobbler_api"] +} diff --git a/projects/cobbler-frontend/src/main.ts b/projects/cobbler-frontend/src/main.ts index 09b68d89..b76cecb3 100644 --- a/projects/cobbler-frontend/src/main.ts +++ b/projects/cobbler-frontend/src/main.ts @@ -10,6 +10,7 @@ import { provideRouter, withViewTransitions } from '@angular/router'; import { COBBLER_URL, cobblerUrlFactory } from 'cobbler-api'; import { routes } from './app/app-routing.module'; import { AppComponent } from './app/app.component'; +import { AppConfigService } from './app/services/app-config.service'; import { AuthGuardService } from './app/services/auth-guard.service'; import { UserService } from './app/services/user.service'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; @@ -26,6 +27,7 @@ bootstrapApplication(AppComponent, { }, UserService, AuthGuardService, + AppConfigService, { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'fill', floatLabel: 'always' },