diff --git a/projects/cobbler-frontend/src/app/app-routing.module.ts b/projects/cobbler-frontend/src/app/app-routing.module.ts index 0180703f..f8f96abb 100644 --- a/projects/cobbler-frontend/src/app/app-routing.module.ts +++ b/projects/cobbler-frontend/src/app/app-routing.module.ts @@ -24,6 +24,7 @@ import { NotFoundComponent } from './not-found/not-found.component'; import { AuthGuardService } from './services/auth-guard.service'; import { SettingsViewComponent } from './settings/view/settings-view.component'; import { UnauthorizedComponent } from './unauthorized/unauthorized.component'; +import {SignaturesComponent} from "./signatures/signatures.component"; export const routes: Routes = [ @@ -51,6 +52,7 @@ export const routes: Routes = [ {path: 'status', component: StatusComponent, canActivate: [AuthGuardService]}, {path: 'hardlink', component: HardlinkComponent, canActivate: [AuthGuardService]}, {path: 'events', component: AppEventsComponent, canActivate: [AuthGuardService]}, + {path: 'signatures', component: SignaturesComponent, canActivate: [AuthGuardService]}, {path: '404', component: NotFoundComponent}, {path: '**', redirectTo: '/404'}, ]; diff --git a/projects/cobbler-frontend/src/app/manage-menu/manage-menu.component.html b/projects/cobbler-frontend/src/app/manage-menu/manage-menu.component.html index f89e7be9..5237291f 100644 --- a/projects/cobbler-frontend/src/app/manage-menu/manage-menu.component.html +++ b/projects/cobbler-frontend/src/app/manage-menu/manage-menu.component.html @@ -149,6 +149,10 @@

Cobbler

Events + + + Signatures +
+

Signatures

+ + + +
+ + + + + + + + + + {{ node.data }} + + + + + {{ node.data }} + + + + + + @for (column of columns; track column) { + + + + + } + + +
+ {{ column.header }} + + {{ column.cell(row) }} +
+
+
+
diff --git a/projects/cobbler-frontend/src/app/signatures/signatures.component.scss b/projects/cobbler-frontend/src/app/signatures/signatures.component.scss new file mode 100644 index 00000000..93cb1372 --- /dev/null +++ b/projects/cobbler-frontend/src/app/signatures/signatures.component.scss @@ -0,0 +1,19 @@ +.title-table { + display: table; + width: 100%; +} + +.title-row { + display: table-cell; + width: 100%; +} + +.title-cell-text { + display: table-cell; + width: 100%; + vertical-align: middle; +} + +.title-cell-button { + display: table-cell; +} diff --git a/projects/cobbler-frontend/src/app/signatures/signatures.component.spec.ts b/projects/cobbler-frontend/src/app/signatures/signatures.component.spec.ts new file mode 100644 index 00000000..26f20902 --- /dev/null +++ b/projects/cobbler-frontend/src/app/signatures/signatures.component.spec.ts @@ -0,0 +1,44 @@ +import {provideHttpClient} from '@angular/common/http'; +import {provideHttpClientTesting} from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {MatDividerModule} from '@angular/material/divider'; +import {MatIconModule} from '@angular/material/icon'; +import {MatTableModule} from '@angular/material/table'; +import {MatTreeModule} from '@angular/material/tree'; +import {COBBLER_URL} from 'cobbler-api'; + +import { SignaturesComponent } from './signatures.component'; + +describe('SignaturesComponent', () => { + let component: SignaturesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + SignaturesComponent, + MatTreeModule, + MatIconModule, + MatTableModule, + MatDividerModule, + ], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { + provide: COBBLER_URL, + useValue: new URL('http://localhost/cobbler_api') + }, + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SignaturesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/cobbler-frontend/src/app/signatures/signatures.component.ts b/projects/cobbler-frontend/src/app/signatures/signatures.component.ts new file mode 100644 index 00000000..9e3b7269 --- /dev/null +++ b/projects/cobbler-frontend/src/app/signatures/signatures.component.ts @@ -0,0 +1,186 @@ +import {AsyncPipe, NgForOf, NgIf} from '@angular/common'; +import {Component, OnInit} from '@angular/core'; +import {MatDivider} from '@angular/material/divider'; +import {MatList, MatListItem} from '@angular/material/list'; +import {MatProgressSpinner} from '@angular/material/progress-spinner'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import { + MatCell, MatCellDef, + MatColumnDef, + MatHeaderCell, + MatHeaderCellDef, + MatHeaderRow, MatHeaderRowDef, + MatRow, MatRowDef, + MatTable +} from '@angular/material/table'; +import {filter, repeat, take} from 'rxjs/operators'; +import {UserService} from '../services/user.service'; +import {CobblerApiService} from 'cobbler-api'; +import { + MatTree, + MatTreeFlatDataSource, + MatTreeFlattener, + MatTreeNode, MatTreeNodeDef, + MatTreeNodePadding, + MatTreeNodeToggle +} from '@angular/material/tree'; +import {FlatTreeControl} from '@angular/cdk/tree'; +import {MatIcon} from '@angular/material/icon'; +import {MatIconButton} from '@angular/material/button'; + +interface TableRow { + key: string; + value: any; +} + +/** + * Food data with nested structure. + * Each node has a name and an optional list of children. + */ +interface OsNode { + data: string | Array; + children?: OsNode[]; +} + +/** Flat node with expandable and level information */ +interface OsBreedFlatNode { + expandable: boolean; + data: string | Array; + level: number; +} + +@Component({ + selector: 'cobbler-signatures', + standalone: true, + imports: [ + MatTree, + MatTreeNode, + MatIcon, + MatIconButton, + MatTreeNodeToggle, + MatTreeNodePadding, + MatTreeNodeDef, + MatTable, + MatHeaderCell, + MatCell, + MatHeaderRow, + MatRow, + MatColumnDef, + MatHeaderCellDef, + MatCellDef, + MatHeaderRowDef, + MatRowDef, + MatDivider, + AsyncPipe, + MatList, + MatListItem, + MatProgressSpinner, + NgForOf, + NgIf + ], + templateUrl: './signatures.component.html', + styleUrl: './signatures.component.scss' +}) +export class SignaturesComponent implements OnInit { + // Table + columns = [ + { + columnDef: 'key', + header: 'Attribute', + cell: (element: TableRow) => `${element.key}`, + }, + { + columnDef: 'value', + header: 'Value', + cell: (element: TableRow) => `${element.value}`, + }, + ]; + + displayedColumns = this.columns.map(c => c.columnDef); + + // Tree + private _transformer = (node: OsNode, level: number) => { + return { + expandable: !!node.children && node.children.length > 0, + data: node.data, + level: level, + }; + }; + + treeControl = new FlatTreeControl( + node => node.level, + node => node.expandable, + ); + + treeFlattener = new MatTreeFlattener( + this._transformer, + node => node.level, + node => node.expandable, + node => node.children, + ); + dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + + // Spinner + public isLoading = true; + + constructor( + public userService: UserService, + private cobblerApiService: CobblerApiService, + private _snackBar: MatSnackBar, + ) { + } + + ngOnInit(): void { + this.generateSignatureUiData(); + } + + hasChild = (_: number, node: OsBreedFlatNode) => node.expandable; + + hasOsVersion = (_: number, node: OsBreedFlatNode) => typeof node.data !== 'string'; + + generateSignatureUiData(): void { + this.cobblerApiService.get_signatures(this.userService.token).subscribe(value => { + const newData: Array = []; + for (const k in value.breeds) { + const children: Array = []; + for (const j in value.breeds[k]) { + const osVersionData: Array = []; + for (const i in value.breeds[k][j]) { + osVersionData.push({key: i, value: value.breeds[k][j][i]}); + } + children.push({data: j, children: [{data: osVersionData, children: []}]}); + } + newData.push({data: k, children: children}); + } + this.dataSource.data = newData; + this.isLoading = false + }, error => { + // HTML encode the error message since it originates from XML + this._snackBar.open(this.toHTML(error.message), 'Close'); + }); + } + + updateSignatures(): void { + this.isLoading = true + this.cobblerApiService.background_signature_update(this.userService.token).subscribe( + value => { + this.cobblerApiService.get_task_status(value).pipe( + repeat(), + filter(data => data.state === "failed" || data.state === "complete"), + take(1) + ).subscribe(value1 => { + this.isLoading = false + this.generateSignatureUiData() + }) + }, + error => { + // HTML encode the error message since it originates from XML + this._snackBar.open(this.toHTML(error.message), 'Close'); + }); + } + + toHTML(input: string): any { + // FIXME: Deduplicate method + return new DOMParser().parseFromString(input, 'text/html').documentElement.textContent; + } +}