diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index 009b857..277d682 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -35,9 +35,7 @@ import { GroupsComponent } from './components/groups/groups.component'; import { MatDividerModule } from '@angular/material/divider'; import { MatStepperModule } from '@angular/material/stepper'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { CreateClientComponent } from './components/groups/shared/clients/create-client/create-client.component'; import { DeleteModalComponent } from './shared/delete_modal/delete-modal/delete-modal.component'; -import { EditClientComponent } from './components/groups/shared/clients/edit-client/edit-client.component'; import { ClassroomViewComponent } from './components/groups/shared/classroom-view/classroom-view.component'; import { MatProgressSpinner } from "@angular/material/progress-spinner"; import { MatProgressBarModule } from '@angular/material/progress-bar'; @@ -130,6 +128,7 @@ import { CreateSubnetComponent } from "./components/ogdhcp/create-subnet/create- import { AddClientsToSubnetComponent } from "./components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component"; import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component'; import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component'; +import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './locale/', '.json'); } @@ -150,9 +149,8 @@ export function HttpLoaderFactory(http: HttpClient) { AddRoleModalComponent, ChangePasswordModalComponent, GroupsComponent, - CreateClientComponent, + ManageClientComponent, DeleteModalComponent, - EditClientComponent, ClassroomViewComponent, ClientViewComponent, ShowOrganizationalUnitComponent, @@ -215,7 +213,7 @@ export function HttpLoaderFactory(http: HttpClient) { ManageOrganizationalUnitComponent, BackupImageComponent, ShowClientsComponent, - OperationResultDialogComponent, + OperationResultDialogComponent ], bootstrap: [AppComponent], imports: [BrowserModule, diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.ts index 2865375..e302018 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.ts @@ -5,8 +5,8 @@ import {MatTableDataSource} from "@angular/material/table"; import {PartitionAssistantComponent} from "./partition-assistant/partition-assistant.component"; import {MatDialog} from "@angular/material/dialog"; import {Router} from "@angular/router"; -import {EditClientComponent} from "../../shared/clients/edit-client/edit-client.component"; import {ToastrService} from "ngx-toastr"; +import { ManageClientComponent } from "../../shared/clients/manage-client/manage-client.component"; interface ClientInfo { property: string; @@ -198,7 +198,7 @@ export class ClientMainViewComponent implements OnInit { onEditClick(event: MouseEvent, uuid: string): void { event.stopPropagation(); - const dialogRef = this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' } ); + const dialogRef = this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' } ); dialogRef.afterClosed().subscribe(); } diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index e98f94c..c953296 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -10,9 +10,7 @@ import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree' import { Subscription } from 'rxjs'; import { DataService } from './services/data.service'; import { UnidadOrganizativa, Client, TreeNode, FlatNode, Command } from './model/model'; -import { CreateClientComponent } from './shared/clients/create-client/create-client.component'; import { ManageOrganizationalUnitComponent } from './shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component'; -import { EditClientComponent } from './shared/clients/edit-client/edit-client.component'; import { ShowOrganizationalUnitComponent } from './shared/organizational-units/show-organizational-unit/show-organizational-unit.component'; import { LegendComponent } from './shared/legend/legend.component'; import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component'; @@ -22,6 +20,7 @@ import { MatTableDataSource } from '@angular/material/table'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { CreateMultipleClientComponent } from "./shared/clients/create-multiple-client/create-multiple-client.component"; import { SelectionModel } from "@angular/cdk/collections"; +import { ManageClientComponent } from "./shared/clients/manage-client/manage-client.component"; enum NodeType { OrganizationalUnit = 'organizational-unit', @@ -392,7 +391,7 @@ export class GroupsComponent implements OnInit, OnDestroy { addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void { event.stopPropagation(); const targetNode = organizationalUnit || this.selectedNode; - const dialogRef = this.dialog.open(CreateClientComponent, { + const dialogRef = this.dialog.open(ManageClientComponent, { data: { organizationalUnit: targetNode }, width: '900px', }); @@ -443,7 +442,7 @@ export class GroupsComponent implements OnInit, OnDestroy { const dialogRef = node?.type !== NodeType.Client ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) - : this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); + : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' }); dialogRef.afterClosed().subscribe(() => { if (node) { @@ -510,7 +509,7 @@ export class GroupsComponent implements OnInit, OnDestroy { const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid); const dialogRef = type !== NodeType.Client ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) - : this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); + : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' }); dialogRef.afterClosed().subscribe(() => { this.refreshData(this.selectedNode?.id, selectedClientsBeforeEdit); diff --git a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.css b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.css deleted file mode 100644 index b3c6559..0000000 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.css +++ /dev/null @@ -1,51 +0,0 @@ -h1 { - text-align: center; - font-family: 'Roboto', sans-serif; - font-weight: 400; - color: #3f51b5; - margin-bottom: 20px; - margin-top: 20px; -} - -.form-field { - width: 100%; -} - -.mat-dialog-content { - padding: 15px 50px 15px 50px; -} - -mat-option .unit-name { - display: block; -} - -mat-option .unit-path { - display: block; - font-size: 0.8em; - color: gray; -} - -.loading-spinner { - display: block; - margin: 0 auto; - align-items: center; - justify-content: center; -} - -.create-client-container { - position: relative; -} - -.grid-form { - display: grid; - grid-template-columns: repeat(2, 1fr); - column-gap: 20px; - row-gap: 20px; -} - -.action-container { - display: flex; - justify-content: flex-end; - gap: 1em; - padding: 1.5em; -} \ No newline at end of file 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 deleted file mode 100644 index 3cbfd51..0000000 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html +++ /dev/null @@ -1,115 +0,0 @@ -
-

