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;
+ }
+}