From bfbcf4414ee5a7e013f56d4c54eb9e6d71c02256 Mon Sep 17 00:00:00 2001 From: Albrecht Lohofener Date: Tue, 25 Mar 2025 23:22:41 +0100 Subject: [PATCH] Add API calls to backend --- web-frontend/src/app/app.config.ts | 11 +- .../app/backend-communication.service.spec.ts | 16 +++ .../src/app/backend-communication.service.ts | 75 ++++++++++++ .../src/app/console/console.component.html | 5 +- .../src/app/console/console.component.scss | 7 ++ .../src/app/console/console.component.ts | 11 +- .../src/app/status/status.component.html | 27 ++++- .../src/app/status/status.component.scss | 14 +++ .../src/app/status/status.component.ts | 17 ++- .../system-information-datasource.ts | 111 ------------------ .../system-information.component.html | 27 +---- .../system-information.component.scss | 3 - .../system-information.component.spec.ts | 2 - .../system-information.component.ts | 30 ++--- 14 files changed, 189 insertions(+), 167 deletions(-) create mode 100644 web-frontend/src/app/backend-communication.service.spec.ts create mode 100644 web-frontend/src/app/backend-communication.service.ts delete mode 100644 web-frontend/src/app/system-information/system-information-datasource.ts diff --git a/web-frontend/src/app/app.config.ts b/web-frontend/src/app/app.config.ts index a9af518..b7ccb19 100644 --- a/web-frontend/src/app/app.config.ts +++ b/web-frontend/src/app/app.config.ts @@ -4,6 +4,15 @@ import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; +import { provideHttpClient } from '@angular/common/http'; +import { BackendCommunicationService } from './backend-communication.service'; + export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideClientHydration(withEventReplay())] + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideClientHydration(withEventReplay()), + provideHttpClient(), + BackendCommunicationService + ] }; diff --git a/web-frontend/src/app/backend-communication.service.spec.ts b/web-frontend/src/app/backend-communication.service.spec.ts new file mode 100644 index 0000000..1d46d38 --- /dev/null +++ b/web-frontend/src/app/backend-communication.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { BackendCommunicationService } from './backend-communication.service'; + +describe('BackendCommunicationService', () => { + let service: BackendCommunicationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BackendCommunicationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/web-frontend/src/app/backend-communication.service.ts b/web-frontend/src/app/backend-communication.service.ts new file mode 100644 index 0000000..ff02965 --- /dev/null +++ b/web-frontend/src/app/backend-communication.service.ts @@ -0,0 +1,75 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, tap, map } from 'rxjs'; + +interface OpenWrtInfo { + version: string; + kernelVersion: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class BackendCommunicationService { + //urlPrefix: string = 'http://localhost:8006'; // Just for development + urlPrefix: string = ''; + + constructor(private http: HttpClient) { } + + getOpenWrtInfo(): Observable { + return this.http.get(this.urlPrefix + '/api/get_openwrt_info').pipe( + tap(response => console.log('Fetched OpenWrt data:', response)), + map(response => { + let tmpVersion = 'Unknown'; + let tmpKernelVersion = 'Unknown'; + if (response['combined_output'] !== '\n') { + try { + response['combined_output'] = JSON.parse(response['combined_output']); + + tmpVersion = response['combined_output']['return']['pretty-name']; + tmpKernelVersion = response['combined_output']['return']['kernel-release']; + } catch (error) { + //console.error('Error parsing combined_output:', error); + } + } + return { + version: tmpVersion, + kernelVersion: tmpKernelVersion + }; + }) + ); + } + + pollOpenWrtInfo(intervalMs: number = 1000): Observable { + return new Observable(observer => { + const poll = () => { + this.getOpenWrtInfo().subscribe({ + next: (response) => { + if (response.version !== 'Unknown') { + observer.next(response); + observer.complete(); + } else { + setTimeout(poll, intervalMs); + } + }, + error: (error) => { + observer.error(error); + } + }); + }; + poll(); + }); + } + + gracefulReboot(): Observable { + return this.http.get(this.urlPrefix + '/api/reboot'); + } + + getContainerInfo(): Observable { + return this.http.get(this.urlPrefix + '/api/get_container_info').pipe( + map(response => { + return response['combined_output']; + }) + ); + } +} diff --git a/web-frontend/src/app/console/console.component.html b/web-frontend/src/app/console/console.component.html index 31b4561..0d02dfe 100644 --- a/web-frontend/src/app/console/console.component.html +++ b/web-frontend/src/app/console/console.component.html @@ -4,4 +4,7 @@ - +
+ + +
diff --git a/web-frontend/src/app/console/console.component.scss b/web-frontend/src/app/console/console.component.scss index e69de29..cf9a41d 100644 --- a/web-frontend/src/app/console/console.component.scss +++ b/web-frontend/src/app/console/console.component.scss @@ -0,0 +1,7 @@ +.row { + display: flex; +} + +.mat-mdc-button-base { + margin: 0 8px 0 0; +} diff --git a/web-frontend/src/app/console/console.component.ts b/web-frontend/src/app/console/console.component.ts index c46b0d3..76a5272 100644 --- a/web-frontend/src/app/console/console.component.ts +++ b/web-frontend/src/app/console/console.component.ts @@ -1,8 +1,11 @@ import { Component, ElementRef, AfterViewInit, Renderer2 } from '@angular/core'; -import {MatButtonModule} from '@angular/material/button'; +import { MatButtonModule } from '@angular/material/button'; // @ts-expect-error no types import RFB from '@novnc/novnc/lib/rfb'; +import { BackendCommunicationService } from '../backend-communication.service'; + + // TODO, see following issue // https://github.com/novnc/noVNC/pull/1944 // https://github.com/novnc/noVNC/issues/1943 @@ -18,7 +21,7 @@ export class ConsoleComponent implements AfterViewInit { public rfb: any; - constructor(private el: ElementRef, private renderer: Renderer2) {} + constructor(private el: ElementRef, private renderer: Renderer2, private service: BackendCommunicationService) { } ngAfterViewInit(): void { const observer = new IntersectionObserver((entries) => { @@ -71,4 +74,8 @@ export class ConsoleComponent implements AfterViewInit { } } } + + rebootOpenWrt() { + this.service.gracefulReboot().subscribe(); + } } diff --git a/web-frontend/src/app/status/status.component.html b/web-frontend/src/app/status/status.component.html index 692e146..365c026 100644 --- a/web-frontend/src/app/status/status.component.html +++ b/web-frontend/src/app/status/status.component.html @@ -1 +1,26 @@ -status works! +
+ + + OpenWrt image + + + @if (openwrtVersion) { +
    +
  • Version: {{ openwrtVersion }}
  • +
  • Kernel: {{ openWrtKernel }}
  • +
+ } @else { + + } +
+
+ + + + Attached hardware + + + + + +
diff --git a/web-frontend/src/app/status/status.component.scss b/web-frontend/src/app/status/status.component.scss index e69de29..7e28674 100644 --- a/web-frontend/src/app/status/status.component.scss +++ b/web-frontend/src/app/status/status.component.scss @@ -0,0 +1,14 @@ +.status-card { + max-width: 400px; + margin: 5px; +} + +.responsive-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + //gap: 24px; +} + +mat-spinner { + margin-top: 10px; +} diff --git a/web-frontend/src/app/status/status.component.ts b/web-frontend/src/app/status/status.component.ts index 6e964c0..6e5db48 100644 --- a/web-frontend/src/app/status/status.component.ts +++ b/web-frontend/src/app/status/status.component.ts @@ -1,11 +1,24 @@ import { Component } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { BackendCommunicationService } from '../backend-communication.service'; + @Component({ selector: 'app-status', - imports: [], + imports: [MatCardModule, MatButtonModule, MatProgressSpinnerModule], templateUrl: './status.component.html', - styleUrl: './status.component.scss' + styleUrl: './status.component.scss', }) export class StatusComponent { + openwrtVersion?: string; + openWrtKernel?: string; + constructor(private service: BackendCommunicationService) { + this.service.pollOpenWrtInfo().subscribe(response => { + this.openwrtVersion = response.version; + this.openWrtKernel = response.kernelVersion; + }) + } } diff --git a/web-frontend/src/app/system-information/system-information-datasource.ts b/web-frontend/src/app/system-information/system-information-datasource.ts deleted file mode 100644 index fedb5c3..0000000 --- a/web-frontend/src/app/system-information/system-information-datasource.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { DataSource } from '@angular/cdk/collections'; -import { MatPaginator } from '@angular/material/paginator'; -import { MatSort } from '@angular/material/sort'; -import { map } from 'rxjs/operators'; -import { Observable, of as observableOf, merge } from 'rxjs'; - -// TODO: Replace this with your own data model type -export interface SystemInformationItem { - name: string; - id: number; -} - -// TODO: replace this with real data from your application -const EXAMPLE_DATA: SystemInformationItem[] = [ - {id: 1, name: 'Hydrogen'}, - {id: 2, name: 'Helium'}, - {id: 3, name: 'Lithium'}, - {id: 4, name: 'Beryllium'}, - {id: 5, name: 'Boron'}, - {id: 6, name: 'Carbon'}, - {id: 7, name: 'Nitrogen'}, - {id: 8, name: 'Oxygen'}, - {id: 9, name: 'Fluorine'}, - {id: 10, name: 'Neon'}, - {id: 11, name: 'Sodium'}, - {id: 12, name: 'Magnesium'}, - {id: 13, name: 'Aluminum'}, - {id: 14, name: 'Silicon'}, - {id: 15, name: 'Phosphorus'}, - {id: 16, name: 'Sulfur'}, - {id: 17, name: 'Chlorine'}, - {id: 18, name: 'Argon'}, - {id: 19, name: 'Potassium'}, - {id: 20, name: 'Calcium'}, -]; - -/** - * Data source for the SystemInformation view. This class should - * encapsulate all logic for fetching and manipulating the displayed data - * (including sorting, pagination, and filtering). - */ -export class SystemInformationDataSource extends DataSource { - data: SystemInformationItem[] = EXAMPLE_DATA; - paginator: MatPaginator | undefined; - sort: MatSort | undefined; - - constructor() { - super(); - } - - /** - * Connect this data source to the table. The table will only update when - * the returned stream emits new items. - * @returns A stream of the items to be rendered. - */ - connect(): Observable { - if (this.paginator && this.sort) { - // Combine everything that affects the rendered data into one update - // stream for the data-table to consume. - return merge(observableOf(this.data), this.paginator.page, this.sort.sortChange) - .pipe(map(() => { - return this.getPagedData(this.getSortedData([...this.data ])); - })); - } else { - throw Error('Please set the paginator and sort on the data source before connecting.'); - } - } - - /** - * Called when the table is being destroyed. Use this function, to clean up - * any open connections or free any held resources that were set up during connect. - */ - disconnect(): void {} - - /** - * Paginate the data (client-side). If you're using server-side pagination, - * this would be replaced by requesting the appropriate data from the server. - */ - private getPagedData(data: SystemInformationItem[]): SystemInformationItem[] { - if (this.paginator) { - const startIndex = this.paginator.pageIndex * this.paginator.pageSize; - return data.splice(startIndex, this.paginator.pageSize); - } else { - return data; - } - } - - /** - * Sort the data (client-side). If you're using server-side sorting, - * this would be replaced by requesting the appropriate data from the server. - */ - private getSortedData(data: SystemInformationItem[]): SystemInformationItem[] { - if (!this.sort || !this.sort.active || this.sort.direction === '') { - return data; - } - - return data.sort((a, b) => { - const isAsc = this.sort?.direction === 'asc'; - switch (this.sort?.active) { - case 'name': return compare(a.name, b.name, isAsc); - case 'id': return compare(+a.id, +b.id, isAsc); - default: return 0; - } - }); - } -} - -/** Simple sort comparator for example ID/Name columns (for client-side sorting). */ -function compare(a: string | number, b: string | number, isAsc: boolean): number { - return (a < b ? -1 : 1) * (isAsc ? 1 : -1); -} diff --git a/web-frontend/src/app/system-information/system-information.component.html b/web-frontend/src/app/system-information/system-information.component.html index fba358b..169678c 100644 --- a/web-frontend/src/app/system-information/system-information.component.html +++ b/web-frontend/src/app/system-information/system-information.component.html @@ -1,26 +1,3 @@ -
- - - - - - - - - - - - - - - -
Id{{row.id}}Name{{row.name}}
- - - +
+
{{ containerInfo }}
diff --git a/web-frontend/src/app/system-information/system-information.component.scss b/web-frontend/src/app/system-information/system-information.component.scss index 5050fb6..e69de29 100644 --- a/web-frontend/src/app/system-information/system-information.component.scss +++ b/web-frontend/src/app/system-information/system-information.component.scss @@ -1,3 +0,0 @@ -.full-width-table { - width: 100%; -} diff --git a/web-frontend/src/app/system-information/system-information.component.spec.ts b/web-frontend/src/app/system-information/system-information.component.spec.ts index 252b6c4..a2ff8e1 100644 --- a/web-frontend/src/app/system-information/system-information.component.spec.ts +++ b/web-frontend/src/app/system-information/system-information.component.spec.ts @@ -1,5 +1,4 @@ import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { SystemInformationComponent } from './system-information.component'; @@ -9,7 +8,6 @@ describe('SystemInformationComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [NoopAnimationsModule] }).compileComponents(); })); diff --git a/web-frontend/src/app/system-information/system-information.component.ts b/web-frontend/src/app/system-information/system-information.component.ts index 411f3a9..a8ddc14 100644 --- a/web-frontend/src/app/system-information/system-information.component.ts +++ b/web-frontend/src/app/system-information/system-information.component.ts @@ -1,28 +1,20 @@ -import { AfterViewInit, Component, ViewChild } from '@angular/core'; -import { MatTableModule, MatTable } from '@angular/material/table'; -import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator'; -import { MatSortModule, MatSort } from '@angular/material/sort'; -import { SystemInformationDataSource, SystemInformationItem } from './system-information-datasource'; +import { Component } from '@angular/core'; +import { BackendCommunicationService } from '../backend-communication.service'; + @Component({ selector: 'app-system-information', templateUrl: './system-information.component.html', styleUrl: './system-information.component.scss', - standalone: true, - imports: [MatTableModule, MatPaginatorModule, MatSortModule] + imports: [] }) -export class SystemInformationComponent implements AfterViewInit { - @ViewChild(MatPaginator) paginator!: MatPaginator; - @ViewChild(MatSort) sort!: MatSort; - @ViewChild(MatTable) table!: MatTable; - dataSource = new SystemInformationDataSource(); +export class SystemInformationComponent { + containerInfo?: string; - /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ - displayedColumns = ['id', 'name']; - - ngAfterViewInit(): void { - this.dataSource.sort = this.sort; - this.dataSource.paginator = this.paginator; - this.table.dataSource = this.dataSource; + constructor(private service: BackendCommunicationService) { + this.service.getContainerInfo().subscribe(response => { + this.containerInfo = response; + }) } + }