Añadir Cliente

-
- -
- - Padre - - - {{ getSelectedParentName() }} - - -
{{ unit.name }}
-
{{ unit.path }}
-
-
-
- - - Nombre - - - - - OgLive - - - {{ oglive.name }} - - - - - - Número de Serie - - - - - Interfaz de red - - - {{ type.name }} - - - - - - Controlador de red - - - {{ type.name }} - - - - - - MAC - Ejemplo: 00:11:22:33:44:55 - - Formato de MAC inválido. Ejemplo válido: 00:11:22:33:44:55 - - - - Dirección IP - Ejemplo: 127.0.0.1 - - Formato de dirección IP inválido. Ejemplo válido: 127.0.0.1 - - - - Plantilla PXE - - - {{ template.name }} - - - - - - Perfil de Hardware - - - {{ unit.description }} - - - Formato de URL inválido. - - - - Repositorio - - - {{ repository.name }} - - - - - - {{ 'menuLabel' | translate }} - - - {{ menu.name }} - - - {{ 'menuError' | translate }} - -
-
- -
- - -
-
\ No newline at end of file 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 deleted file mode 100644 index c807b1c..0000000 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Component, Inject, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { ToastrService } from 'ngx-toastr'; -import { DataService } from '../../../services/data.service'; - -@Component({ - selector: 'app-create-client', - templateUrl: './create-client.component.html', - styleUrls: ['./create-client.component.css'] -}) -export class CreateClientComponent implements OnInit { - baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; - clientForm!: FormGroup; - parentUnits: any[] = []; - parentUnitsWithPaths: { id: string, name: string, path: string }[] = []; - hardwareProfiles: any[] = []; - ogLives: any[] = []; - menus: any[] = []; - templates: any[] = []; - repositories: any[] = []; - loading: boolean = false; - displayedColumns: string[] = ['name', 'ip']; - protected netifaceTypes = [ - { name: 'Eth0', value: 'eth0' }, - { name: 'Eth1', value: 'eth1' }, - { name: 'Eth2', value: 'eth2' } - ]; - protected netDriverTypes = [ - { name: 'Generic', value: 'generic' } - ]; - - constructor( - private fb: FormBuilder, - private dialogRef: MatDialogRef, - private http: HttpClient, - private snackBar: MatSnackBar, - private toastService: ToastrService, - private dataService: DataService, - @Inject(MAT_DIALOG_DATA) public data: any - ) {} - - ngOnInit(): void { - this.loadParentUnits(); - this.loadHardwareProfiles(); - this.loadOgLives(); - this.loadPxeTemplates(); - this.loadRepositories(); - this.loadMenus() - this.initForm(); - } - - initForm(): void { - this.clientForm = this.fb.group({ - organizationalUnit: [ - this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null, - Validators.required - ], - name: ['', Validators.required], - serialNumber: [''], - netiface: null, - netDriver: null, - mac: ['', Validators.required], - ip: ['', Validators.required], - template: [null], - hardwareProfile: [null], - ogLive: [null], - repository: [null], - menu: [null] - }); - } - - loadParentUnits(): void { - this.loading = true; - const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`; - - this.http.get(url).subscribe( - response => { - this.parentUnits = response['hydra:member']; - this.parentUnitsWithPaths = this.parentUnits.map(unit => ({ - id: unit['@id'], - name: unit.name, - path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits), - repository: unit.networkSettings?.repository?.['@id'], - hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'], - ogLive: unit.networkSettings?.ogLive?.['@id'], - menu: unit.networkSettings?.menu?.['@id'] - })); - - // 🚀 Ahora que los datos están listos, aplicamos la configuración inicial - const initialUnitId = this.clientForm.get('organizationalUnit')?.value; - if (initialUnitId) { - this.setOrganizationalUnitDefaults(initialUnitId); - } - - this.loading = false; - }, - error => { - console.error('Error fetching parent units:', error); - this.loading = false; - } - ); - } - - getSelectedParentName(): string | undefined { - const parentId = this.clientForm.get('organizationalUnit')?.value; - return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name; - } - - loadHardwareProfiles(): void { - this.dataService.getHardwareProfiles().subscribe( - (data: any[]) => { - this.hardwareProfiles = data; - this.loading = false; - }, - error => { - console.error('Error fetching hardware profiles:', error); - this.loading = false; - } - ); - } - - loadOgLives(): void { - const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`; - - this.http.get(url).subscribe( - response => { - this.ogLives = response['hydra:member']; - }, - error => { - console.error('Error fetching ogLives:', error); - } - ); - } - - loadPxeTemplates(): void { - const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`; - - this.http.get(url).subscribe( - response => { - this.templates = response['hydra:member']; - }, - error => { - console.error('Error fetching PXE templates:', error); - } - ); - } - - 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`; - - this.http.get(url).subscribe( - response => { - this.repositories = response['hydra:member']; - }, - error => { - console.error('Error fetching ogLives:', error); - } - ); - } - - onParentChange(event: any): void { - this.setOrganizationalUnitDefaults(event.value); - } - - setOrganizationalUnitDefaults(unitId: string): void { - const selectedUnit: any = this.parentUnitsWithPaths.find(unit => unit.id === unitId); - if (selectedUnit) { - this.clientForm.patchValue({ - repository: selectedUnit.repository || null, - hardwareProfile: selectedUnit.hardwareProfile || null, - ogLive: selectedUnit.ogLive || null, - menu: selectedUnit.menu || null - }); - } - } - - onSubmit(): void { - if (this.clientForm.valid) { - const formData = this.clientForm.value; - - this.http.post(`${this.baseUrl}/clients`, formData).subscribe( - (response) => { - this.toastService.success('Cliente creado exitosamente', 'Éxito'); - this.dialogRef.close({ - client: response, - organizationalUnit: formData.organizationalUnit, - }); - }, - (error) => { - this.toastService.error(error.error['hydra:description'], 'Error al crear el cliente'); - } - ); - } else { - this.toastService.error('Formulario inválido. Por favor, revise los campos obligatorios.', 'Error'); - } - } - - onNoClick(): void { - this.dialogRef.close(); - } -} diff --git a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.css b/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.css deleted file mode 100644 index b3c6559..0000000 --- a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.css +++ /dev/null @@ -1,51 +0,0 @@ -h1 { - text-align: center; - font-family: 'Roboto', sans-serif; - font-weight: 400; - color: #3f51b5; - margin-bottom: 20px; - margin-top: 20px; -} - -.form-field { - width: 100%; -} - -.mat-dialog-content { - padding: 15px 50px 15px 50px; -} - -mat-option .unit-name { - display: block; -} - -mat-option .unit-path { - display: block; - font-size: 0.8em; - color: gray; -} - -.loading-spinner { - display: block; - margin: 0 auto; - align-items: center; - justify-content: center; -} - -.create-client-container { - position: relative; -} - -.grid-form { - display: grid; - grid-template-columns: repeat(2, 1fr); - column-gap: 20px; - row-gap: 20px; -} - -.action-container { - display: flex; - justify-content: flex-end; - gap: 1em; - padding: 1.5em; -} \ No newline at end of file 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 deleted file mode 100644 index ba47bb9..0000000 --- a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.html +++ /dev/null @@ -1,112 +0,0 @@ -

{{ 'editClientDialogTitle' | translate }}

-
- -
- - - {{ 'organizationalUnitLabel' | translate }} - - - {{ getSelectedParentName() }} - - -
{{ unit.name }}
-
{{ unit.path }}
-
-
-
- - - {{ 'nameLabel' | translate }} - - - - - {{ 'ogLiveLabel' | translate }} - - - {{ ogLive.name }} - - - - - - {{ 'serialNumberLabel' | translate }} - - - - - {{ 'netifaceLabel' | translate }} - - - {{ type.name }} - - - - - - {{ 'netDriverLabel' | translate }} - - - {{ type.name }} - - - - - - {{ 'macLabel' | translate }} - - {{ 'macError' | translate }} - - - - {{ 'ipLabel' | translate }} - - {{ 'ipError' | translate }} - - - - {{ 'templateLabel' | translate }} - - - {{ template.name }} - - - - - - {{ 'hardwareProfileLabel' | translate }} - - - {{ unit.description }} - - - - - - Repositorio - - - {{ repository.name }} - - - - - - {{ '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 deleted file mode 100644 index e53dd47..0000000 --- a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Component, Inject } from '@angular/core'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { CreateClientComponent } from '../create-client/create-client.component'; -import { DataService } from '../../../services/data.service'; -import { ToastrService } from 'ngx-toastr'; - -@Component({ - selector: 'app-edit-client', - templateUrl: './edit-client.component.html', - styleUrls: ['./edit-client.component.css'] -}) -export class EditClientComponent { - baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; - clientForm!: FormGroup; - parentUnits: any[] = []; - parentUnitsWithPaths: { id: string, name: string, path: string }[] = []; - hardwareProfiles: any[] = []; - repositories: any[] = []; - ogLives: any[] = []; - templates: any[] = []; - menus: any[] = []; - isEditMode: boolean; - protected netifaceTypes = [ - { "name": 'Eth0', "value": "eth0" }, - { "name": 'Eth1', "value": "eth1" }, - { "name": 'Eth2', "value": "eth2" }, - ]; - protected netDriverTypes = [ - { "name": 'Generic', "value": "generic" }, - ]; - loading: boolean = false; - - constructor( - private fb: FormBuilder, - private dialogRef: MatDialogRef, - private http: HttpClient, - private dataService: DataService, - private toastService: ToastrService, - @Inject(MAT_DIALOG_DATA) public data: any - ) { - this.isEditMode = !!data?.uuid; - if (this.isEditMode) { - this.loadData(data.uuid); - } - } - - ngOnInit(): void { - this.loadParentUnits(); - this.loadHardwareProfiles(); - this.loadOgLives(); - this.loadPxeTemplates() - this.loadRepositories(); - this.loadMenus() - this.clientForm = this.fb.group({ - organizationalUnit: [null, Validators.required], - name: ['', Validators.required], - serialNumber: null, - netiface: null, - netDriver: null, - mac: null, - ip: null, - template: null, - hardwareProfile: null, - ogLive: null, - repository: null, - menu: null, - }); - } - - loadParentUnits(): void { - this.loading = true; - const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`; - - this.http.get(url).subscribe( - response => { - this.parentUnits = response['hydra:member']; - this.parentUnitsWithPaths = this.parentUnits.map(unit => ({ - id: unit['@id'], - name: unit.name, - path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits) - })); - this.loading = false; - }, - error => { - console.error('Error fetching parent units:', error); - this.loading = false; - } - ); - } - - getSelectedParentName(): string | undefined { - const parentId = this.clientForm.get('organizationalUnit')?.value; - return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name; - } - - loadHardwareProfiles(): void { - this.dataService.getHardwareProfiles().subscribe( - (data: any[]) => { - this.hardwareProfiles = data; - }, - (error: any) => { - console.error('Error fetching hardware profiles', error); - } - ); - } - - loadOgLives(): void { - const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=10000`; - - this.http.get(url).subscribe( - response => { - this.ogLives = response['hydra:member']; - }, - error => { - console.error('Error fetching ogLives:', error); - } - ); - } - - 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`; - - this.http.get(url).subscribe( - response => { - this.repositories = response['hydra:member']; - }, - error => { - console.error('Error fetching ogLives:', error); - } - ); - } - - loadPxeTemplates(): void { - const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`; - - this.http.get(url).subscribe( - response => { - this.templates = response['hydra:member']; - }, - error => { - console.error('Error fetching ogLives:', error); - } - ); - } - - loadData(uuid: string) { - this.loading = true; - const url = `${this.baseUrl}/clients/${uuid}`; - - this.http.get(url).subscribe( - data => { - this.clientForm.patchValue({ - name: data.name, - ip: data.ip, - mac: data.mac, - netiface: data.netiface, - netDriver: data.netDriver, - serialNumber: data.serialNumber, - hardwareProfile: data.hardwareProfile ? data.hardwareProfile['@id'] : null, - organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null, - 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; - }, - error => { - console.error('Error al cargar datos del cliente:', error); - this.loading = false; - } - ); - } - - onSubmit() { - if (this.clientForm.valid) { - const formData = this.clientForm.value; - - if (this.isEditMode) { - // Edit mode: Send PUT request to update the unit - const putUrl = `${this.baseUrl}/clients/${this.data.uuid}`; - const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); - - this.http.patch(putUrl, formData, { headers }).subscribe( - response => { - this.dialogRef.close(); - this.openSnackBar(false, 'Cliente actualizado exitosamente'); - - }, - error => { - console.error('Error al realizar PUT:', error); - this.openSnackBar(true, 'Error al actualizar el cliente: ' + error.error['hydra:description']); - } - ); - } else { - // Create mode: Send POST request to create a new unit - const postUrl = `${this.baseUrl}/clients`; - const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); - - this.http.post(postUrl, formData, { headers }).subscribe( - response => { - this.dialogRef.close(); - this.openSnackBar(false, 'Cliente creado exitosamente'); - }, - error => { - console.error('Error al realizar POST:', error); - this.openSnackBar(true, 'Error al crear el cliente: ' + error.error['hydra:description']); - } - ); - } - } - } - - onNoClick(): void { - this.dialogRef.close(); - } - - openSnackBar(isError: boolean, message: string) { - if (isError) { - this.toastService.error(' Error al crear el cliente: ' + message, 'Error'); - } else - this.toastService.success(message, 'Éxito'); - } -} diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css new file mode 100644 index 0000000..55a0232 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css @@ -0,0 +1,51 @@ +h1 { + text-align: center; + font-family: 'Roboto', sans-serif; + font-weight: 400; + color: #3f51b5; + margin-bottom: 20px; + margin-top: 20px; +} + +.form-field { + width: 100%; +} + +.mat-dialog-content { + padding: 15px 50px 15px 50px; +} + +mat-option .unit-name { + display: block; +} + +mat-option .unit-path { + display: block; + font-size: 0.8em; + color: gray; +} + +.loading-spinner { + display: block; + margin: 0 auto; + align-items: center; + justify-content: center; +} + +.create-client-container { + position: relative; +} + +.grid-form { + display: grid; + grid-template-columns: repeat(2, 1fr); + column-gap: 20px; + row-gap: 20px; +} + +.action-container { + display: flex; + justify-content: flex-end; + gap: 1em; + padding: 1.5em; +} \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html new file mode 100644 index 0000000..4d90eb2 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html @@ -0,0 +1,113 @@ +
+

{{ dialogTitle | translate }}

+
+ +
+ + {{ 'organizationalUnitLabel' | translate }} + + + {{ getSelectedParentName() }} + + +
{{ unit.name }}
+
{{ unit.path }}
+
+
+
+ + + {{ 'nameLabel' | translate }} + + + + + {{ 'ogLiveLabel' | translate }} + + + {{ ogLive.name }} + + + + + + {{ 'serialNumberLabel' | translate }} + + + + + {{ 'netifaceLabel' | translate }} + + + {{ type.name }} + + + + + + {{ 'netDriverLabel' | translate }} + + + {{ type.name }} + + + + + + {{ 'macLabel' | translate }} + + {{ 'macError' | translate }} + + + + {{ 'ipLabel' | translate }} + + {{ 'ipError' | translate }} + + + + {{ 'templateLabel' | translate }} + + + {{ template.name }} + + + + + + {{ 'hardwareProfileLabel' | translate }} + + + {{ profile.description }} + + + + + + {{ 'repositoryLabel' | translate }} + + + {{ repository.name }} + + + + + + {{ 'menuLabel' | translate }} + + + {{ menu.name }} + + + {{ 'menuError' | translate }} + +
+
+ +
+ + +
+
\ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.spec.ts b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.spec.ts new file mode 100644 index 0000000..d357706 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.spec.ts @@ -0,0 +1,69 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { ToastrModule } from 'ngx-toastr'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { ManageClientComponent } from './manage-client.component'; +import { DataService } from '../../../services/data.service'; + +describe('ManageClientComponent', () => { + let component: ManageClientComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ManageClientComponent], + imports: [ + HttpClientTestingModule, + ReactiveFormsModule, + MatDialogModule, + MatSnackBarModule, + ToastrModule.forRoot(), + TranslateModule.forRoot(), + MatProgressSpinnerModule + ], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: { uuid: '123', organizationalUnit: { '@id': '/units/1' } } }, + DataService + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ManageClientComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize form', () => { + expect(component.clientForm).toBeDefined(); + expect(component.clientForm.controls['name']).toBeDefined(); + expect(component.clientForm.controls['mac']).toBeDefined(); + expect(component.clientForm.controls['ip']).toBeDefined(); + }); + + it('should set dialog title for edit mode', () => { + component.isEditMode = true; + component.data = { uuid: '123', organizationalUnit: { '@id': '/units/1' } }; + component.ngOnInit(); + expect(component.dialogTitle).toBe('editClientDialogTitle'); + }); + + it('should call onSubmit when form is valid', () => { + spyOn(component, 'onSubmit'); + component.clientForm.controls['name'].setValue('Test Client'); + component.clientForm.controls['mac'].setValue('00:11:22:33:44:55'); + component.clientForm.controls['ip'].setValue('192.168.1.1'); + fixture.detectChanges(); + const button = fixture.debugElement.nativeElement.querySelector('.submit-button'); + button.click(); + expect(component.onSubmit).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.ts b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.ts new file mode 100644 index 0000000..a9d7863 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.ts @@ -0,0 +1,302 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Component, Inject, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { ToastrService } from 'ngx-toastr'; +import { DataService } from '../../../services/data.service'; + +@Component({ + selector: 'app-manage-client', + templateUrl: './manage-client.component.html', + styleUrls: ['./manage-client.component.css'] +}) +export class ManageClientComponent implements OnInit { + baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; + clientForm!: FormGroup; + parentUnits: any[] = []; + parentUnitsWithPaths: { id: string, name: string, path: string }[] = []; + hardwareProfiles: any[] = []; + ogLives: any[] = []; + menus: any[] = []; + templates: any[] = []; + repositories: any[] = []; + loading: boolean = false; + displayedColumns: string[] = ['name', 'ip']; + dialogTitle: string; + protected netifaceTypes = [ + { name: 'Eth0', value: 'eth0' }, + { name: 'Eth1', value: 'eth1' }, + { name: 'Eth2', value: 'eth2' } + ]; + protected netDriverTypes = [ + { name: 'Generic', value: 'generic' } + ]; + isEditMode: boolean; + + constructor( + private fb: FormBuilder, + private dialogRef: MatDialogRef, + private http: HttpClient, + private snackBar: MatSnackBar, + private toastService: ToastrService, + private dataService: DataService, + @Inject(MAT_DIALOG_DATA) public data: any + ) { + this.isEditMode = !!data?.uuid; + this.dialogTitle = this.isEditMode ? 'editClientDialogTitle' : 'addClientDialogTitle'; + } + + ngOnInit(): void { + this.loading = true; + this.initForm(); + const observables = [ + this.loadParentUnits(), + this.loadHardwareProfiles(), + this.loadOgLives(), + this.loadPxeTemplates(), + this.loadRepositories(), + this.loadMenus() + ]; + + Promise.all(observables).then(() => { + if (this.isEditMode) { + this.loadData(this.data.uuid).then(() => { + this.loading = false; + }); + } else { + this.loading = false; + } + }).catch(error => { + console.error('Error loading data:', error); + this.loading = false; + }); + } + + initForm(): void { + this.clientForm = this.fb.group({ + organizationalUnit: [ + this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null, + Validators.required + ], + name: ['', Validators.required], + serialNumber: [''], + netiface: null, + netDriver: null, + mac: ['', Validators.required], + ip: ['', Validators.required], + template: [null], + hardwareProfile: [null], + ogLive: [null], + repository: [null], + menu: [null] + }); + } + + loadParentUnits(): Promise { + return new Promise((resolve, reject) => { + const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`; + + this.http.get(url).subscribe( + response => { + this.parentUnits = response['hydra:member']; + this.parentUnitsWithPaths = this.parentUnits.map(unit => ({ + id: unit['@id'], + name: unit.name, + path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits), + repository: unit.networkSettings?.repository?.['@id'], + hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'], + ogLive: unit.networkSettings?.ogLive?.['@id'], + menu: unit.networkSettings?.menu?.['@id'] + })); + + const initialUnitId = this.clientForm.get('organizationalUnit')?.value; + if (initialUnitId) { + this.setOrganizationalUnitDefaults(initialUnitId); + } + + resolve(); + }, + error => { + console.error('Error fetching parent units:', error); + reject(error); + } + ); + }); + } + + getSelectedParentName(): string | undefined { + const parentId = this.clientForm.get('organizationalUnit')?.value; + return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name; + } + + loadHardwareProfiles(): Promise { + return new Promise((resolve, reject) => { + this.dataService.getHardwareProfiles().subscribe( + (data: any[]) => { + this.hardwareProfiles = data; + resolve(); + }, + error => { + console.error('Error fetching hardware profiles:', error); + reject(error); + } + ); + }); + } + + loadOgLives(): Promise { + return new Promise((resolve, reject) => { + const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`; + + this.http.get(url).subscribe( + response => { + this.ogLives = response['hydra:member']; + resolve(); + }, + error => { + console.error('Error fetching ogLives:', error); + reject(error); + } + ); + }); + } + + loadPxeTemplates(): Promise { + return new Promise((resolve, reject) => { + const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`; + + this.http.get(url).subscribe( + response => { + this.templates = response['hydra:member']; + resolve(); + }, + error => { + console.error('Error fetching PXE templates:', error); + reject(error); + } + ); + }); + } + + loadMenus(): Promise { + return new Promise((resolve, reject) => { + const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`; + + this.http.get(url).subscribe( + response => { + this.menus = response['hydra:member']; + resolve(); + }, + error => { + console.error('Error fetching menus:', error); + reject(error); + } + ); + }); + } + + loadRepositories(): Promise { + return new Promise((resolve, reject) => { + const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`; + + this.http.get(url).subscribe( + response => { + this.repositories = response['hydra:member']; + resolve(); + }, + error => { + console.error('Error fetching ogLives:', error); + reject(error); + } + ); + }); + } + + onParentChange(event: any): void { + this.setOrganizationalUnitDefaults(event.value); + } + + setOrganizationalUnitDefaults(unitId: string): void { + const selectedUnit: any = this.parentUnitsWithPaths.find(unit => unit.id === unitId); + if (selectedUnit) { + this.clientForm.patchValue({ + repository: selectedUnit.repository || null, + hardwareProfile: selectedUnit.hardwareProfile || null, + ogLive: selectedUnit.ogLive || null, + menu: selectedUnit.menu || null + }); + } + } + + loadData(uuid: string): Promise { + return new Promise((resolve, reject) => { + const url = `${this.baseUrl}/clients/${uuid}`; + + this.http.get(url).subscribe( + data => { + this.clientForm.patchValue({ + name: data.name, + ip: data.ip, + mac: data.mac, + netiface: data.netiface, + netDriver: data.netDriver, + serialNumber: data.serialNumber, + hardwareProfile: data.hardwareProfile ? data.hardwareProfile['@id'] : null, + organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null, + 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, + }); + resolve(); + }, + error => { + console.error('Error al cargar datos del cliente:', error); + reject(error); + } + ); + }); + } + + onSubmit(): void { + if (this.clientForm.valid) { + const formData = this.clientForm.value; + + if (this.isEditMode) { + const putUrl = `${this.baseUrl}/clients/${this.data.uuid}`; + const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); + + this.http.patch(putUrl, formData, { headers }).subscribe( + response => { + this.dialogRef.close(); + this.toastService.success('Cliente actualizado exitosamente', 'Éxito'); + }, + error => { + console.error('Error al realizar PUT:', error); + this.toastService.error('Error al actualizar el cliente: ' + error.error['hydra:description'], 'Error'); + } + ); + } else { + this.http.post(`${this.baseUrl}/clients`, formData).subscribe( + (response) => { + this.toastService.success('Cliente creado exitosamente', 'Éxito'); + this.dialogRef.close({ + client: response, + organizationalUnit: formData.organizationalUnit, + }); + }, + (error) => { + this.toastService.error(error.error['hydra:description'], 'Error al crear el cliente'); + } + ); + } + } else { + this.toastService.error('Formulario inválido. Por favor, revise los campos obligatorios.', 'Error'); + } + } + + onNoClick(): void { + this.dialogRef.close(); + } +} \ No newline at end of file