diff --git a/ogWebconsole/src/app/app-routing.module.ts b/ogWebconsole/src/app/app-routing.module.ts index b10a835..6fee8f4 100644 --- a/ogWebconsole/src/app/app-routing.module.ts +++ b/ogWebconsole/src/app/app-routing.module.ts @@ -40,6 +40,7 @@ import { MainRepositoryViewComponent } from "./components/repositories/main-repository-view/main-repository-view.component"; import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component"; +import {MenusComponent} from "./components/menus/menus.component"; const routes: Routes = [ { path: '', redirectTo: 'auth/login', pathMatch: 'full' }, { @@ -74,6 +75,7 @@ const routes: Routes = [ { path: 'software', component: SoftwareComponent }, { path: 'software-profiles', component: SoftwareProfileComponent }, { path: 'operative-systems', component: OperativeSystemComponent }, + { path: 'menus', component: MenusComponent }, ], }, { diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index 1245eeb..ef891a9 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -124,6 +124,8 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component'; import { MatSortModule } from '@angular/material/sort'; +import { MenusComponent } from './components/menus/menus.component'; +import { CreateMenuComponent } from './components/menus/create-menu/create-menu.component'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './locale/', '.json'); } @@ -206,6 +208,8 @@ export function HttpLoaderFactory(http: HttpClient) { MainRepositoryViewComponent, ExecuteCommandOuComponent, EnvVarsComponent, + MenusComponent, + CreateMenuComponent, ], bootstrap: [AppComponent], imports: [BrowserModule, diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index f8f1849..dae5459 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -183,10 +183,6 @@ mat-tree { padding: 10px; } -button { - margin: 5px; -} - mat-tree mat-tree-node { display: flex; align-items: center; diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 1fe953b..a697a88 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -49,7 +49,6 @@ {{ 'computerGroups' | translate }} - diff --git a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html index 4557346..2697534 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html +++ b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html @@ -130,7 +130,16 @@ - + + + {{ 'menuLabel' | translate }} + + + {{ menu.name }} + + + {{ 'menuError' | translate }} + diff --git a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts index e9d5209..d7f7e45 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts @@ -18,6 +18,7 @@ export class CreateClientComponent implements OnInit { parentUnits: any[] = []; hardwareProfiles: any[] = []; ogLives: any[] = []; + menus: any[] = []; templates: any[] = []; uploadedClients: any[] = []; repositories: any[] = []; @@ -51,6 +52,7 @@ export class CreateClientComponent implements OnInit { this.loadOgLives(); this.loadPxeTemplates(); this.loadRepositories(); + this.loadMenus() } initForm(): void { @@ -71,6 +73,7 @@ export class CreateClientComponent implements OnInit { ], ogLive: [null], repository: [null], + menu: [null] }); } @@ -129,6 +132,19 @@ export class CreateClientComponent implements OnInit { ); } + loadMenus(): void { + const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`; + + this.http.get(url).subscribe( + response => { + this.menus = response['hydra:member']; + }, + error => { + console.error('Error fetching menus:', error); + } + ); + } + loadRepositories(): void { const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`; @@ -146,13 +162,13 @@ export class CreateClientComponent implements OnInit { const file = event.target.files[0]; if (file) { const reader = new FileReader(); - + reader.onload = (e: any) => { const textData = e.target.result; const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g; let match; const clients = []; - + while ((match = regex.exec(textData)) !== null) { clients.push({ name: match[1], @@ -160,7 +176,7 @@ export class CreateClientComponent implements OnInit { ip: match[3] }); } - + if (clients.length > 0) { this.uploadedClients = clients; this.toastService.success('Archivo cargado correctamente, los datos están listos para enviarse.', 'Éxito'); @@ -170,16 +186,16 @@ export class CreateClientComponent implements OnInit { this.showTextarea = true; } }; - + reader.readAsText(file); } } - + onTextarea(text: string): void { const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g; let match; const clients = []; - + while ((match = regex.exec(text)) !== null) { clients.push({ name: match[1], @@ -187,7 +203,7 @@ export class CreateClientComponent implements OnInit { ip: match[3] }); } - + if (clients.length > 0) { this.uploadedClients = clients; this.toastService.success('Datos cargados correctamente, los datos están listos para enviarse.', 'Éxito'); @@ -197,7 +213,7 @@ export class CreateClientComponent implements OnInit { this.showTextarea = true; } } - + onSubmit(): void { if (this.isSingleClientForm) { if (this.clientForm.valid) { @@ -230,7 +246,7 @@ export class CreateClientComponent implements OnInit { netiface: null, netDriver: null }; - + this.http.post(`${this.baseUrl}/clients`, formData).subscribe( response => { this.toastService.success(`Cliente ${client.name} creado exitosamente`, 'Éxito'); @@ -241,7 +257,7 @@ export class CreateClientComponent implements OnInit { } ); }); - + this.uploadedClients = []; this.dialogRef.close(); } else { @@ -249,11 +265,11 @@ export class CreateClientComponent implements OnInit { } } } - + toggleClientForm(): void { this.isSingleClientForm = !this.isSingleClientForm; } - + onNoClick(): void { this.dialogRef.close(); } diff --git a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.html index 547bca3..663e8e0 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.html +++ b/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.html @@ -88,6 +88,15 @@ + + {{ 'menuLabel' | translate }} + + + {{ menu.name }} + + + {{ 'menuError' | translate }} + diff --git a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.ts b/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.ts index 6aa2807..1b05c2c 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.ts @@ -19,6 +19,7 @@ export class EditClientComponent { repositories: any[] = []; ogLives: any[] = []; templates: any[] = []; + menus: any[] = []; isEditMode: boolean; protected netifaceTypes = [ { "name": 'Eth0', "value": "eth0" }, @@ -50,6 +51,7 @@ export class EditClientComponent { this.loadOgLives(); this.loadPxeTemplates() this.loadRepositories(); + this.loadMenus() this.clientForm = this.fb.group({ organizationalUnit: [null, Validators.required], name: ['', Validators.required], @@ -62,6 +64,7 @@ export class EditClientComponent { hardwareProfile: null, ogLive: null, repository: null, + menu: null, }); } @@ -102,6 +105,19 @@ export class EditClientComponent { ); } + loadMenus(): void { + const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`; + + this.http.get(url).subscribe( + response => { + this.menus = response['hydra:member']; + }, + error => { + console.error('Error fetching menus:', error); + } + ); + } + loadRepositories(): void { const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`; @@ -146,6 +162,7 @@ export class EditClientComponent { repository: data.repository ? data.repository['@id'] : null, ogLive: data.ogLive ? data.ogLive['@id'] : null, template: data.template ? data.template['@id'] : null, + menu: data.menu ? data.menu['@id'] : null, }); this.loading = false; }, diff --git a/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.css b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.css new file mode 100644 index 0000000..606abbf --- /dev/null +++ b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.css @@ -0,0 +1,43 @@ +.dialog-content { + display: flex; + flex-direction: column; + gap: 16px; +} + +.menu-form { + width: 100%; + display: flex; + flex-direction: column; +} + +.form-field { + width: 100%; + margin-bottom: 16px; +} + +.dialog-actions { + display: flex; + justify-content: flex-end; + margin-top: 24px; +} + +button { + margin-left: 8px; +} + +@media (max-width: 600px) { + .form-field { + width: 100%; + } + + .dialog-actions { + flex-direction: column; + align-items: stretch; + } + + button { + width: 100%; + margin-left: 0; + margin-bottom: 8px; + } +} diff --git a/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.html b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.html new file mode 100644 index 0000000..eba0711 --- /dev/null +++ b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.html @@ -0,0 +1,30 @@ +

