Add API calls to backend

This commit is contained in:
Albrecht Lohofener
2025-03-25 23:22:41 +01:00
parent 50b0b1a081
commit bfbcf4414e
14 changed files with 189 additions and 167 deletions
+10 -1
View File
@@ -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
]
};
@@ -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();
});
});
@@ -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<OpenWrtInfo> {
return this.http.get<any>(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<OpenWrtInfo> {
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<any> {
return this.http.get(this.urlPrefix + '/api/reboot');
}
getContainerInfo(): Observable<any> {
return this.http.get<any>(this.urlPrefix + '/api/get_container_info').pipe(
map(response => {
return response['combined_output'];
})
);
}
}
@@ -4,4 +4,7 @@
</div>
</div>
<button mat-stroked-button (click)="startClient()">Reconnect</button>
<div class="row">
<button mat-stroked-button (click)="startClient()">Reconnect console</button>
<button mat-stroked-button (click)="rebootOpenWrt()">Reboot OpenWrt</button>
</div>
@@ -0,0 +1,7 @@
.row {
display: flex;
}
.mat-mdc-button-base {
margin: 0 8px 0 0;
}
@@ -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();
}
}
@@ -1 +1,26 @@
<span>status works!</span>
<div class="responsive-grid">
<mat-card class="status-card" appearance="outlined">
<mat-card-header>
<mat-card-title>OpenWrt image</mat-card-title>
</mat-card-header>
<mat-card-content>
@if (openwrtVersion) {
<ul>
<li>Version: {{ openwrtVersion }}</li>
<li>Kernel: {{ openWrtKernel }}</li>
</ul>
} @else {
<mat-spinner [diameter]="50"></mat-spinner>
}
</mat-card-content>
</mat-card>
<mat-card class="status-card" appearance="outlined">
<mat-card-header>
<mat-card-title>Attached hardware</mat-card-title>
</mat-card-header>
<mat-card-content>
</mat-card-content>
</mat-card>
</div>
@@ -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;
}
@@ -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;
})
}
}
@@ -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<SystemInformationItem> {
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<SystemInformationItem[]> {
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);
}
@@ -1,26 +1,3 @@
<div class="mat-elevation-z2">
<table mat-table class="full-width-table" matSort aria-label="Elements">
<!-- Id Column -->
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Id</th>
<td mat-cell *matCellDef="let row">{{row.id}}</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
<td mat-cell *matCellDef="let row">{{row.name}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator #paginator
[length]="dataSource.data.length"
[pageIndex]="0"
[pageSize]="10"
[pageSizeOptions]="[5, 10, 20]"
aria-label="Select page">
</mat-paginator>
<div>
<pre>{{ containerInfo }}</pre>
</div>
@@ -1,3 +0,0 @@
.full-width-table {
width: 100%;
}
@@ -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();
}));
@@ -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<SystemInformationItem>;
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;
})
}
}