{{ menuId ? 'Editar' : 'Añadir' }} menú

+ + + + + + + + + diff --git a/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.spec.ts b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.spec.ts new file mode 100644 index 0000000..f56d3a4 --- /dev/null +++ b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreateMenuComponent } from './create-menu.component'; + +describe('CreateMenuComponent', () => { + let component: CreateMenuComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CreateMenuComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CreateMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.ts b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.ts new file mode 100644 index 0000000..f544930 --- /dev/null +++ b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.ts @@ -0,0 +1,94 @@ +import {Component, Inject} from '@angular/core'; +import {FormBuilder, FormGroup, Validators} from "@angular/forms"; +import {HttpClient} from "@angular/common/http"; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {ToastrService} from "ngx-toastr"; +import {DataService} from "../../images/data.service"; + +@Component({ + selector: 'app-create-menu', + templateUrl: './create-menu.component.html', + styleUrl: './create-menu.component.css' +}) +export class CreateMenuComponent { + baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; + menuForm: FormGroup; + menuId: string | null = null; + softwareProfiles: any[] = []; + + constructor( + private fb: FormBuilder, + private http: HttpClient, + public dialogRef: MatDialogRef, + private toastService: ToastrService, + private dataService: DataService, + @Inject(MAT_DIALOG_DATA) public data: any + ) { + this.menuForm = this.fb.group({ + name: ['', Validators.required], + publicUrl: ['', Validators.required], + resolution: ['', Validators.required], + comments: [''], + }); + } + + ngOnInit() { + if (this.data) { + this.load() + } + } + + load(): void { + this.dataService.getImage(this.data).subscribe({ + next: (response) => { + this.menuForm = this.fb.group({ + name: [response.name, Validators.required], + publicUrl: [response.publicUrl, Validators.required], + resolution: [response.resolution, Validators.required], + comments: [response.comments], + }); + this.menuId = response['@id']; + }, + error: (err) => { + console.error('Error fetching remote calendar:', err); + } + }); + } + + save(): void { + const payload = { + name: this.menuForm.value.name, + publicUrl: this.menuForm.value.publicUrl, + resolution: this.menuForm.value.resolution, + comments: this.menuForm.value.comments, + }; + + if (this.menuId) { + this.http.put(`${this.baseUrl}${this.menuId}`, payload).subscribe( + (response) => { + this.toastService.success('Menu editado correctamente'); + this.dialogRef.close(); + }, + (error) => { + this.toastService.error(error['error']['hydra:description']); + console.error('Error al editar la imagen', error); + } + ); + } else { + this.http.post(`${this.baseUrl}/menus`, payload).subscribe( + (response) => { + this.toastService.success('Menu añadido correctamente'); + this.dialogRef.close(); + }, + (error) => { + this.toastService.error(error['error']['hydra:description']); + console.error('Error al añadir el menu', error); + } + ); + } + } + + close(): void { + this.dialogRef.close(); + } +} diff --git a/ogWebconsole/src/app/components/menus/menus.component.css b/ogWebconsole/src/app/components/menus/menus.component.css new file mode 100644 index 0000000..cbdd6d8 --- /dev/null +++ b/ogWebconsole/src/app/components/menus/menus.component.css @@ -0,0 +1,83 @@ +.title { + font-size: 24px; +} + +.images-button-row { + display: flex; + justify-content: flex-start; + margin-top: 16px; +} + +.divider { + margin: 20px 0; +} + +.lists-container { + padding: 16px; +} + +.imagesLists-container { + flex: 1; +} + +.card.unidad-card { + height: 100%; + box-sizing: border-box; +} + +.image-container { + display: flex; + align-items: center; + margin-bottom: 16px; + border-bottom: 1px solid rgba(122, 122, 122, 0.555); +} + +.image-container h4 { + margin: 0; + flex: 1; +} + +.image-name{ + cursor: pointer; +} + +table { + width: 100%; + margin-top: 50px; +} + +.search-container { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 0 5px; + box-sizing: border-box; +} + +.search-string { + flex: 2; + padding: 5px; +} + +.search-boolean { + flex: 1; + padding: 5px; +} + +.header-container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; +} + +.mat-elevation-z8 { + box-shadow: 0px 0px 0px rgba(0,0,0,0.2); +} + +.paginator-container { + display: flex; + justify-content: end; + margin-bottom: 30px; +} diff --git a/ogWebconsole/src/app/components/menus/menus.component.html b/ogWebconsole/src/app/components/menus/menus.component.html new file mode 100644 index 0000000..031ca6e --- /dev/null +++ b/ogWebconsole/src/app/components/menus/menus.component.html @@ -0,0 +1,51 @@ +
+ +

Administrar menús

+
+ +
+
+ + +
+ + Buscar nombre de menú + + search + Pulsar 'enter' para buscar + +
+ + + + + + + + + + + + + +
{{ column.header }} + + {{ column.cell(menu) }} + + Acciones + + +
+ +
+ + +
diff --git a/ogWebconsole/src/app/components/menus/menus.component.spec.ts b/ogWebconsole/src/app/components/menus/menus.component.spec.ts new file mode 100644 index 0000000..4f5c591 --- /dev/null +++ b/ogWebconsole/src/app/components/menus/menus.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MenusComponent } from './menus.component'; + +describe('MenusComponent', () => { + let component: MenusComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MenusComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MenusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ogWebconsole/src/app/components/menus/menus.component.ts b/ogWebconsole/src/app/components/menus/menus.component.ts new file mode 100644 index 0000000..4434d07 --- /dev/null +++ b/ogWebconsole/src/app/components/menus/menus.component.ts @@ -0,0 +1,141 @@ +import { Component } from '@angular/core'; +import {MatTableDataSource} from "@angular/material/table"; +import {DatePipe} from "@angular/common"; +import {MatDialog} from "@angular/material/dialog"; +import {HttpClient} from "@angular/common/http"; +import {ToastrService} from "ngx-toastr"; +import {JoyrideService} from "ngx-joyride"; +import {Router} from "@angular/router"; +import {CreateRepositoryComponent} from "../repositories/create-repository/create-repository.component"; +import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delete-modal.component"; +import {CreateMenuComponent} from "./create-menu/create-menu.component"; +import {CreateImageComponent} from "../images/create-image/create-image.component"; + +@Component({ + selector: 'app-menus', + templateUrl: './menus.component.html', + styleUrl: './menus.component.css' +}) +export class MenusComponent { + baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; + dataSource = new MatTableDataSource(); + length: number = 0; + itemsPerPage: number = 10; + page: number = 0; + loading: boolean = false; + filters: { [key: string]: string } = {}; + datePipe: DatePipe = new DatePipe('es-ES'); + columns = [ + { + columnDef: 'id', + header: 'Id', + cell: (repository: any) => `${repository.id}` + }, + { + columnDef: 'name', + header: 'Nombre de menú', + cell: (repository: any) => `${repository.name}` + }, + { + columnDef: 'publicUrl', + header: 'Url pública', + cell: (repository: any) => `${repository.publicUrl}` + }, + { + columnDef: 'resolution', + header: 'Resolución', + cell: (repository: any) => `${repository.resolution}` + }, + { + columnDef: 'createdAt', + header: 'Fecha de creación', + cell: (repository: any) => `${this.datePipe.transform(repository.createdAt, 'dd/MM/yyyy hh:mm:ss')}` + } + ]; + displayedColumns = [...this.columns.map(column => column.columnDef), 'actions']; + + private apiUrl = `${this.baseUrl}/menus`; + + constructor( + public dialog: MatDialog, + private http: HttpClient, + private toastService: ToastrService, + private joyrideService: JoyrideService, + private router: Router + ) {} + + ngOnInit(): void { + this.search(); + } + + addImage(): void { + const dialogRef = this.dialog.open(CreateMenuComponent, { + width: '600px' + }); + + dialogRef.afterClosed().subscribe(() => { + this.search(); + }); + } + + search(): void { + this.loading = true; + this.http.get(`${this.apiUrl}?page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe( + data => { + this.dataSource.data = data['hydra:member']; + this.length = data['hydra:totalItems']; + this.loading = false; + }, + error => { + console.error('Error fetching images', error); + this.loading = false; + } + ); + } + + editMenu(event: MouseEvent, menu: any): void { + event.stopPropagation(); + this.dialog.open(CreateMenuComponent, { + width: '800px', + data: menu['@id'] + }).afterClosed().subscribe(() => this.search()); + } + + deleteMenu(event: MouseEvent,menu: any): void { + event.stopPropagation(); + this.dialog.open(DeleteModalComponent, { + width: '300px', + data: { name: menu.name }, + }).afterClosed().subscribe((result) => { + if (result) { + this.http.delete(`${this.apiUrl}/${menu.uuid}`).subscribe({ + next: () => { + this.toastService.success('Menu eliminado con éxito'); + this.search(); + }, + error: (error) => { + console.error('Error al eliminar el menú:', error); + } + }); + } + }); + } + + onPageChange(event: any): void { + this.page = event.pageIndex; + this.itemsPerPage = event.pageSize; + this.length = event.length; + this.search(); + } + + iniciarTour(): void { + this.joyrideService.startTour({ + steps: [ + 'titleStep', + 'addStep', + ], + showPrevButton: true, + themeColor: '#3f51b5' + }); + } +} diff --git a/ogWebconsole/src/app/components/repositories/repositories.component.ts b/ogWebconsole/src/app/components/repositories/repositories.component.ts index 2845c94..20229db 100644 --- a/ogWebconsole/src/app/components/repositories/repositories.component.ts +++ b/ogWebconsole/src/app/components/repositories/repositories.component.ts @@ -128,5 +128,4 @@ export class RepositoriesComponent { themeColor: '#3f51b5' }); } - } diff --git a/ogWebconsole/src/app/layout/sidebar/sidebar.component.html b/ogWebconsole/src/app/layout/sidebar/sidebar.component.html index a343128..e8a12f2 100644 --- a/ogWebconsole/src/app/layout/sidebar/sidebar.component.html +++ b/ogWebconsole/src/app/layout/sidebar/sidebar.component.html @@ -151,17 +151,10 @@ - + list {{ 'menus' | translate }} - - - - search - {{ 'search' | translate }} - - diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index ea2edaa..311bee0 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -211,6 +211,7 @@ "mcastPortLabel": "Multicast Port", "mcastModeLabel": "Multicast Mode", "menuUrlLabel": "Menu URL", + "menuLabel": "Menu", "hardwareProfileLabel": "Hardware Profile", "urlFormatError": "Invalid URL format.", "validationToggle": "Validation", diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index 967e9b4..0033d35 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -363,6 +363,7 @@ "selectOptionPlaceholder": "Selecciona una opción", "yesOption": "Sí", "noOption": "No", + "menuLabel": "Menu", "actionsColumn": "Acciones", "createServerButton": "Crear servidor", "labelName": "Nombre",