From 794682ec34df265581681828bd18373a983b4fc7 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Tue, 26 Nov 2024 10:58:17 +0100 Subject: [PATCH 01/17] Updated pxe UX --- ogWebconsole/src/app/app.module.ts | 4 - .../pxe-images/pxe-images.component.css | 23 ++-- .../pxe-images/pxe-images.component.html | 28 ++--- .../ogboot/pxe-images/pxe-images.component.ts | 18 +-- .../add-clients-to-pxe.component.css | 17 --- .../add-clients-to-pxe.component.html | 39 ------- .../add-clients-to-pxe.component.spec.ts | 69 ----------- .../add-clients-to-pxe.component.ts | 98 ---------------- .../ogboot/pxe/clients/clients.component.css | 47 -------- .../ogboot/pxe/clients/clients.component.html | 32 ------ .../pxe/clients/clients.component.spec.ts | 70 ------------ .../ogboot/pxe/clients/clients.component.ts | 107 ------------------ .../create-pxe-template.component.ts | 5 +- .../components/ogboot/pxe/pxe.component.css | 31 +++-- .../components/ogboot/pxe/pxe.component.html | 37 ++---- .../components/ogboot/pxe/pxe.component.ts | 89 +++++---------- .../og-dhcp-subnets.component.html | 1 - ogWebconsole/src/locale/en.json | 2 +- ogWebconsole/src/locale/es.json | 2 +- 19 files changed, 73 insertions(+), 646 deletions(-) delete mode 100644 ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.css delete mode 100644 ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.html delete mode 100644 ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.spec.ts delete mode 100644 ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.ts delete mode 100644 ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.css delete mode 100644 ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.html delete mode 100644 ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.spec.ts delete mode 100644 ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.ts diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index 62de434..9ec2f42 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -113,8 +113,6 @@ import { CreateSoftwareProfileComponent } from './components/software-profile/cr import { OperativeSystemComponent } from './components/operative-system/operative-system.component'; import { CreateOperativeSystemComponent } from './components/operative-system/create-operative-system/create-operative-system.component'; import { ShowTemplateContentComponent } from './components/ogboot/pxe/show-template-content/show-template-content.component'; -import { AddClientsToPxeComponent } from './components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component'; -import { ClientsComponent } from './components/ogboot/pxe/clients/clients.component'; import { RepositoriesComponent } from './components/repositories/repositories.component'; import { CreateRepositoryComponent } from './components/repositories/create-repository/create-repository.component'; import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component'; @@ -200,8 +198,6 @@ export function HttpLoaderFactory(http: HttpClient) { OperativeSystemComponent, CreateOperativeSystemComponent, ShowTemplateContentComponent, - AddClientsToPxeComponent, - ClientsComponent, RepositoriesComponent, CreateRepositoryComponent, ExecuteCommandComponent, diff --git a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.css b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.css index 449d540..7d92813 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.css +++ b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.css @@ -42,7 +42,6 @@ table { align-items: center; height: 100px; padding: 10px; - margin-top: 16px; } .mat-elevation-z8 { @@ -55,21 +54,15 @@ table { margin-bottom: 30px; } -.example-headers-align .mat-expansion-panel-header-description { - justify-content: space-between; - align-items: center; +.images-button-row { + display: flex; + justify-content: flex-start; + margin-top: 16px; + gap: 10px; } -.example-headers-align .mat-mdc-form-field + .mat-mdc-form-field { - margin-left: 8px; -} - -.button-row { - display: table-cell; - max-width: 600px; -} - -.button-row .mat-mdc-button-base { - margin: 8px 8px 8px 0; +mat-spinner { + margin: 0 auto; + align-self: center; } diff --git a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html index 0c0a30a..dd66093 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html +++ b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html @@ -1,17 +1,3 @@ - - - - {{ 'serverInfoTitle' | translate }} - -
- -
-
- -
-
-
-
@@ -35,7 +22,7 @@ search {{ 'searchHint' | translate }} - + {{ 'searchDefaultLabel' | translate }} @@ -55,7 +42,8 @@
- + +
- + + + + +
{{ column.header }} @@ -78,7 +66,7 @@ - {{ image.name ? image.name.substring(0, 20) + '...' : '' }} + {{ image.name }} @@ -95,8 +83,8 @@ - {{ 'actionsColumnHeader' | translate }} + {{ 'actionsColumnHeader' | translate }} @@ -110,7 +98,7 @@ menu - + diff --git a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.ts b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.ts index 0015fb1..25125b5 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.ts @@ -44,11 +44,6 @@ export class PXEimagesComponent implements OnInit { header: 'Nombre de imagen', cell: (user: any) => `${user.name}` }, - { - columnDef: 'downloadUrl', - header: 'Url descarga', - cell: (user: any) => `${user.downloadUrl}` - }, { columnDef: 'isDefault', header: 'Imagen por defecto', @@ -56,7 +51,7 @@ export class PXEimagesComponent implements OnInit { }, { columnDef: 'installed', - header: 'Imagen instalada en ogBoot', + header: 'Imagen instalada', cell: (user: any) => `${user.installed}` }, { @@ -83,8 +78,11 @@ export class PXEimagesComponent implements OnInit { ) {} ngOnInit(): void { + this.loading = true; this.search(); this.loadAlert(); + this.syncOgBoot() + this.loading = false; } addImage(): void { @@ -99,15 +97,12 @@ export class PXEimagesComponent implements OnInit { } search(): void { - this.loading = true; this.dataService.getImages(this.filters).subscribe( data => { this.dataSource.data = data; - this.loading = false; }, error => { console.error('Error fetching og lives', error); - this.loading = false; } ); } @@ -250,7 +245,7 @@ export class PXEimagesComponent implements OnInit { syncOgBoot(): void { this.http.post(`${this.apiUrl}/sync`, {}) .subscribe(response => { - this.toastService.success('Sincronización completada'); + this.toastService.success('Sincronización con oGBoot exitosa'); this.search() }, error => { console.error('Error al sincronizar', error); @@ -261,7 +256,6 @@ export class PXEimagesComponent implements OnInit { iniciarTour(): void { this.joyrideService.startTour({ steps: [ - 'serverInfoStep', 'titleStep', 'addImageStep', 'searchNameStep', @@ -275,5 +269,5 @@ export class PXEimagesComponent implements OnInit { themeColor: '#3f51b5' }); } - + } diff --git a/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.css b/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.css deleted file mode 100644 index f994fc3..0000000 --- a/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.css +++ /dev/null @@ -1,17 +0,0 @@ -.loading-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100px; -} - -mat-form-field { - width: 100%; -} - -mat-dialog-actions { - display: flex; - justify-content: flex-end; -} - diff --git a/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.html b/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.html deleted file mode 100644 index d72e22f..0000000 --- a/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.html +++ /dev/null @@ -1,39 +0,0 @@ -

{{ 'addClientsTitle' | translate: { subnetName: data.subnetName } }}

- - - - - - - {{ client.name }} - - - - -
-

{{ 'selectedClientsTitle' | translate }}

-
    -
  • - {{ client.name }} - -
  • -
-
-
- - - - - diff --git a/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.spec.ts b/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.spec.ts deleted file mode 100644 index 0a4cd09..0000000 --- a/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { AddClientsToPxeComponent } from './add-clients-to-pxe.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatOptionModule } from '@angular/material/core'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatPaginatorModule } from '@angular/material/paginator'; -import { MatProgressSpinner, MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatSelectModule } from '@angular/material/select'; -import { MatTableModule } from '@angular/material/table'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ToastrModule } from 'ngx-toastr'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { MatListModule } from '@angular/material/list'; -import { MatTabsModule } from '@angular/material/tabs'; -import { TranslateModule } from '@ngx-translate/core'; - -describe('AddClientsToPxeComponent', () => { - let component: AddClientsToPxeComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [AddClientsToPxeComponent], - imports: [ - HttpClientTestingModule, - ToastrModule.forRoot(), - BrowserAnimationsModule, - MatDividerModule, - MatFormFieldModule, - MatInputModule, - MatIconModule, - MatButtonModule, - MatTableModule, - MatPaginatorModule, - MatTooltipModule, - FormsModule, - ReactiveFormsModule, - MatProgressSpinnerModule, - MatDialogModule, - MatSelectModule, - MatTabsModule, - MatAutocompleteModule, - MatListModule, - TranslateModule.forRoot() - ], - providers: [ - { provide: MatDialogRef, useValue: {} }, - { provide: MAT_DIALOG_DATA, useValue: {} } - ] - }) - .compileComponents(); - - fixture = TestBed.createComponent(AddClientsToPxeComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.ts b/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.ts deleted file mode 100644 index 7a349d1..0000000 --- a/ogWebconsole/src/app/components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -import {Component, Inject} from '@angular/core'; -import {Observable, startWith} from "rxjs"; -import {FormControl} from "@angular/forms"; -import {HttpClient} from "@angular/common/http"; -import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; -import {ToastrService} from "ngx-toastr"; -import {map} from "rxjs/operators"; - -@Component({ - selector: 'app-add-clients-to-pxe', - templateUrl: './add-clients-to-pxe.component.html', - styleUrl: './add-clients-to-pxe.component.css' -}) -export class AddClientsToPxeComponent { - baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; - clients: any[] = []; - selectedClients: any[] = []; - loading: boolean = true; - filters: { [key: string]: string } = {}; - filteredClients!: Observable; - clientControl = new FormControl(); - - constructor( - private http: HttpClient, - public dialogRef: MatDialogRef, - private toastService: ToastrService, - @Inject(MAT_DIALOG_DATA) public data: { subnetUuid: string, subnetName: string } - ) {} - - ngOnInit(): void { - this.loading = true; - - this.loadClients(); - - this.filteredClients = this.clientControl.valueChanges.pipe( - startWith(''), - map(value => (typeof value === 'string' ? value : value?.name)), - map(name => (name ? this._filterClients(name) : this.clients.slice())) - ); - } - - loadClients() { - this.http.get( `${this.baseUrl}/clients?&page=1&itemsPerPage=10000&exists[template]=false`).subscribe( - response => { - this.clients = response['hydra:member']; - this.loading = false; - }, - error => { - console.error('Error fetching parent units:', error); - this.loading = false; - } - ); - } - - save() { - const postData = { - clients: this.selectedClients.map(client => client['@id']) - }; - - this.http.post(`${this.baseUrl}/pxe-templates/${this.data.subnetUuid}/add-clients`, postData).subscribe( - response => { - this.toastService.success('Clientes asignados correctamente'); - }, - error => { - this.toastService.error(error.error['hydra:description']); - } - ); - - this.dialogRef.close(this.selectedClients); - } - - close() { - this.dialogRef.close(); - } - - removeClient(client: any) { - const index = this.selectedClients.indexOf(client); - if (index >= 0) { - this.selectedClients.splice(index, 1); - } - } - - private _filterClients(name: string): any[] { - const filterValue = name.toLowerCase(); - return this.clients.filter(client => client.name.toLowerCase().includes(filterValue)); - } - - displayFnClient(client: any): string { - return client && client.name ? client.name : ''; - } - - onOptionClientSelected(client: any) { - if (!this.selectedClients.includes(client)) { - this.selectedClients.push(client); - } - this.clientControl.setValue(''); - } -} diff --git a/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.css b/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.css deleted file mode 100644 index 4e5aff4..0000000 --- a/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.css +++ /dev/null @@ -1,47 +0,0 @@ - -mat-dialog-actions { - margin-top: 20px; - display: flex; - justify-content: flex-end; -} - -button { - margin-left: 10px; -} - -.green-icon { - color: green; -} - -.red-icon { - color: red; -} - - -.spacing-container { - margin-top: 20px; - margin-bottom: 16px; -} - -.list-item-content { - display: flex; - align-items: flex-start; /* Alinea el contenido al inicio */ - justify-content: space-between; /* Espacio entre los textos y los íconos */ - width: 100%; /* Asegúrate de que el contenido ocupe todo el ancho */ -} - -.text-content { - flex-grow: 1; - margin-right: 16px; - margin-left: 10px; -} - -.icon-container { - display: flex; - align-items: center; -} - -.right-icon { - margin-left: 8px; - cursor: pointer; -} diff --git a/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.html b/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.html deleted file mode 100644 index 7a75d2e..0000000 --- a/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.html +++ /dev/null @@ -1,32 +0,0 @@ -

{{ 'manageClientsTitle' | translate }}

- - - - -
- - computer - -
-
{{ client.name }}
-
{{ client.mac }}
-
-
- - - -
-
-
-
-
-
- - - diff --git a/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.spec.ts b/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.spec.ts deleted file mode 100644 index d1b206f..0000000 --- a/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ClientsComponent } from './clients.component'; -import { MatInputModule } from '@angular/material/input'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatOptionModule } from '@angular/material/core'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatPaginatorModule } from '@angular/material/paginator'; -import { MatProgressSpinner, MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatSelectModule } from '@angular/material/select'; -import { MatTableModule } from '@angular/material/table'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ToastrModule } from 'ngx-toastr'; -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatListModule } from '@angular/material/list'; -import { MatTabsModule } from '@angular/material/tabs'; -import { TranslateModule } from '@ngx-translate/core'; - -describe('ClientsComponent', () => { - let component: ClientsComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ClientsComponent], - imports: [ - HttpClientTestingModule, - ToastrModule.forRoot(), - BrowserAnimationsModule, - MatDividerModule, - MatFormFieldModule, - MatInputModule, - MatIconModule, - MatButtonModule, - MatTableModule, - MatPaginatorModule, - MatTooltipModule, - FormsModule, - ReactiveFormsModule, - MatProgressSpinnerModule, - MatDialogModule, - MatSelectModule, - MatTabsModule, - MatAutocompleteModule, - MatListModule, - TranslateModule.forRoot() - ], - providers: [ - { provide: MatDialogRef, useValue: {} }, - { provide: MAT_DIALOG_DATA, useValue: {data: {id: 123}} } - - ] - }) - .compileComponents(); - - fixture = TestBed.createComponent(ClientsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.ts b/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.ts deleted file mode 100644 index c479610..0000000 --- a/ogWebconsole/src/app/components/ogboot/pxe/clients/clients.component.ts +++ /dev/null @@ -1,107 +0,0 @@ -import {Component, Inject} from '@angular/core'; -import {FormBuilder, FormGroup, Validators} from "@angular/forms"; -import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog"; -import {HttpClient} from "@angular/common/http"; -import {ToastrService} from "ngx-toastr"; -import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component"; -import {Observable} from "rxjs"; -import { - ServerInfoDialogComponent -} from "../../../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component"; - -@Component({ - selector: 'app-clients', - templateUrl: './clients.component.html', - styleUrl: './clients.component.css' -}) -export class ClientsComponent { - baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; - templateForm!: FormGroup; - clients: any[] = []; - alertMessage: string | null = null; - - constructor( - public dialogRef: MatDialogRef, - public dialog: MatDialog, - private http: HttpClient, - private toastService: ToastrService, - @Inject(MAT_DIALOG_DATA) public data: any - ) {} - - ngOnInit() { - this.getPxeClients() - } - - getPxeClients(): void { - this.http.get(`${this.baseUrl}/clients?template.id=${this.data.data.id}`).subscribe({ - next: data => { - this.clients = data['hydra:member'] - }, - error: error => { - console.error('Error al obtener los clientes PXE:', error); - } - }); - } - - addClientToTemplate(client: any): void { - const postData = { - client: client['@id'] - }; - - this.http.post(`${this.baseUrl}/pxe-templates/${this.data.data.uuid}/sync-client`, postData).subscribe( - response => { - this.toastService.success('Clientes asignados correctamente'); - this.getPxeClients() - }, - error => { - this.toastService.error(error.error['hydra:description']); - } - ); - } - - loadAlert(client: any): Observable { - return this.http.post(`${this.baseUrl}/clients/server/${client.uuid}/get-pxe`, {}); - } - - showInfo(client:any) { - this.loadAlert(client).subscribe( - response => { - this.alertMessage = response.message; - - this.dialog.open(ServerInfoDialogComponent, { - width: '600px', - data: { - message: this.alertMessage - } - }); - }, - error => { - console.error('Error al cargar la información del alert', error); - } - ); - } - - deleteClient(client: any): void { - const dialogRef = this.dialog.open(DeleteModalComponent, { - width: '300px', - data: { name: client.name } - }); - - dialogRef.afterClosed().subscribe(result => { - if (result) { - this.http.post(`${this.baseUrl}/pxe-templates/${this.data.data.uuid}/delete-client`, { client: client['@id'] }).subscribe({ - next: () => { - this.toastService.success('Cliente eliminado exitosamente'); - this.dialogRef.close(); - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - } - }); - }}) - } - - onCancel(): void { - this.dialogRef.close(false); - } -} diff --git a/ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.ts b/ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.ts index 14d208a..2cded87 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.ts @@ -34,7 +34,7 @@ set ISODIR ogLive kernel http://__SERVERIP__/tftpboot/\${ISODIR}/ogvmlinuz \${kernelargs} initrd http://__SERVERIP__/tftpboot/\${ISODIR}/oginitrd.img boot`, - + disco: `#!ipxe iseq \${platform} efi && goto uefi_boot || goto bios_boot @@ -104,8 +104,7 @@ exit` this.dialogRef.close(true); }, error: error => { - this.toastService.error('Error al crear la plantilla PXE'); - this.dialogRef.close(false); + this.toastService.error(error.error['hydra:description']); } }); } diff --git a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.css b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.css index b9ee046..b478ce5 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.css +++ b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.css @@ -34,6 +34,7 @@ table { display: flex; justify-content: space-between; align-items: center; + height: 100px; padding: 10px; } @@ -41,27 +42,21 @@ table { box-shadow: 0px 0px 0px rgba(0,0,0,0.2); } +.template-button-row { + display: flex; + justify-content: flex-start; + margin-top: 16px; + gap: 10px; +} + +mat-spinner { + margin: 0 auto; + align-self: center; +} + .paginator-container { display: flex; justify-content: end; margin-bottom: 30px; } -.example-headers-align .mat-expansion-panel-header-description { - justify-content: space-between; - align-items: center; -} - -.example-headers-align .mat-mdc-form-field + .mat-mdc-form-field { - margin-left: 8px; -} - -.example-button-row { - display: table-cell; - max-width: 600px; -} - -.example-button-row .mat-mdc-button-base { - margin: 8px 8px 8px 0; -} - diff --git a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html index f2fb59b..303362e 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html +++ b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html @@ -1,23 +1,10 @@ - - - - {{ 'serverInfoTitle' | translate }} - -
- -
-
- -
-
-
-

{{ 'adminPxeTitle' | translate }}

-
+
+
@@ -41,7 +28,8 @@
- + +
- + diff --git a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts index 40dc2c1..c0ff7a7 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts @@ -6,21 +6,12 @@ import { MatTableDataSource } from '@angular/material/table'; import { PageEvent } from '@angular/material/paginator'; import { ToastrService } from 'ngx-toastr'; import { DatePipe } from '@angular/common'; -import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component'; import { DataService } from './data.service'; -import { - ShowOrganizationalUnitComponent -} from "../../groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component"; import {ShowTemplateContentComponent} from "./show-template-content/show-template-content.component"; import {ServerInfoDialogComponent} from "../../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component"; -import { - AddClientsToSubnetComponent -} from "../../ogdhcp/og-dhcp-subnets/add-clients-to-subnet/add-clients-to-subnet.component"; -import {Subnet} from "../../ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component"; -import {AddClientsToPxeComponent} from "./add-clients-to-pxe/add-clients-to-pxe.component"; import {Observable} from "rxjs"; -import {ClientsComponent} from "./clients/clients.component"; import { JoyrideService } from 'ngx-joyride'; +import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component"; @Component({ selector: 'app-pxe', @@ -78,19 +69,20 @@ export class PxeComponent { ) { } ngOnInit(): void { + this.loading = true; this.search(); + this.loadAlert() + this.syncTemplates() + this.loading = false; } search(): void { - this.loading = true; this.dataService.getPxeTemplates(this.filters).subscribe( data => { this.dataSource.data = data; - this.loading = false; }, error => { console.error('Error fetching pxe templates', error); - this.loading = false; } ); } @@ -105,6 +97,7 @@ export class PxeComponent { }); } + editPxeTemplate(template: any) { const dialogRef = this.dialog.open(CreatePxeTemplateComponent, { data: template, // Pasa los datos del template para edición @@ -118,40 +111,26 @@ export class PxeComponent { toggleAction(image: any, action: string): void { switch (action) { - case 'create': - this.http.post(`${this.apiUrl}/server/${image.uuid}/post`, {}).subscribe({ - next: (response) => { - this.search(); - // @ts-ignore - this.toastService.success(response.success); - }, - error: (error) => { - this.toastService.error(error.error.error); - } - }); - break; - case 'sync': - this.http.get(`${this.apiUrl}/server/${image.uuid}/get`, {}).subscribe({ - next: (response) => { - this.search(); - // @ts-ignore - this.toastService.success(response.success); - }, - error: (error) => { - this.toastService.error(error.error.error); - } - }); - break; case 'delete': - this.http.post(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({ - next: () => { - this.toastService.success('Plantilla eliminada correctamente'); - this.search(); - }, - error: (error) => { - this.toastService.error(error.error.error); - } + const dialogRef = this.dialog.open(DeleteModalComponent, { + width: '300px', + data: { name: image.name } }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.http.post(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({ + next: () => { + this.toastService.success('Plantilla eliminada exitosamente'); + this.search(); + }, + error: (error) => { + console.error('Error al eliminar la subred', error); + this.toastService.error(error.error['hydra:description']); + } + }); + } + }) break; default: console.error('Acción no soportada:', action); @@ -164,11 +143,6 @@ export class PxeComponent { const dialogRef = this.dialog.open(ShowTemplateContentComponent, { data: { data }, width: '700px'}); } - editClients(event: MouseEvent, data: any): void { - event.stopPropagation(); - const dialogRef = this.dialog.open(ClientsComponent, { data: { data }, width: '700px'}); - } - syncTemplates() { this.http.post(`${this.apiUrl}/sync`, {}) .subscribe(response => { @@ -180,17 +154,6 @@ export class PxeComponent { }); } - addClientsToPxe(template: Subnet) { - const dialogRef = this.dialog.open(AddClientsToPxeComponent, { - width: '600px', - data: { subnetUuid: template.uuid, subnetName: template.name } - }); - - dialogRef.afterClosed().subscribe(result => { - this.search(); - }); - } - applyFilter() { this.http.get(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({ next: (response) => { @@ -207,7 +170,7 @@ export class PxeComponent { return this.http.get(`${this.apiUrl}/server/get-collection`); } - openSubnetInfoDialog() { + openInfoDialog() { this.loadAlert().subscribe( response => { this.alertMessage = response.message; @@ -248,5 +211,5 @@ export class PxeComponent { themeColor: '#3f51b5' }); } - + } diff --git a/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component.html b/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component.html index 903ebd5..1338179 100644 --- a/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component.html +++ b/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component.html @@ -38,7 +38,6 @@ -
{{ column.header }} @@ -57,26 +45,17 @@ - {{ 'actionsColumn' | translate }} + {{ 'actionsColumn' | translate }} - - - - - - - -
diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index 0cc49e6..4bb7273 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -6,7 +6,7 @@ "loginError": "Login error: {{error}}", "labelUsers": "Users", "labelRoles": "Roles", - "adminImagesTitle": "Manage clients", + "adminImagesTitle": "Manage images", "addUser": "Add users", "searchLabel": "Search image name", "searchPlaceholder": "Search", diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index 784d4b6..bec0ac1 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -6,7 +6,7 @@ "loginError": "Login error: {{error}}", "labelUsers": "Usuarios", "labelRoles": "Roles", - "adminImagesTitle": "Administrar clientes", + "adminImagesTitle": "Administrar OgLives", "addUser": "Añadir usuarios", "searchLabel": "Buscar nombre de imagen", "searchPlaceholder": "Búsqueda", From 4d8c1f09913aded26bf596c96ad9d76c835d7232 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Wed, 27 Nov 2024 16:49:49 +0100 Subject: [PATCH 02/17] Updated client view --- .../client-main-view.component.css | 13 -- .../client-main-view.component.html | 115 ++++++++---------- .../client-main-view.component.ts | 8 +- .../deploy-image/deploy-image.component.html | 4 +- .../deploy-image/deploy-image.component.ts | 8 +- 5 files changed, 63 insertions(+), 85 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css index d174de6..c86bb98 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css @@ -5,16 +5,6 @@ padding: 10px; } -.client-header { - display: flex; - align-items: center; - margin-bottom: 10px; - background-color: #fff; - padding: 20px; - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - .client-icon { flex-shrink: 0; margin-right: 20px; @@ -63,7 +53,6 @@ justify-content: center; } - .icon-pc { font-size: 25px; color: #3b82f6; @@ -85,9 +74,7 @@ } .info-section { - margin-bottom: 30px; background-color: #fff; - padding: 20px; border-radius: 12px; } diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html index 44a7f85..a78d6f8 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html @@ -12,71 +12,64 @@ - - -
- -
+
+ +
-
-
-
-
-
{{ clientData?.property }}
-
{{ clientData?.value }}
-
-
-
-
-
{{ clientData?.property }}
-
{{ clientData?.value }}
-
-
+
+
+
+
+
{{ clientData?.property }}
+
{{ clientData?.value }}
- - -
-

Discos/Particiones

+
+
+
{{ clientData?.property }}
+
{{ clientData?.value }}
+
+
+
+
+

Discos/Particiones

+
-
-
- - - +
+
{{ column.header }} - - {{ column.cell(image) }} - - - - {{ (image.size / 1024).toFixed(2) }} GB - - -
+ + + - -
{{ column.header }} + + {{ column.cell(image) }} -
+ + + {{ (image.size / 1024).toFixed(2) }} GB + + +
+ + +
+ +
+
+ + + +

Disco {{ disk.diskNumber }}

+

Usado: {{ (disk.used).toFixed(2) }} GB ({{ disk.percentage }}%)

+

Total: {{ disk.total }} GB

+
- -
- -
-
- - - -

Disco {{ disk.diskNumber }}

-

Usado: {{ (disk.used).toFixed(2) }} GB ({{ disk.percentage }}%)

-

Total: {{ disk.total }} GB

-
-
-
-
- - - +
+
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 83a7d83..78b9a40 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 @@ -110,26 +110,22 @@ export class ClientMainViewComponent implements OnInit { updateGeneralData() { this.generalData = [ { property: 'Nombre', value: this.clientData?.name || '' }, - { property: 'Uuid', value: this.clientData?.uuid || '' }, { property: 'IP', value: this.clientData?.ip || '' }, { property: 'MAC', value: this.clientData?.mac || '' }, { property: 'Nº de serie', value: this.clientData?.serialNumber || '' }, - { property: 'Netiface', value: this.clientData?.netiface || '' }, + { property: 'Netiface', value: this.clientData?.netiface || this.clientData?.organizationalUnit?.networkSettings?.netiface || '' }, { property: 'Perfil hardware', value: this.clientData?.hardwareProfile?.description || '' }, - { property: 'Menú', value: this.clientData?.menu?.description || '' }, ]; } updateNetworkData() { this.networkData = [ + { property: 'Pxe', value: this.clientData?.template?.name || '' }, { property: 'Remote Pc', value: this.clientData.remotePc || '' }, { property: 'Subred', value: this.clientData?.subnet || '' }, { property: 'OGlive', value: this.clientData?.ogLive?.name || '' }, { property: 'Autoexec', value: '' }, { property: 'Repositorio', value: this.clientData?.repository?.name || '' }, - { property: 'Validación', value: this.clientData?.organizationalUnit?.networkSettings?.validation || '' }, - { property: 'Pxe', value: this.clientData?.template?.name || '' }, - { property: 'Creado por', value: this.clientData?.createdBy || '' } ]; } diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html index 5762385..e514d0b 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html @@ -1,5 +1,5 @@
-

Deploy imagen en {{ clientName }}

+

Desplegar imagen en {{ clientName }}

@@ -25,7 +25,7 @@ Seleccione método de deploy - {{ method }} + {{ method }}
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts index 58623ea..2c48a74 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts @@ -3,7 +3,7 @@ import {MatTableDataSource} from "@angular/material/table"; import {SelectionModel} from "@angular/cdk/collections"; import {HttpClient} from "@angular/common/http"; import {ToastrService} from "ngx-toastr"; -import {ActivatedRoute} from "@angular/router"; +import {ActivatedRoute, Router} from "@angular/router"; @Component({ selector: 'app-deploy-image', @@ -92,7 +92,8 @@ export class DeployImageComponent { constructor( private http: HttpClient, private toastService: ToastrService, - private route: ActivatedRoute + private route: ActivatedRoute, + private router: Router, ) {} ngOnInit() { @@ -134,7 +135,7 @@ export class DeployImageComponent { } loadImages() { - const url = `${this.baseUrl}/images?page=1&itemsPerPage=1000`; + const url = `${this.baseUrl}/images?status=success&page=1&itemsPerPage=1000`; this.http.get(url).subscribe( (response: any) => { this.images = response['hydra:member']; @@ -162,6 +163,7 @@ export class DeployImageComponent { .subscribe({ next: (response) => { this.toastService.success('Imagen creada exitosamente'); + this.router.navigate(['/images']); }, error: (error) => { console.error('Error:', error); From d8dad3b14b1a43249c9d83cb0cfaa97a5b0785fa Mon Sep 17 00:00:00 2001 From: apuente Date: Thu, 28 Nov 2024 17:32:19 +0100 Subject: [PATCH 03/17] Refactor groups --- ogWebconsole/package-lock.json | 16 + ogWebconsole/package.json | 2 + .../components/groups/groups.component.css | 497 ++++++++--------- .../components/groups/groups.component.html | 317 ++++++----- .../app/components/groups/groups.component.ts | 519 ++++++------------ .../src/app/components/groups/model/model.ts | 3 + .../groups/services/data.service.ts | 12 +- .../create-client/create-client.component.css | 60 +- .../create-client.component.html | 189 ++++--- .../create-client/create-client.component.ts | 109 ++-- 10 files changed, 888 insertions(+), 836 deletions(-) diff --git a/ogWebconsole/package-lock.json b/ogWebconsole/package-lock.json index befd096..461ff5a 100644 --- a/ogWebconsole/package-lock.json +++ b/ogWebconsole/package-lock.json @@ -24,6 +24,7 @@ "jwt-decode": "^4.0.0", "ngx-joyride": "^2.5.0", "ngx-toastr": "^19.0.0", + "papaparse": "^5.4.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "^0.14.6" @@ -35,6 +36,7 @@ "@angular/localize": "^18.1.0", "@ngx-env/builder": "^18.0.1", "@types/jasmine": "~5.1.0", + "@types/papaparse": "^5.3.15", "jasmine-core": "~5.1.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", @@ -5902,6 +5904,15 @@ "@types/node": "*" } }, + "node_modules/@types/papaparse": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.15.tgz", + "integrity": "sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -11955,6 +11966,11 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/ogWebconsole/package.json b/ogWebconsole/package.json index e3edac1..a2a5595 100644 --- a/ogWebconsole/package.json +++ b/ogWebconsole/package.json @@ -26,6 +26,7 @@ "jwt-decode": "^4.0.0", "ngx-joyride": "^2.5.0", "ngx-toastr": "^19.0.0", + "papaparse": "^5.4.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "^0.14.6" @@ -37,6 +38,7 @@ "@angular/localize": "^18.1.0", "@ngx-env/builder": "^18.0.1", "@types/jasmine": "~5.1.0", + "@types/papaparse": "^5.3.15", "jasmine-core": "~5.1.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index b2923f7..f7b4608 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -1,321 +1,324 @@ -.groupLists-container { - display: flex; - flex-wrap: wrap; - height: auto; - margin-bottom: 30px; -} - -.search-container { - display: flex; - flex-grow: 1; - margin: 10px; -} - -.search-container mat-form-field { - width: 50%; -} - -.card { - flex-grow: 1; - margin: 10px; - - border: 2px solid rgba(102, 102, 102, 0.103) +.card-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + padding: 20px; } .header-container { display: flex; justify-content: space-between; align-items: center; - height: 100px; - padding: 10px; -} - -.unidad-card { - flex: 1 1 20%; - background-color: #fafafa; - height: 600px; - overflow-y: auto; - box-shadow: none !important; - -} - -.elements-card { - flex: 1 1 75%; - background-color: #fafafa; - height: 600px; - overflow-y: auto; - box-shadow: none !important; -} - -.element-content { - overflow-y: auto; -} - -.title { - margin-left: 10px; -} - -.details-card, .classroom-view { - flex: 1 1 25%; -} - -mat-card-title { - display: flex; - justify-content: space-between; - margin: 10px; -} - -.title-with-breadcrumb { - display: flex; - flex-direction: column; -} - -mat-card-subtitle { - font-size: 0.875rem; - color: rgba(0, 0, 0, 0.54); -} - -mat-card-subtitle a { - cursor: pointer; - text-decoration: underline; - color: #929292; -} - -mat-card-subtitle a:hover { - text-decoration: none; + padding: 20px; + background-color: #f5f5f5; + border-bottom: 1px solid #ddd; } .groups-button-row { display: flex; - gap: 10px; + gap: 15px; } -.item-content { +.button-container { display: flex; - width: 100%; + justify-content: center; + margin: 20px 0; } -.item-content mat-icon { - margin-right: 10px; +button[mat-raised-button] { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + font-size: 16px; } -.clickable-item:hover { +mat-card { + background-color: #ffffff; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; + overflow: hidden; + align-items: center; +} + +mat-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15); +} + +.unidad-card { cursor: pointer; -} - -.selected-item { - background-color: #e0e0e0; -} - -.actions { + padding: 16px; + font-size: 14px; display: flex; - margin-left: auto; - align-self: center; + flex-direction: column; + justify-content: space-between; +} + +.unidad-card.selected-item { + border: 2px solid #1976d2; +} + +mat-card-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: flex; + align-items: center; + font-size: 16px; + font-weight: bold; + color: #333; + margin: 0; +} + +mat-card-title mat-icon { + font-size: 20px; + margin-right: 8px; + color: #1976d2; +} + +mat-card-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 16px; } .actions mat-icon { - cursor: pointer; - margin-left: 16px; color: #757575; + cursor: pointer; + transition: color 0.2s; } .actions mat-icon:hover { - color: #212121; + color: #1976d2; } .empty-list { display: flex; justify-content: center; align-items: center; - height: 100%; + height: 200px; + font-size: 16px; + color: #777; } -mat-spinner { - margin: 0 auto; - align-self: center; -} - -.container { +.search-container { display: flex; - justify-content: flex-end; -} -.classroomBtn-container { - display: flex; - justify-content: flex-end; - width: 100%; + gap: 20px; + margin: 20px 0; } -.container { - display: flex; - flex-direction: column; -} - -.header { - display: flex; - align-items: center; - gap: 10px; - padding: 20px; -} - -.header mat-form-field { - width: 300px; -} - -.main-content { - display: flex; +.search-container mat-form-field { + flex: 1; } .filters { padding: 20px; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 8px; +} + +.details-container { display: flex; flex-direction: column; - width: 300px; -} - -.saved-filter { - display: flex; - flex-direction: column; - width: 300px; - margin-bottom: 10px; - padding: 10px; -} - -.results { - width: 100%; -} - -.results-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 16px; - margin-bottom: 16px; -} - -.result-card { - width: 100%; - max-width: 250px; - height: 250px; -} -.paginator-container { - display: flex; - justify-content: center; - margin-bottom: 30px; - -} - -.divider { - margin: 20px 0; -} - -mat-card { - margin-bottom: 20px; -} - -.mat-tooltip { - white-space: pre-line; -} - -.classroom-grid { - display: flex; - flex-wrap: wrap; - gap: 16px; - justify-content: flex-start; /* Opcional: para alinear a la izquierda */ -} - -.classroom-item { - flex: 0 1 calc(16.66% - 16px); /* 6 columnas */ - max-width: calc(16.66% - 16px); + gap: 20px; + padding: 20px; + align-items: center; text-align: center; - box-sizing: border-box; } -.classroom-pc { - position: relative; +.details-placeholder { + background-color: #f5f5f5; + border: 1px solid #ddd; + border-radius: 8px; + padding: 20px; + width: 100%; + max-width: 600px; +} + +button[mat-raised-button] { + align-self: flex-start; +} + + +@media (max-width: 1024px) { + .card-container { + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + } + + .header-container { + flex-direction: column; + gap: 10px; + } + + .groups-button-row { + flex-wrap: wrap; + gap: 10px; + } +} + +@media (max-width: 768px) { + mat-card { + padding: 12px; + } + + .unidad-card { + font-size: 12px; + } +} + +.details-container { display: flex; flex-direction: column; align-items: center; - padding: 8px; - background-color: #f4f4f4; + gap: 20px; + padding: 30px; + background-color: #f9f9f9; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + max-width: 1200px; + margin: 20px auto; +} + +.details-placeholder { + background-color: #ffffff; + border: 1px solid #e0e0e0; + border-radius: 12px; + padding: 20px; + width: 100%; + max-width: 800px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} + +mat-tree { + width: 100%; + background-color: #f9f9f9; + border: 1px solid #ddd; border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 10px; } -.pc-image { - width: 80px; - height: 80px; -} - -.pc-details { - margin-top: 8px; - font-size: 12px; -} - -.client-name { - font-weight: bold; - display: block; -} - -.client-ip, -.client-mac { - color: #666; - font-size: 10px; - display: block; -} - -.pc-actions { - margin-top: 8px; +mat-tree mat-tree-node { display: flex; - justify-content: center; - gap: 8px; + align-items: center; + padding: 10px; + border-radius: 6px; + transition: background-color 0.2s, color 0.2s; + cursor: pointer; } -.pc-og-live { - border: 2px solid #4caf50; +mat-tree mat-tree-node:hover { + background-color: #e3f2fd; } -.pc-busy { - border: 2px solid #ff9800; +mat-tree mat-tree-node button.mat-icon-button { + margin-left: auto; + color: #757575; } -.pc-off { - border: 2px solid #f44336; +mat-tree mat-tree-node button.mat-icon-button:hover { + color: #1976d2; } -.pc-linux { - border: 2px solid #9c27b0; +mat-tree mat-tree-node span { + font-size: 16px; + font-weight: 500; + color: #555; } -.pc-windows { - border: 2px solid #2196f3; +mat-tree mat-tree-node mat-icon { + margin-right: 10px; + color: #757575; + transition: color 0.2s; } -/* Pantallas medianas: 4 columnas */ -@media (max-width: 1024px) { - .classroom-item { - flex: 0 1 calc(25% - 16px); /* 4 columnas */ - } +mat-tree mat-tree-node.expandable mat-icon { + color: black; + cursor: pointer; } -/* Pantallas pequeñas: 2 columnas */ -@media (max-width: 768px) { - .classroom-item { - flex: 0 1 calc(50% - 16px); /* 2 columnas */ - } +mat-tree mat-tree-node.expandable.disabled mat-icon { + color: grey; + opacity: 0.5; + cursor: not-allowed; } -/* Pantallas muy pequeñas: 1 columna */ -@media (max-width: 480px) { - .classroom-item { - flex: 0 1 100%; /* 1 columna */ - } +mat-tree mat-tree-node:hover mat-icon { + color: black; } -.client-text { - font-size: 0.8rem; - color: rgba(0, 0, 0, 0.54); +/* Iconos por tipo */ +mat-tree mat-tree-node mat-icon.node-icon { + color: #757575; + margin-right: 10px; } -.client-name { - font-size: 0.9rem; - text-align: center; +mat-tree mat-tree-node mat-icon.node-icon.organizational-unit { + color: #1976d2; /* Azul para unidades organizativas */ +} + +mat-tree mat-tree-node mat-icon.node-icon.classroom { + color: #388e3c; /* Verde para aulas */ +} + +mat-tree mat-tree-node mat-icon.node-icon.client { + color: #f57c00; /* Naranja para clientes */ +} + +mat-tree mat-tree-node mat-icon.node-icon.group { + color: #d32f2f; /* Rojo para grupos */ +} + + +mat-tree mat-tree-node button.mat-icon-button { + margin-right: 10px; +} + +mat-tree mat-tree-node button.mat-icon-button.disabled-toggle { + color: grey; + opacity: 0.5; + cursor: not-allowed; +} + +mat-tree mat-tree-node button.mat-icon-button.disabled-toggle:hover { + background-color: transparent; /* Desactiva hover */ +} + +mat-tree mat-tree-node mat-icon { + margin-right: 10px; + color: #757575; + transition: color 0.2s; +} + +mat-tree mat-tree-node mat-icon.node-icon.organizational-unit { + color: #1976d2; /* Azul para unidades organizativas */ +} + +mat-tree mat-tree-node mat-icon.node-icon.classroom { + color: #388e3c; /* Verde para aulas */ +} + +mat-tree mat-tree-node mat-icon.node-icon.client { + color: #f57c00; /* Naranja para clientes */ +} + +mat-tree mat-tree-node mat-icon.node-icon.group { + color: #d32f2f; /* Rojo para grupos */ +} + +mat-tree mat-tree-node:hover { + background-color: #e3f2fd; + cursor: pointer; +} + +mat-tree mat-tree-node.disabled { + cursor: not-allowed; +} + +mat-tree mat-tree-node.disabled:hover { + background-color: transparent; /* Desactiva hover */ } diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 2de059a..d2d2d6f 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -4,142 +4,199 @@ -

{{ 'adminGroupsTitle' | translate }}

+

+ {{ 'adminGroupsTitle' | translate }} +

- - - + + +
-
- - {{ 'organizationalUnitTitle' | translate }} - - - - -
- apartment - {{ unidad.name }} - - more_vert - - - - - - - - - - - - -
-
-
-
-
- - - -
- {{ 'internalElementsTitle' | translate }} - - - {{ crumb }} - > - - + +
+ + + + apartment {{ unidad.name }} + + + +
+
- - - - - - -
- info - {{ 'noInternalElementsMessage' | translate }} -
- -
- - apartment - meeting_room - school - computer - lan - help_outline - - {{ child.name }} -
- more_vert - - - - -
-
-
-
- - -
-
-
- PC Icon -
- {{ pc.name }} - {{ pc.ip }} - {{ pc.mac }} -
-
- - -
-
-
-
- -
+ + + + + + + +
+ + + + +
+
+

{{ 'Details of' | translate }} {{ selectedUnidad?.name }}

+ + + + + + + {{ + node.type === 'organizational-unit' + ? 'apartment' + : node.type === 'classroom' + ? 'school' + : node.type === 'client' + ? 'computer' + : 'group' + }} + + {{ node.name }} ({{ node.type }}) + + + + + + + {{ + node.type === 'organizational-unit' + ? 'apartment' + : node.type === 'classroom' + ? 'school' + : node.type === 'client' + ? 'computer' + : 'group' + }} + + {{ node.name }} ({{ node.type }}) + + - IP: {{ node.ip }} + + + + + + + + {{ + node.type === 'organizational-unit' + ? 'apartment' + : node.type === 'classroom' + ? 'school' + : node.type === 'client' + ? 'computer' + : 'group' + }} + + {{ node.name }} ({{ node.type }}) + + - IP: {{ node.ip }} + + + + + + + + computer + device_hub + {{ node.name }} ({{ node.type }}) + + - IP: {{ node.ip }} + + + + + + + + + + + + + +
+
+
+ + @@ -153,4 +210,4 @@ - + \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 3066539..ecd89a0 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -1,33 +1,38 @@ -import {Component, OnInit, ViewChild} from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { DataService } from './services/data.service'; -import { ClientCollection, UnidadOrganizativa } from './model/model'; +import { UnidadOrganizativa } from './model/model'; import { MatDialog } from '@angular/material/dialog'; +import { MatBottomSheet } from '@angular/material/bottom-sheet'; +import { MatTabChangeEvent } from '@angular/material/tabs'; +import { JoyrideService } from 'ngx-joyride'; +import { FlatTreeControl } from '@angular/cdk/tree'; +import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; + import { CreateOrganizationalUnitComponent } from './shared/organizational-units/create-organizational-unit/create-organizational-unit.component'; -import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component'; import { CreateClientComponent } from './shared/clients/create-client/create-client.component'; import { EditOrganizationalUnitComponent } from './shared/organizational-units/edit-organizational-unit/edit-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 {ToastrService} from "ngx-toastr"; -import {TreeViewComponent} from "./shared/tree-view/tree-view.component"; -import {MatBottomSheet} from "@angular/material/bottom-sheet"; -import {LegendComponent} from "./shared/legend/legend.component"; -import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal'; -import {HttpClient} from "@angular/common/http"; -import {PageEvent} from "@angular/material/paginator"; -import { SaveFiltersDialogComponent } from './shared/save-filters-dialog/save-filters-dialog.component'; -import { AcctionsModalComponent } from './shared/acctions-modal/acctions-modal.component'; -import {MatTableDataSource} from "@angular/material/table"; -import {DatePipe} from "@angular/common"; -import {AdvancedSearchComponent} from "./components/advanced-search/advanced-search.component"; -import {MatTabChangeEvent} from "@angular/material/tabs"; -import {ClientTabViewComponent} from "./components/client-tab-view/client-tab-view.component"; -import { - OrganizationalUnitTabViewComponent -} from "./components/organizational-unit-tab-view/organizational-unit-tab-view.component"; -import { ExecuteCommandComponent } from '../commands/main-commands/execute-command/execute-command.component'; -import { ExecuteCommandOuComponent } from './shared/execute-command-ou/execute-command-ou.component'; -import { JoyrideService } from 'ngx-joyride'; +import { ShowOrganizationalUnitComponent } from './shared/organizational-units/show-organizational-unit/show-organizational-unit.component'; +import { TreeViewComponent } from './shared/tree-view/tree-view.component'; +import { LegendComponent } from './shared/legend/legend.component'; +import { ClientTabViewComponent } from './components/client-tab-view/client-tab-view.component'; +import { OrganizationalUnitTabViewComponent } from './components/organizational-unit-tab-view/organizational-unit-tab-view.component'; + +interface TreeNode { + name: string; + type: string; + children?: TreeNode[]; + ip?: string; + '@id'?: string; +} + +interface FlatNode { + name: string; + type: string; + level: number; + expandable: boolean; + ip?: string; +} @Component({ selector: 'app-groups', @@ -36,53 +41,60 @@ import { JoyrideService } from 'ngx-joyride'; }) export class GroupsComponent implements OnInit { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; - dataSource = new MatTableDataSource(); organizationalUnits: UnidadOrganizativa[] = []; selectedUnidad: UnidadOrganizativa | null = null; selectedDetail: any | null = null; - children: any[] = []; - breadcrumb: string[] = []; - clientsData: any[] = []; - breadcrumbData: any[] = []; - loading:boolean = false; - loadingChildren:boolean = false; + loading: boolean = false; + loadingChildren: boolean = false; searchTerm: string = ''; - selectedFilter1: string = 'none'; - selectedFilter2: string = 'none'; - selectedFilterOS: string[] = []; - selectedFilterStatus: string[] = []; - filterIP: string = ''; - filterMAC: string = ''; - filterName: string = ''; - filteredResults: any[] = []; - savedFilterNames: any[] = []; - length: number = 0; - itemsPerPage: number = 10; - page: number = 1; - pageSizeOptions: number[] = [5, 10, 25, 100]; - selectedElements: any[] = []; - isAllSelected: boolean = false; - filters: { [key: string]: string } = {}; - datePipe: DatePipe = new DatePipe('es-ES'); + + treeControl: FlatTreeControl; + treeFlattener: MatTreeFlattener; + treeDataSource: MatTreeFlatDataSource; + selectedNode: TreeNode | null = null; + + @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; + @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; constructor( - private dataService: DataService, - public dialog: MatDialog, - private toastService: ToastrService, - private _bottomSheet: MatBottomSheet, - private http: HttpClient, - private joyrideService: JoyrideService - ) {} + private dataService: DataService, + public dialog: MatDialog, + private _bottomSheet: MatBottomSheet, + private joyrideService: JoyrideService + ) { + this.treeFlattener = new MatTreeFlattener( + (node: TreeNode, level: number) => ({ + name: node.name, + type: node.type, + level, + expandable: !!node.children?.length, + ip: node.ip, + ['@id']: node['@id'] + }), + node => node.level, + node => node.expandable, + node => node.children + ); + + this.treeControl = new FlatTreeControl( + node => node.level, + node => node.expandable + ); + + this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + } ngOnInit(): void { this.search(); this.getFilters(); } - @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; - @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; + clearSelection(): void { + this.selectedUnidad = null; + this.selectedDetail = null; + } - onTabChange(event: MatTabChangeEvent) { + onTabChange(event: MatTabChangeEvent): void { switch (event.index) { case 2: this.clientTabComponent.search(); @@ -97,12 +109,8 @@ export class GroupsComponent implements OnInit { getFilters(): void { this.dataService.getFilters().subscribe( - data => { - this.savedFilterNames = data.map((filter: any) => [filter.name, filter.uuid]); - }, - error => { - console.error('Error fetching filters:', error); - } + data => {}, + error => console.error('Error fetching filters:', error) ); } @@ -123,67 +131,78 @@ export class GroupsComponent implements OnInit { onSelectUnidad(unidad: UnidadOrganizativa): void { this.selectedUnidad = unidad; this.selectedDetail = unidad; - this.breadcrumb = [unidad.name]; - this.breadcrumbData = [unidad]; - this.loadChildrenAndClients(unidad.id); + + this.loadChildrenAndClients(unidad.id).then(fullData => { + const treeData = this.convertToTreeData(fullData); + this.treeDataSource.data = treeData[0]?.children || []; + }); } - onSelectChild(child: any): void { - this.selectedDetail = child; - if (child.type !== 'client' && child.uuid && child.id) { - this.breadcrumb.push(child.name || child.name); - this.breadcrumbData.push(child); - this.loadChildrenAndClients(child.id); + async loadChildrenAndClients(id: string): Promise { + try { + const childrenData = await this.dataService.getChildren(id).toPromise(); + const clientsData = await this.dataService.getClients(id).toPromise(); + + const processHierarchy = (nodes: UnidadOrganizativa[]): TreeNode[] => { + return nodes.map(node => ({ + name: node.name, + type: node.type, + '@id': node['@id'], + children: [ + ...(node.children ? processHierarchy(node.children) : []), + ...(node.clients + ? node.clients.map((client: any) => ({ + name: client.name, + type: 'client', + '@id': client['@id'], + ip: client.ip + })) + : []) + ], + ip: node.type === 'client' ? (node as any).ip : undefined + })); + }; + + return { + ...this.selectedUnidad, + children: childrenData ? processHierarchy(childrenData) : [], + clients: clientsData || [] + }; + } catch (error) { + console.error('Error loading children and clients:', error); + return this.selectedUnidad; } } - navigateToBreadcrumb(index: number): void { - this.breadcrumb = this.breadcrumb.slice(0, index + 1); - const target = this.breadcrumbData[index]; - this.breadcrumbData = this.breadcrumbData.slice(0, index + 1); - this.selectedDetail = target; - this.loadChildrenAndClients(target.id); + convertToTreeData(data: any): TreeNode[] { + const processNode = (node: UnidadOrganizativa): TreeNode => ({ + name: node.name, + type: node.type, + '@id': node['@id'], + children: [ + ...(node.children?.map(processNode) || []), + ...(node.clients + ? node.clients.map((client: any) => ({ + name: client.name, + type: 'client', + '@id': client['@id'], + ip: client.ip + })) + : []) + ], + ip: node.type === 'client' ? (node as any).ip : undefined + }); + + return [processNode(data)]; } - loadChildrenAndClients(id: string): void { - this.loadingChildren = true - this.dataService.getChildren(id).subscribe( - childrenData => { - this.dataService.getClients(id).subscribe( - clientsData => { - this.clientsData = clientsData; - const newChildren = [...childrenData, ...clientsData]; - - if (newChildren.length > 0) { - this.children = newChildren; - } else { - this.children = []; - } - this.loadingChildren = false - }, - error => { - console.error('Error fetching clients', error); - this.clientsData = []; - this.children = []; - this.loadingChildren = false - } - ); - }, - error => { - console.error('Error fetching children', error); - this.children = []; - this.loadingChildren = false - } - ); - } - - addOU(event: MouseEvent, parent:any = null): void { + addOU(event: MouseEvent, parent: any = null): void { event.stopPropagation(); - const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px'}); + const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px' }); dialogRef.afterClosed().subscribe(() => { this.dataService.getOrganizationalUnits().subscribe( data => { - this.organizationalUnits = data + this.organizationalUnits = data; }, error => console.error('Error fetching unidades organizativas', error) ); @@ -192,262 +211,76 @@ export class GroupsComponent implements OnInit { addClient(event: MouseEvent, organizationalUnit: any = null): void { event.stopPropagation(); - const dialogRef = this.dialog.open(CreateClientComponent, { data: { organizationalUnit }, width: '900px' }); - dialogRef.afterClosed().subscribe(() => { - this.dataService.getOrganizationalUnits().subscribe( - data => { - this.organizationalUnits = data; - - if (organizationalUnit && organizationalUnit.id) { - this.loadChildrenAndClients(organizationalUnit.id); - } - }, - error => console.error('Error fetching unidades organizativas', error) - ); + this.dataService.getOrganizationalUnits().subscribe( + data => { + this.organizationalUnits = data; + if (organizationalUnit && organizationalUnit.id) { + this.loadChildrenAndClients(organizationalUnit.id); + } + }, + error => console.error('Error fetching unidades organizativas', error) + ); }); -} - onDeleteClick(event: MouseEvent, uuid: string, name: string, type: string): void { + } + + setSelectedNode(node: TreeNode): void { + this.selectedNode = node; + } + + onEditNode(event: MouseEvent, node: TreeNode | null): void { + if (!node) return; + + const uuid = node['@id'] ? node['@id'].split('/').pop() : ''; + const type = node.type; + event.stopPropagation(); - if (type === 'client') { - const dialogRef = this.dialog.open(DeleteModalComponent, { - width: '400px', - data: { name } - }); - dialogRef.afterClosed().subscribe(result => { - if (result) { - this.dataService.deleteElement(uuid, type).subscribe( - () => { - this.loadChildrenAndClients(this.selectedUnidad?.id || ''); - this.dataService.getOrganizationalUnits().subscribe( - data => this.organizationalUnits = data, - error => console.error('Error fetching unidades organizativas', error) - ); - this.openSnackBar(false, 'Entidad eliminada exitosamente') - }, - error => { - console.error('Error deleting element', error) - this.openSnackBar(true, error.error['hydra:description']) - } - ); - } - }); + if (type !== 'client') { + this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); } else { - const dialogDeleteGroupRef = this.dialog.open(DeleteModalComponent, { - width: '400px', - data: { name } - }); - - dialogDeleteGroupRef.afterClosed().subscribe(result => { - if (result && result === 'delete') { - this.dataService.deleteElement(uuid, type).subscribe( - () => { - this.loadChildrenAndClients(this.selectedUnidad?.id || ''); - this.dataService.getOrganizationalUnits().subscribe( - data => this.organizationalUnits = data, - error => console.error('Error fetching unidades organizativas', error) - ); - this.openSnackBar(false, 'Entidad eliminada exitosamente') - }, - error => { - console.error('Error deleting element', error) - this.openSnackBar(true, error.error['hydra:description']) - } - ); - } else if (result && result === 'change') { - this.dataService.changeParent(uuid).subscribe( - () => { - this.loadChildrenAndClients(this.selectedUnidad?.id || ''); - this.dataService.getOrganizationalUnits().subscribe( - data => this.organizationalUnits = data, - error => console.error('Error fetching unidades organizativas', error) - ); - this.openSnackBar(false, 'Entidad eliminada exitosamente') - }, - error => { - console.error('Error deleting element', error) - this.openSnackBar(true, error.error['hydra:description']) - } - ); - } - }); + this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); } } + onDelete(node: TreeNode | null): void { + if (!node) return; + // Additional logic for deleting + } + + onCustomAction(node: TreeNode | null): void { + if (!node) return; + // Logic for custom actions + } + onEditClick(event: MouseEvent, type: any, uuid: string): void { event.stopPropagation(); - if (type != "client") { - const dialogRef = this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px'}); + if (type != 'client') { + this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); } else { - const dialogRef = this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' } ); + this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); } } onShowClick(event: MouseEvent, data: any): void { event.stopPropagation(); - if (data.type != "client") { - const dialogRef = this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px'}); + if (data.type != 'client') { + this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px' }); } } onTreeClick(event: MouseEvent, data: any): void { event.stopPropagation(); - if (data.type != "client") { - const dialogRef = this.dialog.open(TreeViewComponent, { data: { data }, width: '800px'}); + if (data.type != 'client') { + this.dialog.open(TreeViewComponent, { data: { data }, width: '800px' }); } } - onExecuteCommand(event: MouseEvent, child: any, name: string, type:string): void { - console.log('Executing command on:', child); - - this.dialog.open(ExecuteCommandOuComponent, { - width: '50%', - data: { childUnitUuid: child } - }).afterClosed().subscribe((result) => { - if (result) { - console.log('Comando ejecutado con éxito'); - } else { - console.log('Ejecución de comando cancelada'); - } - }); - } - - openSnackBar(isError: boolean, message: string) { - if (isError) { - this.toastService.error(' Error al eliminar la entidad: ' + message, 'Error'); - } else - this.toastService.success(message, 'Éxito'); - } - openBottomSheet(): void { this._bottomSheet.open(LegendComponent); } - roomMap(): void { - if (this.selectedDetail && this.selectedDetail.type === 'classroom') { - const dialogRef = this.dialog.open(ClassroomViewDialogComponent, { - width: '90vw', - data: { clients: this.clientsData } - }); - - dialogRef.afterClosed().subscribe(result => { - console.log('The dialog was closed'); - }); - } - } - - applyFilter() { - this.dataService.getFilteredResults(this.selectedFilter1, this.selectedFilter2, this.filterName, this.filterIP, this.filterMAC, this.page, this.itemsPerPage) - .subscribe( - response => { - this.filteredResults = response.results; - this.length = response.total; - }, - error => { - console.error('Error al obtener los resultados filtrados', error); - this.filteredResults = []; - } - ); - } - - onPageChange(event: PageEvent) { - this.page = event.pageIndex; - this.itemsPerPage = event.pageSize; - this.applyFilter(); - } - - saveFilters() { - const dialogRef = this.dialog.open(SaveFiltersDialogComponent); - - dialogRef.afterClosed().subscribe(result => { - if (result) { - const filters = { - name: result, - favourite: true, - filters: { - filter0: this.filterName, - filter1: this.selectedFilter1, - filter2: this.selectedFilter2, - filter3: this.selectedFilterOS, - filter4: this.selectedFilterStatus, - filter5: this.filterIP, - filter6: this.filterMAC, - - } - }; - - this.http.post(`${this.baseUrl}/views`, filters).subscribe(response => { - console.log('Response from server:', response); - this.toastService.success('Se ha guardado el filtro correctamente'); - }, error => { - console.error('Error:', error); - this.toastService.error(error); - }); - } - }); - } - - loadSelectedFilter(savedFilter: any) { - const url = `${this.baseUrl}/views/` + savedFilter[1]; - console.log('llamando a:', url); - - this.dataService.getFilter(savedFilter[1]).subscribe(response => { - console.log('Response from server:', response.filters); - if (response) { - console.log('Filter1:', response.filters); - this.filterName = response.filters.filter0 || ''; - this.selectedFilter1 = response.filters.filter1 || null; - this.selectedFilter2 = response.filters.filter2 || ''; - - this.selectedFilterOS = response.filters.filter3 || []; - this.selectedFilterStatus = response.filters.filter4 || []; - this.filterIP = response.filters.filter5 || ''; - this.filterMAC = response.filters.filter6 || ''; - - this.applyFilter(); - } - }, error => { - console.error('Error:', error); - }); - - } - - - onCheckboxChange(event: any, name: string, uuid: string) { - if (event.checked) { - this.selectedElements.push(uuid); - } else { - const index = this.selectedElements.indexOf(name); - if (index > -1) { - this.selectedElements.splice(index, 1); - } - } - - this.isAllSelected = this.selectedElements.length === this.filteredResults.length; - } - - toggleSelectAll() { - this.isAllSelected = !this.isAllSelected; - - if (this.isAllSelected) { - this.selectedElements = this.filteredResults.map(result => result.uuid); - } else { - this.selectedElements = []; - } - } - - isSelected(name: string): boolean { - return this.selectedElements.includes(name); - - } - - - sendActions() { - const dialogRef = this.dialog.open(AcctionsModalComponent, { data: { selectedElements: this.selectedElements }, width: '700px'}); - } - iniciarTour(): void { this.joyrideService.startTour({ steps: ['groupsTitleStepText', 'addStep', 'keyStep', 'unitStep', 'elementsStep', 'tabsStep'], @@ -455,4 +288,8 @@ export class GroupsComponent implements OnInit { themeColor: '#3f51b5' }); } + + hasChild = (_: number, node: FlatNode): boolean => node.expandable; + isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable; + } diff --git a/ogWebconsole/src/app/components/groups/model/model.ts b/ogWebconsole/src/app/components/groups/model/model.ts index b37b389..289a796 100644 --- a/ogWebconsole/src/app/components/groups/model/model.ts +++ b/ogWebconsole/src/app/components/groups/model/model.ts @@ -9,6 +9,9 @@ export interface Aula { } export interface UnidadOrganizativa { + clients: any[]; + children: UnidadOrganizativa[]; + '@id'?: string; id: string; name: string; uuid: string; diff --git a/ogWebconsole/src/app/components/groups/services/data.service.ts b/ogWebconsole/src/app/components/groups/services/data.service.ts index ffc42f2..1946670 100644 --- a/ogWebconsole/src/app/components/groups/services/data.service.ts +++ b/ogWebconsole/src/app/components/groups/services/data.service.ts @@ -183,6 +183,16 @@ export class DataService { }) ); } - + + getOrganizationalUnitById(id: string): Observable { + const url = `${this.baseUrl}/organizational-units/${id}`; + return this.http.get(url).pipe( + catchError(error => { + console.error('Error fetching organizational unit', error); + return throwError(error); + }) + ); + } + } 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 index fcf40bd..d32557e 100644 --- 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 @@ -18,7 +18,8 @@ h1 { } .mat-dialog-content { - padding: 50px; + padding-left: 50px; + padding-right: 20px; } button { @@ -61,3 +62,60 @@ mat-option .unit-path { .form-field { width: 100%; } + +.inputs-container { + display: inline-flex; + gap: 20px; + padding-left: 50px; + padding-right: 20px; +} + +.upload-container { + display: flex; + justify-content: center; + align-items: center; + margin-top: 10px; +} + +button[mat-raised-button] { + font-size: 14px; + text-transform: none; + padding: 10px 20px; + border-radius: 4px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: background-color 0.3s ease, box-shadow 0.3s ease; +} + +button[mat-raised-button]:hover { + background-color: #303f9f; + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.3); +} + +/* Contenedor para la tabla con scroll */ +.scrollable-table { + max-height: 300px; /* Altura máxima de la tabla */ + overflow-y: auto; /* Habilitar scroll vertical */ + overflow-x: hidden; /* Ocultar scroll horizontal si no es necesario */ + border: 1px solid #ddd; + border-radius: 4px; + margin-top: 10px; +} + +/* Tabla */ +.mat-elevation-z8 { + width: 100%; +} + +.client-table th, .client-table td { + text-align: left; + padding: 10px; +} + +.client-table th { + position: sticky; + top: 0; + background-color: #3f51b5; + color: white; + z-index: 2; +} 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 130eaaa..e44a9d7 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 @@ -1,92 +1,131 @@

{{ 'addClientDialogTitle' | translate }}

-
- -
- - {{ 'organizationalUnitLabel' | translate }} - - -
{{ unit.name }}
-
{{ unit.path }}
-
-
-
- - {{ 'nameLabel' | translate }} - - +
+
+

Añadir un cliente

+ + + + {{ 'organizationalUnitLabel' | translate }} + + +
{{ unit.name }}
+
{{ unit.path }}
+
+
+
- - {{ 'ogLiveLabel' | translate }} - - - {{ oglive.name }} - - - + + {{ 'nameLabel' | translate }} + + - - {{ 'serialNumberLabel' | translate }} - - + + {{ 'ogLiveLabel' | translate }} + + + {{ oglive.name }} + + + - - {{ 'netifaceLabel' | translate }} - - - {{ type.name }} - - - + + {{ 'serialNumberLabel' | translate }} + + - - {{ 'netDriverLabel' | translate }} - - - {{ type.name }} - - - + + {{ 'netifaceLabel' | translate }} + + + {{ type.name }} + + + - - {{ 'macLabel' | translate }} - {{ 'macHint' | translate }} - - {{ 'macError' | translate }} - + + {{ 'netDriverLabel' | translate }} + + + {{ type.name }} + + + - - {{ 'ipLabel' | translate }} - {{ 'ipHint' | translate }} - - {{ 'ipError' | translate }} - + + {{ 'macLabel' | translate }} + {{ 'macHint' | translate }} + + {{ 'macError' | translate }} + - - {{ 'templateLabel' | translate }} - - - {{ template.name }} - - - + + {{ 'ipLabel' | translate }} + {{ 'ipHint' | translate }} + + {{ 'ipError' | translate }} + - - {{ 'hardwareProfileLabel' | translate }} - - - {{ unit.description }} - - - {{ 'hardwareProfileError' | translate }} - - + + {{ 'templateLabel' | translate }} + + + {{ template.name }} + + + + + + {{ 'hardwareProfileLabel' | translate }} + + + {{ unit.description }} + + + {{ 'hardwareProfileError' | translate }} + + +
+ + + +
+

Añadir multiples clientes

+
+ + +
+ + +

Clientes importados:

+
+ + + + + + + + + + + + + + + + + + +
Nombre {{ client.name }} IP {{ client.ip }}
+
+
+
- +
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 5cffebd..671a45e 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 @@ -5,6 +5,7 @@ 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'; +import * as Papa from 'papaparse'; @Component({ selector: 'app-create-client', @@ -18,16 +19,17 @@ export class CreateClientComponent implements OnInit { hardwareProfiles: any[] = []; ogLives: any[] = []; templates: any[] = []; - private errorForm: boolean = false; + uploadedClients: any[] = []; + loading: boolean = false; + displayedColumns: string[] = ['name', 'ip']; protected netifaceTypes = [ - { "name": 'Eth0', "value": "eth0" }, - { "name": 'Eth1', "value": "eth1" }, - { "name": 'Eth2', "value": "eth2" }, + { name: 'Eth0', value: 'eth0' }, + { name: 'Eth1', value: 'eth1' }, + { name: 'Eth2', value: 'eth2' } ]; protected netDriverTypes = [ - { "name": 'Generic', "value": "generic" }, + { name: 'Generic', value: 'generic' } ]; - loading: boolean = false; constructor( private fb: FormBuilder, @@ -37,16 +39,22 @@ export class CreateClientComponent implements OnInit { private toastService: ToastrService, private dataService: DataService, @Inject(MAT_DIALOG_DATA) public data: any - ) { } + ) {} ngOnInit(): void { - console.log(this.data); + this.initForm(); this.loadParentUnits(); this.loadHardwareProfiles(); this.loadOgLives(); - this.loadPxeTemplates() + this.loadPxeTemplates(); + } + + initForm(): void { this.clientForm = this.fb.group({ - organizationalUnit: [this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null, Validators.required], + organizationalUnit: [ + this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null, + Validators.required + ], name: ['', Validators.required], serialNumber: [''], netiface: null, @@ -54,25 +62,14 @@ export class CreateClientComponent implements OnInit { mac: ['', Validators.required], ip: ['', Validators.required], template: [null], - hardwareProfile: [this.data.organizationalUnit && this.data.organizationalUnit.networkSettings && this.data.organizationalUnit.networkSettings.hardwareProfile ? this.data.organizationalUnit.networkSettings.hardwareProfile['@id'] : null], + hardwareProfile: [ + this.data.organizationalUnit?.networkSettings?.hardwareProfile?.['@id'] || null + ], ogLive: [null] }); } - loadHardwareProfiles(): void { - this.dataService.getHardwareProfiles().subscribe( - (data: any[]) => { - this.hardwareProfiles = data; - this.loading = false; - }, - (error: any) => { - console.error('Error fetching hardware profiles', error); - this.loading = false; - } - ); - } - - loadParentUnits() { + loadParentUnits(): void { this.loading = true; const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`; @@ -88,9 +85,22 @@ export class CreateClientComponent implements OnInit { ); } - loadOgLives() { + 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']; @@ -109,25 +119,50 @@ export class CreateClientComponent implements OnInit { this.templates = response['hydra:member']; }, error => { - console.error('Error fetching ogLives:', error); + console.error('Error fetching PXE templates:', error); } ); } - onSubmit() { + onFileUpload(event: any): void { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + + // Leer archivo como texto + reader.onload = (e: any) => { + const csvData = e.target.result; + + // Usar PapaParse para convertir CSV a JSON + Papa.parse(csvData, { + header: true, // Utilizar la primera fila como encabezados + skipEmptyLines: true, // Ignorar líneas vacías + complete: (result) => { + this.uploadedClients = result.data; + this.toastService.success('Archivo CSV cargado correctamente', 'Éxito'); + }, + error: (error) => { + console.error('Error al procesar el archivo CSV:', error); + this.toastService.error('Error al procesar el archivo CSV', 'Error'); + } + }); + }; + + reader.readAsText(file); + } + } + + onSubmit(): void { if (this.clientForm.valid) { - this.errorForm = false; const formData = this.clientForm.value; - formData.ogLive = formData.ogLive; this.http.post(`${this.baseUrl}/clients`, formData).subscribe( response => { + this.toastService.success('Cliente creado exitosamente', 'Éxito'); this.dialogRef.close(response); - this.openSnackBar(false, 'Cliente creado exitosamente'); }, error => { console.error('Error during POST:', error); - this.errorForm = true; - this.openSnackBar(true, 'Error al crear el cliente: ' + error.error['hydra:description']); + this.toastService.error('Error al crear el cliente', 'Error'); } ); } @@ -136,12 +171,4 @@ export class CreateClientComponent implements OnInit { 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'); - } - } } From 398e0ffa57166b01a62c7e57e72513319643e9e0 Mon Sep 17 00:00:00 2001 From: apuente Date: Sun, 1 Dec 2024 00:55:38 +0100 Subject: [PATCH 04/17] Refactor groups component --- .../components/groups/groups.component.css | 264 +++++++++++++++--- .../components/groups/groups.component.html | 225 ++++++++------- .../app/components/groups/groups.component.ts | 215 +++++++++++--- .../groups/services/data.service.ts | 3 + .../create-client/create-client.component.css | 219 +++++++++------ .../create-client.component.html | 209 +++++++------- .../create-client/create-client.component.ts | 130 ++++++--- ogWebconsole/src/locale/en.json | 3 +- ogWebconsole/src/locale/es.json | 3 +- 9 files changed, 861 insertions(+), 410 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index f7b4608..f40ad86 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -130,13 +130,18 @@ mat-card-actions { text-align: center; } -.details-placeholder { - background-color: #f5f5f5; - border: 1px solid #ddd; - border-radius: 8px; +.details-wrapper { + width: 95%; padding: 20px; + /* Asegúrate de que no haya propiedades que centren el contenido */ + display: block; +} + +.details-placeholder { width: 100%; - max-width: 600px; + /* Elimina max-width si existe */ + /* max-width: none; */ + /* Otros estilos existentes */ } button[mat-raised-button] { @@ -184,24 +189,15 @@ button[mat-raised-button] { margin: 20px auto; } -.details-placeholder { - background-color: #ffffff; - border: 1px solid #e0e0e0; - border-radius: 12px; - padding: 20px; - width: 100%; - max-width: 800px; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); -} - mat-tree { - width: 100%; background-color: #f9f9f9; - border: 1px solid #ddd; - border-radius: 8px; + border-right: 1px solid #ddd; padding: 10px; } +button{ + margin: 5px; +} mat-tree mat-tree-node { display: flex; align-items: center; @@ -262,15 +258,15 @@ mat-tree mat-tree-node mat-icon.node-icon.organizational-unit { } mat-tree mat-tree-node mat-icon.node-icon.classroom { - color: #388e3c; /* Verde para aulas */ + color: #757575; /* Verde para aulas */ } mat-tree mat-tree-node mat-icon.node-icon.client { - color: #f57c00; /* Naranja para clientes */ + color: #757575; /* Naranja para clientes */ } mat-tree mat-tree-node mat-icon.node-icon.group { - color: #d32f2f; /* Rojo para grupos */ + color: #757575; /* Rojo para grupos */ } @@ -288,27 +284,6 @@ mat-tree mat-tree-node button.mat-icon-button.disabled-toggle:hover { background-color: transparent; /* Desactiva hover */ } -mat-tree mat-tree-node mat-icon { - margin-right: 10px; - color: #757575; - transition: color 0.2s; -} - -mat-tree mat-tree-node mat-icon.node-icon.organizational-unit { - color: #1976d2; /* Azul para unidades organizativas */ -} - -mat-tree mat-tree-node mat-icon.node-icon.classroom { - color: #388e3c; /* Verde para aulas */ -} - -mat-tree mat-tree-node mat-icon.node-icon.client { - color: #f57c00; /* Naranja para clientes */ -} - -mat-tree mat-tree-node mat-icon.node-icon.group { - color: #d32f2f; /* Rojo para grupos */ -} mat-tree mat-tree-node:hover { background-color: #e3f2fd; @@ -322,3 +297,208 @@ mat-tree mat-tree-node.disabled { mat-tree mat-tree-node.disabled:hover { background-color: transparent; /* Desactiva hover */ } + +.mat-menu-item .mat-menu-item-submenu-icon { + display: none; +} + +.filters-container { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 16px; +} + +.filters-container mat-form-field { + flex: 1 1 100%; + max-width: 300px; +} +.filter-container { + margin-bottom: 16px; +} + +.pc-og-live { + /* Estilo para clientes con status 'og-live' */ + color: #4caf50; /* Verde */ +} + +.pc-busy { + /* Estilo para clientes ocupados */ + color: #ff9800; /* Naranja */ +} + +.pc-windows { + /* Estilo para clientes con Windows */ + color: #0078d7; /* Azul Windows */ +} + +.pc-linux { + /* Estilo para clientes con Linux */ + color: #f0ad4e; /* Amarillo */ +} + +.pc-macos { + /* Estilo para clientes con macOS */ + color: #999999; /* Gris */ +} + +.pc-off { + /* Estilo para clientes apagados */ + color: #f44336; /* Rojo */ +} + +.clients-card-container { + display: flex; + flex-wrap: wrap; + gap: 16px; + padding: 16px; + justify-content: flex-start; +} + +.classroom-item { + flex: 1 1 calc(25% - 16px); /* Ajusta este valor para controlar cuántas tarjetas caben en una fila */ + max-width: calc(25% - 16px); + box-sizing: border-box; +} + +.classroom-pc { + border: 1px solid #ccc; + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.classroom-pc .pc-image { + width: 64px; + height: 64px; + margin-bottom: 8px; +} + +.pc-details { + margin-bottom: 8px; +} + +.pc-details .client-name, +.pc-details .client-ip, +.pc-details .client-mac { + display: block; + font-size: 14px; + color: #666; +} + +.pc-actions button { + margin: 0 4px; +} + +.main-container { + display: flex; + flex-direction: row; +} + +.tree-container { + width: 25%; /* El árbol ocupa el 25% del ancho */ + padding: 16px; + overflow-x: hidden; + overflow-y: auto; /* Scroll si hay muchos nodos */ +} + +.clients-container { + width: 75%; /* Los clientes ocupan el 75% del ancho */ + padding: 16px; + box-sizing: border-box; + overflow-y: auto; /* Scroll si hay muchos clientes */ +} + + +.clients-container h3 { + margin-bottom: 15px; + font-size: 1.5em; + color: #333; +} + +.client-item { + display: flex; + justify-content: center; +} + +.client-card { + background-color: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 300px; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 15px; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.client-card:hover { + transform: translateY(-5px); + box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); +} + +.client-image { + width: 60px; + height: 60px; + margin-bottom: 15px; +} + + +.client-details { + margin-top: 10px; +} + +.client-name { + display: block; + font-size: 1.2em; + font-weight: 600; + color: #333; + margin-bottom: 5px; +} + + +.client-ip { + display: block; + font-size: 0.9em; + color: #666; +} + +button[mat-raised-button] { + margin-top: 15px; +} + + + + +.clients-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 16px; +} + +.clients-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.client-item-list { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.client-details-list { + display: flex; + flex-direction: column; +} diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index d2d2d6f..a4c529c 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -1,5 +1,7 @@ + +
- + +
+ [ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}" + (click)="onSelectUnidad(unidad)" class="unidad-card small-card"> apartment {{ unidad.name }} @@ -48,155 +51,167 @@ edit {{ 'editUnitMenu' | translate }} - + +
- + + -
-
-

{{ 'Details of' | translate }} {{ selectedUnidad?.name }}

- - - - - {{ - node.type === 'organizational-unit' - ? 'apartment' - : node.type === 'classroom' - ? 'school' - : node.type === 'client' - ? 'computer' - : 'group' + node.type === 'organizational-unit' + ? 'apartment' + : node.type === 'classroom' + ? 'school' + : node.type === 'client' + ? 'computer' + : 'group' }} - {{ node.name }} ({{ node.type }}) + {{ node.name }} - - - - + {{ - node.type === 'organizational-unit' - ? 'apartment' - : node.type === 'classroom' - ? 'school' - : node.type === 'client' - ? 'computer' - : 'group' + node.type === 'organizational-unit' + ? 'apartment' + : node.type === 'classroom' + ? 'school' + : node.type === 'client' + ? 'computer' + : 'group' }} - {{ node.name }} ({{ node.type }}) + {{ node.name }} - - IP: {{ node.ip }} - - - - - - - - {{ - node.type === 'organizational-unit' - ? 'apartment' - : node.type === 'classroom' - ? 'school' - : node.type === 'client' - ? 'computer' - : 'group' - }} - - {{ node.name }} ({{ node.type }}) - - - IP: {{ node.ip }} - - - - - - - - computer - device_hub - {{ node.name }} ({{ node.type }}) - - - IP: {{ node.ip }} + - IP: {{ node.ip }} - - - - - - - - - +
+ + + + + + + + + + + + + + + +
+

Clientes del {{ selectedNode?.name }}

+
+
+
+ Client Icon +
+ {{ client.name }} + {{ client.ip }} + + + + + + + +
+
+
+
- + +
diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index ecd89a0..31f9535 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -17,6 +17,11 @@ import { TreeViewComponent } from './shared/tree-view/tree-view.component'; import { LegendComponent } from './shared/legend/legend.component'; import { ClientTabViewComponent } from './components/client-tab-view/client-tab-view.component'; import { OrganizationalUnitTabViewComponent } from './components/organizational-unit-tab-view/organizational-unit-tab-view.component'; +import { MatMenuTrigger } from '@angular/material/menu'; +import { HttpClient } from '@angular/common/http'; +import { Router } from '@angular/router'; +import { ToastrService } from 'ngx-toastr'; +import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component'; interface TreeNode { name: string; @@ -24,6 +29,7 @@ interface TreeNode { children?: TreeNode[]; ip?: string; '@id'?: string; + hasClients?: boolean; } interface FlatNode { @@ -32,6 +38,7 @@ interface FlatNode { level: number; expandable: boolean; ip?: string; + hasClients?: boolean; } @Component({ @@ -47,20 +54,26 @@ export class GroupsComponent implements OnInit { loading: boolean = false; loadingChildren: boolean = false; searchTerm: string = ''; - treeControl: FlatTreeControl; treeFlattener: MatTreeFlattener; treeDataSource: MatTreeFlatDataSource; selectedNode: TreeNode | null = null; + commands: any[] = []; + commandsLoading: boolean = false; + selectedClients: any[] = []; + cols: number = 4; @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; constructor( + private http: HttpClient, + private router: Router, private dataService: DataService, public dialog: MatDialog, private _bottomSheet: MatBottomSheet, - private joyrideService: JoyrideService + private joyrideService: JoyrideService, + private toastr: ToastrService ) { this.treeFlattener = new MatTreeFlattener( (node: TreeNode, level: number) => ({ @@ -68,6 +81,7 @@ export class GroupsComponent implements OnInit { type: node.type, level, expandable: !!node.children?.length, + hasClients: node.hasClients, ip: node.ip, ['@id']: node['@id'] }), @@ -87,6 +101,21 @@ export class GroupsComponent implements OnInit { ngOnInit(): void { this.search(); this.getFilters(); + this.updateGridCols(); + window.addEventListener('resize', () => this.updateGridCols()); + } + + updateGridCols(): void { + const width = window.innerWidth; + if (width <= 600) { + this.cols = 1; + } else if (width <= 960) { + this.cols = 2; + } else if (width <= 1280) { + this.cols = 3; + } else { + this.cols = 4; + } } clearSelection(): void { @@ -131,6 +160,7 @@ export class GroupsComponent implements OnInit { onSelectUnidad(unidad: UnidadOrganizativa): void { this.selectedUnidad = unidad; this.selectedDetail = unidad; + console.log('Selected unidad:', unidad); this.loadChildrenAndClients(unidad.id).then(fullData => { const treeData = this.convertToTreeData(fullData); @@ -141,35 +171,24 @@ export class GroupsComponent implements OnInit { async loadChildrenAndClients(id: string): Promise { try { const childrenData = await this.dataService.getChildren(id).toPromise(); - const clientsData = await this.dataService.getClients(id).toPromise(); - + const processHierarchy = (nodes: UnidadOrganizativa[]): TreeNode[] => { return nodes.map(node => ({ name: node.name, type: node.type, '@id': node['@id'], - children: [ - ...(node.children ? processHierarchy(node.children) : []), - ...(node.clients - ? node.clients.map((client: any) => ({ - name: client.name, - type: 'client', - '@id': client['@id'], - ip: client.ip - })) - : []) - ], - ip: node.type === 'client' ? (node as any).ip : undefined + children: node.children ? processHierarchy(node.children) : [], + clients: node.clients || [] })); }; - + + return { ...this.selectedUnidad, - children: childrenData ? processHierarchy(childrenData) : [], - clients: clientsData || [] + children: childrenData ? processHierarchy(childrenData) : [] }; } catch (error) { - console.error('Error loading children and clients:', error); + console.error('Error loading children:', error); return this.selectedUnidad; } } @@ -179,23 +198,43 @@ export class GroupsComponent implements OnInit { name: node.name, type: node.type, '@id': node['@id'], - children: [ - ...(node.children?.map(processNode) || []), - ...(node.clients - ? node.clients.map((client: any) => ({ - name: client.name, - type: 'client', - '@id': client['@id'], - ip: client.ip - })) - : []) - ], - ip: node.type === 'client' ? (node as any).ip : undefined + children: node.children?.map(processNode) || [], + hasClients: node.clients && node.clients.length > 0, }); return [processNode(data)]; } + onNodeClick(node: TreeNode): void { + console.log('Node clicked:', node); + this.selectedNode = node; + + if (node.hasClients) { + const url = `${this.baseUrl}${node['@id']}`; + this.http.get(url).subscribe( + (data: any) => { + console.log('Clients fetched:', data); + this.selectedClients = data.clients || []; + + }, + (error) => { + console.error('Error fetching clients:', error); + } + ); + } else { + this.selectedClients = []; + } + } + + getNodeIcon(node: any): string { + switch (node.type) { + case 'organizational-unit': return 'apartment'; + case 'classroom': return 'school'; + case 'client': return 'computer'; + default: return 'group'; + } + } + addOU(event: MouseEvent, parent: any = null): void { event.stopPropagation(); const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px' }); @@ -245,13 +284,86 @@ export class GroupsComponent implements OnInit { } onDelete(node: TreeNode | null): void { - if (!node) return; - // Additional logic for deleting + console.log('Deleting node:', node); } - onCustomAction(node: TreeNode | null): void { - if (!node) return; - // Logic for custom actions + onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void { + console.log('Deleting node or client:', node); + + const uuid = node && node['@id'] ? node['@id'].split('/').pop() || '' : ''; + const name = node?.name || 'Elemento desconocido'; + const type = node?.type || ''; + + event.stopPropagation(); + + const dialogRef = this.dialog.open(DeleteModalComponent, { + width: '400px', + data: { name } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result === true) { + this.dataService.deleteElement(uuid, type).subscribe( + () => { + console.log('Entity deleted successfully:', uuid); + + this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { + const treeData = this.convertToTreeData(updatedData); + this.treeDataSource.data = treeData[0]?.children || []; + }); + + if (type === 'client' && clientNode) { + console.log('Refreshing clients for node:', clientNode); + this.refreshClients(clientNode); + } + + this.dataService.getOrganizationalUnits().subscribe( + data => { + this.organizationalUnits = data; + }, + error => console.error('Error fetching unidades organizativas:', error) + ); + + this.toastr.success('Entidad eliminada exitosamente'); + }, + error => { + console.error('Error deleting entity:', error); + this.toastr.error('Error al eliminar la entidad', error.message); + } + ); + } + }); + } + + private refreshClients(node: TreeNode): void { + if (!node || !node['@id']) { + console.warn('Node or @id is missing, clearing clients.'); + this.selectedClients = []; + return; + } + + const url = `${this.baseUrl}${node['@id']}`; + console.log('Fetching clients for node with URL:', url); + + this.http.get(url).subscribe( + (data: any) => { + console.log('Response data:', data); + if (data && Array.isArray(data.clients)) { + this.selectedClients = data.clients; + console.log('Clients updated successfully:', this.selectedClients); + } else { + console.warn('No "clients" field found in response, clearing clients.'); + this.selectedClients = []; + } + }, + error => { + console.error('Error refreshing clients:', error); + const errorMessage = error.status === 404 + ? 'No se encontraron clientes para este nodo.' + : 'Error al comunicarse con el servidor.'; + this.toastr.error(errorMessage); + } + ); } onEditClick(event: MouseEvent, type: any, uuid: string): void { @@ -263,11 +375,37 @@ export class GroupsComponent implements OnInit { } } - onShowClick(event: MouseEvent, data: any): void { + fetchCommands(): void { + this.commandsLoading = true; + this.http.get('https://127.0.0.1:8443/commands?page=1&itemsPerPage=30').subscribe( + (response: any) => { + this.commands = response['hydra:member']; + this.commandsLoading = false; + }, + (error) => { + console.error('Error fetching commands:', error); + this.commandsLoading = false; + } + ); + } + + executeCommand(command: any, selectedNode: any): void { + console.log('Executing command:', command.name); + this.toastr.success('Ejecutando comando: ' + command.name+ " en "+ selectedNode.name ) ; + } + + onClientActions(client: any): void { + console.log('Client actions:', client); + } + + onShowDetailsClick(event: MouseEvent, data: any): void { event.stopPropagation(); if (data.type != 'client') { this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px' }); } + if (data.type == 'client') { + this.router.navigate(['clients', data['@id'].split('/').pop()], { state: { clientData: data } }); + } } onTreeClick(event: MouseEvent, data: any): void { @@ -291,5 +429,4 @@ export class GroupsComponent implements OnInit { hasChild = (_: number, node: FlatNode): boolean => node.expandable; isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable; - } diff --git a/ogWebconsole/src/app/components/groups/services/data.service.ts b/ogWebconsole/src/app/components/groups/services/data.service.ts index 1946670..e27c39f 100644 --- a/ogWebconsole/src/app/components/groups/services/data.service.ts +++ b/ogWebconsole/src/app/components/groups/services/data.service.ts @@ -90,6 +90,8 @@ export class DataService { const url = type === 'client' ? `${this.baseUrl}/clients/${uuid}` : `${this.baseUrl}/organizational-units/${uuid}`; + + console.log('DELETE URL:', url); // Depuración return this.http.delete(url).pipe( catchError(error => { console.error('Error deleting element', error); @@ -97,6 +99,7 @@ export class DataService { }) ); } + changeParent(uuid: string): Observable { const url = `${this.baseUrl}/organizational-units/${uuid}/change-parent`; 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 index d32557e..b978520 100644 --- 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 @@ -1,121 +1,168 @@ -h1 { - text-align: center; - font-family: 'Roboto', sans-serif; - font-weight: 400; - color: #3f51b5; - margin-bottom: 20px; -} - -.network-form { +/* Contenedor principal */ +.create-client-container { display: flex; flex-direction: column; - gap: 15px; + padding: 16px; + font-family: Arial, sans-serif; + font-size: 14px; } -.form-field { - width: 100%; - margin-top: 10px; +/* Títulos */ +h1, h3, h4 { + margin: 0 0 16px; + color: #333; + font-weight: 600; } -.mat-dialog-content { - padding-left: 50px; - padding-right: 20px; +h1 { + font-size: 20px; } -button { - text-transform: none; +h3 { + font-size: 18px; +} + +h4 { font-size: 16px; - font-weight: 500; + margin-top: 16px; } -.mat-slide-toggle { - margin-top: 20px; +/* Espaciado entre contenedores */ +.inputs-container { + display: flex; + gap: 24px; + margin-top: 16px; } -mat-option .unit-name { - display: block; +/* Contenedor de cada sección */ +.mat-dialog-content { + flex: 1; + background-color: #f9f9f9; + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } -mat-option .unit-path { - display: block; - font-size: 0.8em; - color: gray; +.create-multiple-client-container { + flex: 1; + background-color: #f9f9f9; + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } -.loading-spinner { - display: block; - margin: 0 auto; - align-items: center; - justify-content: center; -} - -.create-client-container { - position: relative; -} - -.grid-form { +/* Campos del formulario */ +.client-form { display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 20px; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; } .form-field { width: 100%; } -.inputs-container { - display: inline-flex; - gap: 20px; - padding-left: 50px; - padding-right: 20px; +.mat-form-field { + width: 100%; +} + +/* Tabla */ +.scrollable-table { + max-height: 200px; + overflow-y: auto; + margin-top: 16px; + border: 1px solid #ddd; + border-radius: 8px; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + text-align: left; + padding: 8px; + border-bottom: 1px solid #ddd; +} + +th { + background-color: #f1f1f1; + font-weight: bold; +} + +tr:hover { + background-color: #f9f9f9; +} + +/* Botones */ +button { + margin-right: 8px; +} + +button:last-child { + margin-right: 0; +} + +.mat-dialog-actions { + margin-top: 16px; + display: flex; + justify-content: space-between; +} + +button.mat-raised-button { + text-transform: none; + font-weight: 600; +} + +/* Carga */ +.loading-spinner { + margin: 16px auto; + display: block; +} + +/* Alternar formularios */ +.toggle-button { + background: none; + border: none; + color: #007BFF; + cursor: pointer; + font-size: 14px; + text-decoration: underline; +} + +.toggle-button:hover { + text-decoration: none; +} + +/* Mejoras generales */ +.mat-divider { + margin: 0 16px; } .upload-container { display: flex; - justify-content: center; - align-items: center; - margin-top: 10px; + flex-direction: column; + align-items: flex-start; + gap: 8px; } -button[mat-raised-button] { - font-size: 14px; - text-transform: none; - padding: 10px 20px; - border-radius: 4px; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); - cursor: pointer; - transition: background-color 0.3s ease, box-shadow 0.3s ease; +input[type="file"] { + display: none; } -button[mat-raised-button]:hover { - background-color: #303f9f; - box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.3); -} +/* Responsivo */ +@media (max-width: 768px) { + .inputs-container { + flex-direction: column; + gap: 16px; + } -/* Contenedor para la tabla con scroll */ -.scrollable-table { - max-height: 300px; /* Altura máxima de la tabla */ - overflow-y: auto; /* Habilitar scroll vertical */ - overflow-x: hidden; /* Ocultar scroll horizontal si no es necesario */ - border: 1px solid #ddd; - border-radius: 4px; - margin-top: 10px; -} + .mat-dialog-content, .create-multiple-client-container { + padding: 12px; + } -/* Tabla */ -.mat-elevation-z8 { - width: 100%; -} - -.client-table th, .client-table td { - text-align: left; - padding: 10px; -} - -.client-table th { - position: sticky; - top: 0; - background-color: #3f51b5; - color: white; - z-index: 2; + .scrollable-table { + max-height: 150px; + } } 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 e44a9d7..707c348 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 @@ -1,10 +1,7 @@
-

{{ 'addClientDialogTitle' | translate }}

-
-

Añadir un cliente

- +
{{ 'organizationalUnitLabel' | translate }} @@ -15,117 +12,123 @@ - - - {{ 'nameLabel' | translate }} - - - - - {{ 'ogLiveLabel' | translate }} - - - {{ oglive.name }} - - - - - - {{ 'serialNumberLabel' | translate }} - - - - - {{ 'netifaceLabel' | translate }} - - - {{ type.name }} - - - - - - {{ 'netDriverLabel' | translate }} - - - {{ type.name }} - - - - - - {{ 'macLabel' | translate }} - {{ 'macHint' | translate }} - - {{ 'macError' | translate }} - - - - {{ 'ipLabel' | translate }} - {{ 'ipHint' | translate }} - - {{ 'ipError' | translate }} - - - - {{ 'templateLabel' | translate }} - - - {{ template.name }} - - - - - - {{ 'hardwareProfileLabel' | translate }} - - - {{ unit.description }} - - - {{ 'hardwareProfileError' | translate }} -
-
- +
+

Añadir múltiples clientes

+
+ + +

o añadelos manualmente:

+ + +
-
-

Añadir multiples clientes

-
- - +

Clientes importados:

+
+ + + + + + + + + + + + + +
Nombre {{ client.name }} IP {{ client.ip }}
+
- -

Clientes importados:

-
- - + + + +

Añadir un cliente

+ +
- -
- - + + {{ 'nameLabel' | translate }} + + - - - - - - - - + + {{ 'ogLiveLabel' | translate }} + + + {{ oglive.name }} + + + - -
Nombre {{ client.name }} IP {{ client.ip }}
-
+ + {{ 'serialNumberLabel' | translate }} + + + + + {{ 'netifaceLabel' | translate }} + + + {{ type.name }} + + + + + + {{ 'netDriverLabel' | translate }} + + + {{ type.name }} + + + + + + {{ 'macLabel' | translate }} + {{ 'macHint' | translate }} + + {{ 'macError' | translate }} + + + + {{ 'ipLabel' | translate }} + {{ 'ipHint' | translate }} + + {{ 'ipError' | translate }} + + + + {{ 'templateLabel' | translate }} + + + {{ template.name }} + + + + + + {{ 'hardwareProfileLabel' | translate }} + + + {{ unit.description }} + + + {{ 'hardwareProfileError' | 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 671a45e..5c781ea 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 @@ -22,6 +22,7 @@ export class CreateClientComponent implements OnInit { uploadedClients: any[] = []; loading: boolean = false; displayedColumns: string[] = ['name', 'ip']; + isSingleClientForm: boolean = false; protected netifaceTypes = [ { name: 'Eth0', value: 'eth0' }, { name: 'Eth1', value: 'eth1' }, @@ -128,46 +129,109 @@ export class CreateClientComponent implements OnInit { const file = event.target.files[0]; if (file) { const reader = new FileReader(); - - // Leer archivo como texto + reader.onload = (e: any) => { - const csvData = e.target.result; - - // Usar PapaParse para convertir CSV a JSON - Papa.parse(csvData, { - header: true, // Utilizar la primera fila como encabezados - skipEmptyLines: true, // Ignorar líneas vacías - complete: (result) => { - this.uploadedClients = result.data; - this.toastService.success('Archivo CSV cargado correctamente', 'Éxito'); - }, - error: (error) => { - console.error('Error al procesar el archivo CSV:', error); - this.toastService.error('Error al procesar el archivo CSV', 'Error'); - } - }); + 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], + mac: match[2], + ip: match[3] + }); + } + + if (clients.length > 0) { + this.uploadedClients = clients; + this.toastService.success('Archivo cargado correctamente, los datos están listos para enviarse.', 'Éxito'); + } else { + this.toastService.error('No se encontraron datos válidos', 'Error'); + } }; - + reader.readAsText(file); } } - - 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(response); - }, - error => { - console.error('Error during POST:', error); - this.toastService.error('Error al crear el cliente', 'Error'); - } - ); + + 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], + mac: match[2], + ip: match[3] + }); + } + + if (clients.length > 0) { + this.uploadedClients = clients; + this.toastService.success('Datos cargados correctamente, los datos están listos para enviarse.', 'Éxito'); + } else { + this.toastService.error('No se encontraron datos válidos', 'Error'); } } - + + onSubmit(): void { + if (this.isSingleClientForm) { + if (this.clientForm.valid) { + const formData = this.clientForm.value; + console.log('Form data:', formData); + this.http.post(`${this.baseUrl}/clients`, formData).subscribe( + response => { + this.toastService.success('Cliente creado exitosamente', 'Éxito'); + this.dialogRef.close(response); + }, + error => { + console.error('Error durante POST:', error); + this.toastService.error('Error al crear el cliente', 'Error'); + } + ); + } + } else { + if (this.uploadedClients.length > 0) { + this.uploadedClients.forEach(client => { + const formData = { + organizationalUnit: this.clientForm.value.organizationalUnit || null, + name: client.name || null, + mac: client.mac || null, + ip: client.ip || null, + template: this.clientForm.value.template || null, + hardwareProfile: this.clientForm.value.hardwareProfile || null, + ogLive: this.clientForm.value.ogLive || null, + serialNumber: null, + netiface: null, + netDriver: null + }; + + this.http.post(`${this.baseUrl}/clients`, formData).subscribe( + response => { + this.toastService.success(`Cliente ${client.name} creado exitosamente`, 'Éxito'); + }, + error => { + console.error(`Error al crear el cliente ${client.name}:`, error); + this.toastService.error(`Error al crear el cliente ${client.name}`, 'Error'); + } + ); + }); + + this.uploadedClients = []; + this.dialogRef.close(); + } else { + this.toastService.error('No hay clientes cargados para añadir', 'Error'); + } + } + } + + toggleClientForm(): void { + this.isSingleClientForm = !this.isSingleClientForm; + } + onNoClick(): void { this.dialogRef.close(); } diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index 4bb7273..59fe4cd 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -419,5 +419,6 @@ "menus": "Menus", "TOOLTIP_MENUS": "Menu management (option disabled)", "search": "Search", - "TOOLTIP_SEARCH": "Search function (option disabled)" + "TOOLTIP_SEARCH": "Search function (option disabled)", + "detailsOf": "Details of" } diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index bec0ac1..fe202b0 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -420,5 +420,6 @@ "menus": "Menús", "TOOLTIP_MENUS": "Gestión de menús (opción deshabilitada)", "search": "Buscar", - "TOOLTIP_SEARCH": "Función de búsqueda (opción deshabilitada)" + "TOOLTIP_SEARCH": "Función de búsqueda (opción deshabilitada)", + "detailsOf": "Detalles de" } From a6797f8f77090326ce8a589e3c6f1cc34cd6f26d Mon Sep 17 00:00:00 2001 From: apuente Date: Sun, 1 Dec 2024 20:00:16 +0100 Subject: [PATCH 05/17] Refactor groups component --- .../components/groups/groups.component.css | 91 +++++++++++ .../components/groups/groups.component.html | 153 ++++++++++++++++-- .../app/components/groups/groups.component.ts | 100 ++++++------ .../create-client/create-client.component.css | 15 +- .../create-client.component.html | 6 +- .../create-client/create-client.component.ts | 5 + 6 files changed, 294 insertions(+), 76 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index f40ad86..60fad89 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -502,3 +502,94 @@ button[mat-raised-button] { display: flex; flex-direction: column; } + + +.view-toggle-container { + display: flex; + gap: 1rem; + margin-bottom: 1rem; +} + +.clients-grid { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.clients-list .list-item-content { + display: flex; + justify-content: space-between; + width: 100%; + align-items: center; +} + +.client-card, .list-item-content { + border: 1px solid #ccc; + padding: 1rem; + border-radius: 5px; +} + +.client-card { + display: flex; + flex-direction: column; + align-items: center; +} + +.client-image { + width: 50px; + height: 50px; + margin-bottom: 0.5rem; +} +.view-toggle-container { + display: flex; + gap: 1rem; + margin-bottom: 1rem; +} + +.clients-grid { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.clients-list .list-item-content { + display: flex; + justify-content: space-between; + width: 100%; + align-items: center; +} + +.client-card, .list-item-content { + border: 1px solid #ccc; + padding: 1rem; + border-radius: 5px; +} + +.client-card { + display: flex; + flex-direction: column; + align-items: center; +} + +.client-image { + width: 50px; + height: 50px; + margin-bottom: 0.5rem; +} + +.header-actions-container { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.back-button { + flex-shrink: 0; /* Asegura que el botón Back no se reduzca */ +} + +.view-toggle-container { + display: flex; + gap: 1rem; + align-items: center; +} diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index a4c529c..b672684 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -17,11 +17,12 @@ -
+
@@ -77,10 +78,24 @@ - +
+ + + + +
+ + +
+
+
@@ -114,7 +129,7 @@ + @@ -162,6 +177,10 @@ visibility {{ 'viewUnitMenu' | translate }} + - + +

Clientes del {{ selectedNode?.name }}

-
+ + +
Client Icon @@ -189,28 +212,124 @@ - + -
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nombre {{ client.name }} IP {{ client.ip }} MAC {{ client.mac }} OG Live {{ client.oglive }} Estado + + {{ client.status || 'off' }} + + Subred {{ client.subnet }} Plantilla PXE {{ client.pxeTemplate }} Acciones + + + + + + +
+ + + +
+
- - diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 31f9535..b9bf844 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -1,13 +1,17 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import { DataService } from './services/data.service'; -import { UnidadOrganizativa } from './model/model'; +import { HttpClient } from '@angular/common/http'; +import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { MatBottomSheet } from '@angular/material/bottom-sheet'; import { MatTabChangeEvent } from '@angular/material/tabs'; +import { MatMenuTrigger } from '@angular/material/menu'; +import { ToastrService } from 'ngx-toastr'; import { JoyrideService } from 'ngx-joyride'; import { FlatTreeControl } from '@angular/cdk/tree'; import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; +import { DataService } from './services/data.service'; +import { UnidadOrganizativa } from './model/model'; import { CreateOrganizationalUnitComponent } from './shared/organizational-units/create-organizational-unit/create-organizational-unit.component'; import { CreateClientComponent } from './shared/clients/create-client/create-client.component'; import { EditOrganizationalUnitComponent } from './shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component'; @@ -17,11 +21,8 @@ import { TreeViewComponent } from './shared/tree-view/tree-view.component'; import { LegendComponent } from './shared/legend/legend.component'; import { ClientTabViewComponent } from './components/client-tab-view/client-tab-view.component'; import { OrganizationalUnitTabViewComponent } from './components/organizational-unit-tab-view/organizational-unit-tab-view.component'; -import { MatMenuTrigger } from '@angular/material/menu'; -import { HttpClient } from '@angular/common/http'; -import { Router } from '@angular/router'; -import { ToastrService } from 'ngx-toastr'; import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component'; +import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal'; interface TreeNode { name: string; @@ -30,6 +31,7 @@ interface TreeNode { ip?: string; '@id'?: string; hasClients?: boolean; + status?: string ; } interface FlatNode { @@ -62,6 +64,8 @@ export class GroupsComponent implements OnInit { commandsLoading: boolean = false; selectedClients: any[] = []; cols: number = 4; + currentView: 'card' | 'list' = 'list'; + @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; @@ -104,35 +108,26 @@ export class GroupsComponent implements OnInit { this.updateGridCols(); window.addEventListener('resize', () => this.updateGridCols()); } + toggleView(view: 'card' | 'list') { + this.currentView = view; + } updateGridCols(): void { const width = window.innerWidth; - if (width <= 600) { - this.cols = 1; - } else if (width <= 960) { - this.cols = 2; - } else if (width <= 1280) { - this.cols = 3; - } else { - this.cols = 4; - } + this.cols = width <= 600 ? 1 : width <= 960 ? 2 : width <= 1280 ? 3 : 4; } clearSelection(): void { this.selectedUnidad = null; this.selectedDetail = null; + this.selectedClients = []; } onTabChange(event: MatTabChangeEvent): void { - switch (event.index) { - case 2: - this.clientTabComponent.search(); - break; - case 3: - this.organizationalUnitTabComponent.search(); - break; - default: - break; + if (event.index === 2) { + this.clientTabComponent.search(); + } else if (event.index === 3) { + this.organizationalUnitTabComponent.search(); } } @@ -160,8 +155,7 @@ export class GroupsComponent implements OnInit { onSelectUnidad(unidad: UnidadOrganizativa): void { this.selectedUnidad = unidad; this.selectedDetail = unidad; - console.log('Selected unidad:', unidad); - + this.loadChildrenAndClients(unidad.id).then(fullData => { const treeData = this.convertToTreeData(fullData); this.treeDataSource.data = treeData[0]?.children || []; @@ -171,7 +165,6 @@ export class GroupsComponent implements OnInit { async loadChildrenAndClients(id: string): Promise { try { const childrenData = await this.dataService.getChildren(id).toPromise(); - const processHierarchy = (nodes: UnidadOrganizativa[]): TreeNode[] => { return nodes.map(node => ({ name: node.name, @@ -181,8 +174,7 @@ export class GroupsComponent implements OnInit { clients: node.clients || [] })); }; - - + return { ...this.selectedUnidad, children: childrenData ? processHierarchy(childrenData) : [] @@ -201,21 +193,18 @@ export class GroupsComponent implements OnInit { children: node.children?.map(processNode) || [], hasClients: node.clients && node.clients.length > 0, }); - + return [processNode(data)]; } onNodeClick(node: TreeNode): void { - console.log('Node clicked:', node); this.selectedNode = node; - + if (node.hasClients) { const url = `${this.baseUrl}${node['@id']}`; this.http.get(url).subscribe( (data: any) => { - console.log('Clients fetched:', data); this.selectedClients = data.clients || []; - }, (error) => { console.error('Error fetching clients:', error); @@ -289,41 +278,41 @@ export class GroupsComponent implements OnInit { onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void { console.log('Deleting node or client:', node); - + const uuid = node && node['@id'] ? node['@id'].split('/').pop() || '' : ''; const name = node?.name || 'Elemento desconocido'; const type = node?.type || ''; - + event.stopPropagation(); - + const dialogRef = this.dialog.open(DeleteModalComponent, { width: '400px', data: { name } }); - + dialogRef.afterClosed().subscribe(result => { if (result === true) { this.dataService.deleteElement(uuid, type).subscribe( () => { console.log('Entity deleted successfully:', uuid); - + this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { const treeData = this.convertToTreeData(updatedData); this.treeDataSource.data = treeData[0]?.children || []; }); - + if (type === 'client' && clientNode) { console.log('Refreshing clients for node:', clientNode); this.refreshClients(clientNode); } - + this.dataService.getOrganizationalUnits().subscribe( data => { this.organizationalUnits = data; }, error => console.error('Error fetching unidades organizativas:', error) ); - + this.toastr.success('Entidad eliminada exitosamente'); }, error => { @@ -341,10 +330,10 @@ export class GroupsComponent implements OnInit { this.selectedClients = []; return; } - + const url = `${this.baseUrl}${node['@id']}`; console.log('Fetching clients for node with URL:', url); - + this.http.get(url).subscribe( (data: any) => { console.log('Response data:', data); @@ -375,6 +364,20 @@ export class GroupsComponent implements OnInit { } } + onRoomMap(room: any): void { + this.http.get('https://127.0.0.1:8443' + room['@id']).subscribe( + (response: any) => { + this.dialog.open(ClassroomViewDialogComponent, { + width: '90vw', + data: { clients: response.clients } + }); + }, + (error: any) => { + console.error('Error en la solicitud HTTP:', error); + } + ); + } + fetchCommands(): void { this.commandsLoading = true; this.http.get('https://127.0.0.1:8443/commands?page=1&itemsPerPage=30').subscribe( @@ -390,14 +393,18 @@ export class GroupsComponent implements OnInit { } executeCommand(command: any, selectedNode: any): void { - console.log('Executing command:', command.name); - this.toastr.success('Ejecutando comando: ' + command.name+ " en "+ selectedNode.name ) ; + this.toastr.success('Ejecutando comando: ' + command.name + " en " + selectedNode.name); } onClientActions(client: any): void { console.log('Client actions:', client); } + onShowClientDetail(event: MouseEvent, client: any): void { + event.stopPropagation(); + this.router.navigate(['clients', client.uuid], { state: { clientData: client } }); + } + onShowDetailsClick(event: MouseEvent, data: any): void { event.stopPropagation(); if (data.type != 'client') { @@ -429,4 +436,5 @@ export class GroupsComponent implements OnInit { hasChild = (_: number, node: FlatNode): boolean => node.expandable; isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable; + } 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 index b978520..b7bc183 100644 --- 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 @@ -1,13 +1,12 @@ -/* Contenedor principal */ .create-client-container { display: flex; flex-direction: column; padding: 16px; font-family: Arial, sans-serif; font-size: 14px; + align-items: center; } -/* Títulos */ h1, h3, h4 { margin: 0 0 16px; color: #333; @@ -27,20 +26,21 @@ h4 { margin-top: 16px; } -/* Espaciado entre contenedores */ .inputs-container { display: flex; gap: 24px; margin-top: 16px; } -/* Contenedor de cada sección */ .mat-dialog-content { flex: 1; background-color: #f9f9f9; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + min-width: 600px; + max-width: 90vw; + width: 800px; } .create-multiple-client-container { @@ -51,7 +51,6 @@ h4 { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } -/* Campos del formulario */ .client-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); @@ -66,7 +65,6 @@ h4 { width: 100%; } -/* Tabla */ .scrollable-table { max-height: 200px; overflow-y: auto; @@ -95,7 +93,6 @@ tr:hover { background-color: #f9f9f9; } -/* Botones */ button { margin-right: 8px; } @@ -115,13 +112,11 @@ button.mat-raised-button { font-weight: 600; } -/* Carga */ .loading-spinner { margin: 16px auto; display: block; } -/* Alternar formularios */ .toggle-button { background: none; border: none; @@ -135,7 +130,6 @@ button.mat-raised-button { text-decoration: none; } -/* Mejoras generales */ .mat-divider { margin: 0 16px; } @@ -151,7 +145,6 @@ input[type="file"] { display: none; } -/* Responsivo */ @media (max-width: 768px) { .inputs-container { flex-direction: column; 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 707c348..fd924bc 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 @@ -1,4 +1,5 @@ -
+
+

{{ 'addClientTitle' | translate }}s

@@ -8,7 +9,6 @@
{{ unit.name }}
-
{{ unit.path }}
@@ -20,8 +20,10 @@

o añadelos manualmente:

+
+

Clientes importados:

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 5c781ea..d79f63f 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 @@ -23,6 +23,7 @@ export class CreateClientComponent implements OnInit { loading: boolean = false; displayedColumns: string[] = ['name', 'ip']; isSingleClientForm: boolean = false; + showTextarea: boolean = true; protected netifaceTypes = [ { name: 'Eth0', value: 'eth0' }, { name: 'Eth1', value: 'eth1' }, @@ -147,8 +148,10 @@ export class CreateClientComponent implements OnInit { if (clients.length > 0) { this.uploadedClients = clients; this.toastService.success('Archivo cargado correctamente, los datos están listos para enviarse.', 'Éxito'); + this.showTextarea = false; } else { this.toastService.error('No se encontraron datos válidos', 'Error'); + this.showTextarea = true; } }; @@ -172,8 +175,10 @@ export class CreateClientComponent implements OnInit { if (clients.length > 0) { this.uploadedClients = clients; this.toastService.success('Datos cargados correctamente, los datos están listos para enviarse.', 'Éxito'); + this.showTextarea = false; } else { this.toastService.error('No se encontraron datos válidos', 'Error'); + this.showTextarea = true; } } From 21078a8ab0e2ee1545013bc01712da52de9b59b2 Mon Sep 17 00:00:00 2001 From: apuente Date: Sun, 1 Dec 2024 23:03:01 +0100 Subject: [PATCH 06/17] Refactor groups component --- .../components/groups/groups.component.css | 13 +++ .../components/groups/groups.component.html | 95 +++++++++++++----- .../app/components/groups/groups.component.ts | 96 ++++++++++++++++++- ogWebconsole/src/locale/es.json | 5 +- 4 files changed, 179 insertions(+), 30 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index 60fad89..811fba0 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -593,3 +593,16 @@ button[mat-raised-button] { gap: 1rem; align-items: center; } + +.filters-container { + display: flex; + flex-wrap: wrap; /* Permite que los elementos pasen a la siguiente línea si no caben */ + gap: 16px; /* Espaciado entre los elementos */ + margin: 16px 0; /* Separación con otros elementos */ + padding: 0 16px; /* Opcional, para añadir un poco de espacio interno */ +} + +.filters-container mat-form-field { + flex: 1 1 300px; /* Toma todo el ancho disponible hasta 300px por elemento */ + max-width: 300px; /* Limita el ancho máximo */ +} diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index b672684..308e714 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -1,5 +1,5 @@ - - +
@@ -22,8 +22,52 @@ {{ 'legendButton' | translate }}
+ -
+ + +
+ + + + Filtros + + + + +
+ + + + {{ savedFilter[0] }} + + + + + + Buscar en el árbol + + + + + Filtrar por tipo + + Todos + Unidades Organizativas + Aulas + Clientes + Grupos + + + + + Buscar cliente + + +
+
+ + @@ -44,10 +88,7 @@
- + - + -
+
@@ -112,12 +156,11 @@ {{ - node.type === 'organizational-unit' - ? 'apartment' - : node.type === 'classroom' - ? 'school' - : node.type === 'client' - ? 'computer' + node.type === 'organizational-unit' ? 'apartment' + : node.type === 'classrooms-group' ? 'meeting_room' + : node.type === 'classroom' ? 'school' + : node.type === 'clients-group' ? 'lan' + : node.type === 'client' ? 'computer' : 'group' }} @@ -139,15 +182,15 @@ 'pc-macos': node.type === 'client' && node.status === 'macos', 'pc-off': node.type === 'client' && node.status === 'off' }"> - {{ - node.type === 'organizational-unit' - ? 'apartment' - : node.type === 'classroom' - ? 'school' - : node.type === 'client' - ? 'computer' - : 'group' - }} + {{ + node.type === 'organizational-unit' ? 'apartment' + : node.type === 'classrooms-group' ? 'meeting_room' + : node.type === 'classroom' ? 'school' + : node.type === 'clients-group' ? 'lan' + : node.type === 'client' ? 'computer' + : 'group' + }} + {{ node.name }} @@ -331,7 +374,7 @@
- + \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index b9bf844..f0fa53b 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -64,8 +64,10 @@ export class GroupsComponent implements OnInit { commandsLoading: boolean = false; selectedClients: any[] = []; cols: number = 4; + selectedClientsOriginal: any[] = []; currentView: 'card' | 'list' = 'list'; - + isTreeViewActive: boolean = false; + savedFilterNames: any[] = []; @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; @@ -121,6 +123,7 @@ export class GroupsComponent implements OnInit { this.selectedUnidad = null; this.selectedDetail = null; this.selectedClients = []; + this.isTreeViewActive = false; } onTabChange(event: MatTabChangeEvent): void { @@ -133,11 +136,30 @@ export class GroupsComponent implements OnInit { getFilters(): void { this.dataService.getFilters().subscribe( - data => {}, - error => console.error('Error fetching filters:', error) + data => { + this.savedFilterNames = data.map((filter: any) => [filter.name, filter.uuid]); + }, + error => { + console.error('Error fetching filters:', error); + } ); } + loadSelectedFilter(savedFilter: any) { + const url = `${this.baseUrl}/views/` + savedFilter[1]; + console.log('llamando a:', url); + + this.dataService.getFilter(savedFilter[1]).subscribe(response => { + console.log('Response from server:', response.filters); + if (response) { + console.log('Filter1:', response.filters); + } + }, error => { + console.error('Error:', error); + }); + } + + search(): void { this.loading = true; this.dataService.getOrganizationalUnits(this.searchTerm).subscribe( @@ -160,6 +182,7 @@ export class GroupsComponent implements OnInit { const treeData = this.convertToTreeData(fullData); this.treeDataSource.data = treeData[0]?.children || []; }); + this.isTreeViewActive = true; } async loadChildrenAndClients(id: string): Promise { @@ -204,6 +227,7 @@ export class GroupsComponent implements OnInit { const url = `${this.baseUrl}${node['@id']}`; this.http.get(url).subscribe( (data: any) => { + this.selectedClientsOriginal = [...data.clients]; this.selectedClients = data.clients || []; }, (error) => { @@ -212,13 +236,17 @@ export class GroupsComponent implements OnInit { ); } else { this.selectedClients = []; + this.selectedClientsOriginal = []; } } getNodeIcon(node: any): string { + console.log('Node:', node); switch (node.type) { case 'organizational-unit': return 'apartment'; + case 'classrooms-group': return 'doors'; case 'classroom': return 'school'; + case 'clients-group': return 'lan'; case 'client': return 'computer'; default: return 'group'; } @@ -436,5 +464,67 @@ export class GroupsComponent implements OnInit { hasChild = (_: number, node: FlatNode): boolean => node.expandable; isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable; + + /* filtros */ + selectedTreeFilter: string = ''; + filterTree(searchTerm: string, filterType: string): void { + const filterNodes = (nodes: any[]): any[] => { + return nodes + .map(node => { + const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesType = filterType ? node.type.toLowerCase() === filterType.toLowerCase() : true; + + // Filtrar hijos recursivamente + const filteredChildren = node.children ? filterNodes(node.children) : []; + + // Si el nodo o algún hijo coincide, incluirlo + if (matchesName && matchesType || filteredChildren.length > 0) { + return { ...node, children: filteredChildren }; + } + return null; // Excluir nodos que no coinciden + }) + .filter(node => node !== null); // Eliminar nodos excluidos + }; + + // Aplicar filtro sobre el árbol completo + const filteredData = filterNodes(this.treeDataSource.data); + + // Actualizar el origen de datos del árbol + this.treeDataSource.data = filteredData; + } + + onTreeFilterInput(event: Event): void { + const input = event.target as HTMLInputElement; + const searchTerm = input?.value || ''; // Maneja el caso de nulo o indefinido + this.filterTree(searchTerm, this.selectedTreeFilter); + } + + onClientFilterInput(event: Event): void { + const input = event.target as HTMLInputElement; + const searchTerm = input?.value || ''; // Maneja valores nulos o indefinidos + this.filterClients(searchTerm); + } + + + filterClients(searchTerm: string): void { + if (!searchTerm) { + // Restaurar los datos originales si no hay filtro + this.selectedClients = [...this.selectedClientsOriginal]; + return; + } + + const lowerTerm = searchTerm.toLowerCase(); + + this.selectedClients = this.selectedClientsOriginal.filter(client => { + const matchesName = client.name.toLowerCase().includes(lowerTerm); + const matchesIP = client.ip?.toLowerCase().includes(lowerTerm) || false; + const matchesStatus = client.status?.toLowerCase().includes(lowerTerm) || false; + const matchesMac = client.mac?.toLowerCase().includes(lowerTerm) || false; + + return matchesName || matchesIP || matchesStatus || matchesMac; + }); + } + + } diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index fe202b0..b13e8b4 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -421,5 +421,8 @@ "TOOLTIP_MENUS": "Gestión de menús (opción deshabilitada)", "search": "Buscar", "TOOLTIP_SEARCH": "Función de búsqueda (opción deshabilitada)", - "detailsOf": "Detalles de" + "detailsOf": "Detalles de", + "editUnitMenu": "Editar", + "addInternalUnitMenu": "Añadir", + "addClientMenu": "Añadir cliente" } From 252f961b73d2f453f10b807ecde2c9c7eaba0777 Mon Sep 17 00:00:00 2001 From: apuente Date: Tue, 3 Dec 2024 10:14:15 +0100 Subject: [PATCH 07/17] Refactor groups view and update styles --- .../components/groups/groups.component.css | 115 +-- .../components/groups/groups.component.html | 683 ++++++++---------- .../app/components/groups/groups.component.ts | 56 +- .../classroom-view.component.css | 15 +- .../classroom-view.component.html | 9 +- .../create-client.component.html | 4 +- .../shared/legend/legend.component.html | 10 + 7 files changed, 390 insertions(+), 502 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index 811fba0..01d8902 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -133,22 +133,17 @@ mat-card-actions { .details-wrapper { width: 95%; padding: 20px; - /* Asegúrate de que no haya propiedades que centren el contenido */ display: block; } .details-placeholder { width: 100%; - /* Elimina max-width si existe */ - /* max-width: none; */ - /* Otros estilos existentes */ } button[mat-raised-button] { align-self: flex-start; } - @media (max-width: 1024px) { .card-container { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); @@ -195,9 +190,10 @@ mat-tree { padding: 10px; } -button{ +button { margin: 5px; } + mat-tree mat-tree-node { display: flex; align-items: center; @@ -247,29 +243,27 @@ mat-tree mat-tree-node:hover mat-icon { color: black; } -/* Iconos por tipo */ mat-tree mat-tree-node mat-icon.node-icon { color: #757575; margin-right: 10px; } mat-tree mat-tree-node mat-icon.node-icon.organizational-unit { - color: #1976d2; /* Azul para unidades organizativas */ + color: #1976d2; } mat-tree mat-tree-node mat-icon.node-icon.classroom { - color: #757575; /* Verde para aulas */ + color: #757575; } mat-tree mat-tree-node mat-icon.node-icon.client { - color: #757575; /* Naranja para clientes */ + color: #757575; } mat-tree mat-tree-node mat-icon.node-icon.group { - color: #757575; /* Rojo para grupos */ + color: #757575; } - mat-tree mat-tree-node button.mat-icon-button { margin-right: 10px; } @@ -281,10 +275,9 @@ mat-tree mat-tree-node button.mat-icon-button.disabled-toggle { } mat-tree mat-tree-node button.mat-icon-button.disabled-toggle:hover { - background-color: transparent; /* Desactiva hover */ + background-color: transparent; } - mat-tree mat-tree-node:hover { background-color: #e3f2fd; cursor: pointer; @@ -295,7 +288,13 @@ mat-tree mat-tree-node.disabled { } mat-tree mat-tree-node.disabled:hover { - background-color: transparent; /* Desactiva hover */ + background-color: transparent; +} + +.selected-node { + background-color: #e0f7fa; + border-left: 4px solid #3F51B5; + padding-left: calc(16px - 4px); } .mat-menu-item .mat-menu-item-submenu-icon { @@ -313,38 +312,33 @@ mat-tree mat-tree-node.disabled:hover { flex: 1 1 100%; max-width: 300px; } + .filter-container { margin-bottom: 16px; } .pc-og-live { - /* Estilo para clientes con status 'og-live' */ - color: #4caf50; /* Verde */ + color: #4caf50; } .pc-busy { - /* Estilo para clientes ocupados */ - color: #ff9800; /* Naranja */ + color: #ff9800; } .pc-windows { - /* Estilo para clientes con Windows */ - color: #0078d7; /* Azul Windows */ + color: #0078d7; } .pc-linux { - /* Estilo para clientes con Linux */ - color: #f0ad4e; /* Amarillo */ + color: #f0ad4e; } .pc-macos { - /* Estilo para clientes con macOS */ - color: #999999; /* Gris */ + color: #999999; } .pc-off { - /* Estilo para clientes apagados */ - color: #f44336; /* Rojo */ + color: #f44336; } .clients-card-container { @@ -356,7 +350,7 @@ mat-tree mat-tree-node.disabled:hover { } .classroom-item { - flex: 1 1 calc(25% - 16px); /* Ajusta este valor para controlar cuántas tarjetas caben en una fila */ + flex: 1 1 calc(25% - 16px); max-width: calc(25% - 16px); box-sizing: border-box; } @@ -399,20 +393,19 @@ mat-tree mat-tree-node.disabled:hover { } .tree-container { - width: 25%; /* El árbol ocupa el 25% del ancho */ + width: 25%; padding: 16px; overflow-x: hidden; - overflow-y: auto; /* Scroll si hay muchos nodos */ + overflow-y: auto; } .clients-container { - width: 75%; /* Los clientes ocupan el 75% del ancho */ + width: 75%; padding: 16px; box-sizing: border-box; - overflow-y: auto; /* Scroll si hay muchos clientes */ + overflow-y: auto; } - .clients-container h3 { margin-bottom: 15px; font-size: 1.5em; @@ -450,7 +443,6 @@ mat-tree mat-tree-node.disabled:hover { margin-bottom: 15px; } - .client-details { margin-top: 10px; } @@ -463,7 +455,6 @@ mat-tree mat-tree-node.disabled:hover { margin-bottom: 5px; } - .client-ip { display: block; font-size: 0.9em; @@ -474,9 +465,6 @@ button[mat-raised-button] { margin-top: 15px; } - - - .clients-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); @@ -503,43 +491,6 @@ button[mat-raised-button] { flex-direction: column; } - -.view-toggle-container { - display: flex; - gap: 1rem; - margin-bottom: 1rem; -} - -.clients-grid { - display: flex; - flex-wrap: wrap; - gap: 1rem; -} - -.clients-list .list-item-content { - display: flex; - justify-content: space-between; - width: 100%; - align-items: center; -} - -.client-card, .list-item-content { - border: 1px solid #ccc; - padding: 1rem; - border-radius: 5px; -} - -.client-card { - display: flex; - flex-direction: column; - align-items: center; -} - -.client-image { - width: 50px; - height: 50px; - margin-bottom: 0.5rem; -} .view-toggle-container { display: flex; gap: 1rem; @@ -585,7 +536,7 @@ button[mat-raised-button] { } .back-button { - flex-shrink: 0; /* Asegura que el botón Back no se reduzca */ + flex-shrink: 0; } .view-toggle-container { @@ -596,13 +547,13 @@ button[mat-raised-button] { .filters-container { display: flex; - flex-wrap: wrap; /* Permite que los elementos pasen a la siguiente línea si no caben */ - gap: 16px; /* Espaciado entre los elementos */ - margin: 16px 0; /* Separación con otros elementos */ - padding: 0 16px; /* Opcional, para añadir un poco de espacio interno */ + flex-wrap: wrap; + gap: 16px; + margin: 16px 0; + padding: 0 16px; } .filters-container mat-form-field { - flex: 1 1 300px; /* Toma todo el ancho disponible hasta 300px por elemento */ - max-width: 300px; /* Limita el ancho máximo */ + flex: 1 1 300px; + max-width: 300px; } diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 308e714..e25deee 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -1,390 +1,309 @@ - +
+ +

+ {{ 'adminGroupsTitle' | translate }} +

+
+ + + +
+
- -
- +
+ + + + + + + + + + +
+ + +
+ +
+ + -

- {{ 'adminGroupsTitle' | translate }} -

-
- - - -
- - - - -
- - - - Filtros - - - - -
- - - - {{ savedFilter[0] }} - - - - - - Buscar en el árbol - - - - - Filtrar por tipo - - Todos - Unidades Organizativas - Aulas - Clientes - Grupos - - - - - Buscar cliente - - -
-
- - - - - -
- - - - apartment {{ unidad.name }} - - - -
- -
- - - - - - - - - -
-
- - - - -
- - - - -
-
+
+
+

{{ selectedUnidad?.name }}

+ + + - -
-
- -
- - -
- -

{{ selectedUnidad?.name }}

- - - - - {{ - node.type === 'organizational-unit' ? 'apartment' - : node.type === 'classrooms-group' ? 'meeting_room' - : node.type === 'classroom' ? 'school' - : node.type === 'clients-group' ? 'lan' - : node.type === 'client' ? 'computer' - : 'group' - }} - - {{ node.name }} - - - - - - - {{ - node.type === 'organizational-unit' ? 'apartment' - : node.type === 'classrooms-group' ? 'meeting_room' - : node.type === 'classroom' ? 'school' - : node.type === 'clients-group' ? 'lan' - : node.type === 'client' ? 'computer' - : 'group' + + {{ + node.type === 'organizational-unit' ? 'apartment' + : node.type === 'classrooms-group' ? 'meeting_room' + : node.type === 'classroom' ? 'school' + : node.type === 'clients-group' ? 'lan' + : node.type === 'client' ? 'computer' + : 'group' }} - - - {{ node.name }} - - - IP: {{ node.ip }} - - + + + + + {{ + node.type === 'organizational-unit' ? 'apartment' + : node.type === 'classrooms-group' ? 'meeting_room' + : node.type === 'classroom' ? 'school' + : node.type === 'clients-group' ? 'lan' + : node.type === 'client' ? 'computer' + : 'group' + }} + + {{ node.name }} + + - IP: {{ node.ip }} + + + + +
+ + + + + + + + + + + + +
+

Clientes {{ selectedNode?.name ? 'del ' + selectedNode?.name : '' }}

+
+
+
+ Client Icon +
+ {{ client.name }} + {{ client.ip }} + + {{ client.status || 'off' }} + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nombre {{ client.name }} IP {{ client.ip }} MAC {{ client.mac }} OG Live {{ client.oglive }} Estado + + {{ client.status || 'off' }} + + Mantenimiento {{ client.mantenimiento }} Subred {{ client.subnet }} Plantilla PXE {{ client.pxeTemplate }} Acciones + - - - - - - - - - - - - - - - - - - - - - -
-

Clientes del {{ selectedNode?.name }}

- - -
-
-
- Client Icon -
- {{ client.name }} - {{ client.ip }} - - - - - - - -
-
-
- - - -
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nombre {{ client.name }} IP {{ client.ip }} MAC {{ client.mac }} OG Live {{ client.oglive }} Estado - - {{ client.status || 'off' }} - - Subred {{ client.subnet }} Plantilla PXE {{ client.pxeTemplate }} Acciones - - - - - - -
- - - -
- -
+ + + + + +
+
- - - \ No newline at end of file +
+
+ diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index f0fa53b..972f0c8 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -4,12 +4,10 @@ import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { MatBottomSheet } from '@angular/material/bottom-sheet'; import { MatTabChangeEvent } from '@angular/material/tabs'; -import { MatMenuTrigger } from '@angular/material/menu'; import { ToastrService } from 'ngx-toastr'; import { JoyrideService } from 'ngx-joyride'; import { FlatTreeControl } from '@angular/cdk/tree'; import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; - import { DataService } from './services/data.service'; import { UnidadOrganizativa } from './model/model'; import { CreateOrganizationalUnitComponent } from './shared/organizational-units/create-organizational-unit/create-organizational-unit.component'; @@ -25,6 +23,7 @@ import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/del import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal'; interface TreeNode { + clients?: any[]; name: string; type: string; children?: TreeNode[]; @@ -68,7 +67,7 @@ export class GroupsComponent implements OnInit { currentView: 'card' | 'list' = 'list'; isTreeViewActive: boolean = false; savedFilterNames: any[] = []; - + selectedTreeFilter: string = ''; @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; @@ -174,16 +173,32 @@ export class GroupsComponent implements OnInit { ); } - onSelectUnidad(unidad: UnidadOrganizativa): void { + onSelectUnidad(unidad: any): void { this.selectedUnidad = unidad; this.selectedDetail = unidad; - + + this.selectedClients = this.collectAllClients(unidad); + this.selectedClientsOriginal = [...this.selectedClients]; + this.loadChildrenAndClients(unidad.id).then(fullData => { const treeData = this.convertToTreeData(fullData); this.treeDataSource.data = treeData[0]?.children || []; }); + this.isTreeViewActive = true; } + + + private collectAllClients(node: any): any[] { + let clients = node.clients || []; + if (node.children && node.children.length > 0) { + node.children.forEach((child: any) => { + clients = clients.concat(this.collectAllClients(child)); + }); + } + return clients; + } + async loadChildrenAndClients(id: string): Promise { try { @@ -222,7 +237,8 @@ export class GroupsComponent implements OnInit { onNodeClick(node: TreeNode): void { this.selectedNode = node; - + this.selectedClients = node.clients || []; + this.selectedClientsOriginal = [...this.selectedClients]; if (node.hasClients) { const url = `${this.baseUrl}${node['@id']}`; this.http.get(url).subscribe( @@ -259,6 +275,10 @@ export class GroupsComponent implements OnInit { this.dataService.getOrganizationalUnits().subscribe( data => { this.organizationalUnits = data; + this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { + const treeData = this.convertToTreeData(updatedData); + this.treeDataSource.data = treeData[0]?.children || []; + }); }, error => console.error('Error fetching unidades organizativas', error) ); @@ -272,8 +292,13 @@ export class GroupsComponent implements OnInit { this.dataService.getOrganizationalUnits().subscribe( data => { this.organizationalUnits = data; + this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { + const treeData = this.convertToTreeData(updatedData); + this.treeDataSource.data = treeData[0]?.children || []; + }); if (organizationalUnit && organizationalUnit.id) { this.loadChildrenAndClients(organizationalUnit.id); + this.refreshClients(organizationalUnit); } }, error => console.error('Error fetching unidades organizativas', error) @@ -393,7 +418,7 @@ export class GroupsComponent implements OnInit { } onRoomMap(room: any): void { - this.http.get('https://127.0.0.1:8443' + room['@id']).subscribe( + this.http.get(`${this.baseUrl}`+ room['@id']).subscribe( (response: any) => { this.dialog.open(ClassroomViewDialogComponent, { width: '90vw', @@ -408,7 +433,7 @@ export class GroupsComponent implements OnInit { fetchCommands(): void { this.commandsLoading = true; - this.http.get('https://127.0.0.1:8443/commands?page=1&itemsPerPage=30').subscribe( + this.http.get(`${this.baseUrl}`+'/commands?page=1&itemsPerPage=30').subscribe( (response: any) => { this.commands = response['hydra:member']; this.commandsLoading = false; @@ -465,8 +490,6 @@ export class GroupsComponent implements OnInit { hasChild = (_: number, node: FlatNode): boolean => node.expandable; isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable; - /* filtros */ - selectedTreeFilter: string = ''; filterTree(searchTerm: string, filterType: string): void { const filterNodes = (nodes: any[]): any[] => { return nodes @@ -474,41 +497,36 @@ export class GroupsComponent implements OnInit { const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase()); const matchesType = filterType ? node.type.toLowerCase() === filterType.toLowerCase() : true; - // Filtrar hijos recursivamente const filteredChildren = node.children ? filterNodes(node.children) : []; - // Si el nodo o algún hijo coincide, incluirlo if (matchesName && matchesType || filteredChildren.length > 0) { return { ...node, children: filteredChildren }; } - return null; // Excluir nodos que no coinciden + return null; }) - .filter(node => node !== null); // Eliminar nodos excluidos + .filter(node => node !== null); }; - // Aplicar filtro sobre el árbol completo const filteredData = filterNodes(this.treeDataSource.data); - // Actualizar el origen de datos del árbol this.treeDataSource.data = filteredData; } onTreeFilterInput(event: Event): void { const input = event.target as HTMLInputElement; - const searchTerm = input?.value || ''; // Maneja el caso de nulo o indefinido + const searchTerm = input?.value || ''; this.filterTree(searchTerm, this.selectedTreeFilter); } onClientFilterInput(event: Event): void { const input = event.target as HTMLInputElement; - const searchTerm = input?.value || ''; // Maneja valores nulos o indefinidos + const searchTerm = input?.value || ''; this.filterClients(searchTerm); } filterClients(searchTerm: string): void { if (!searchTerm) { - // Restaurar los datos originales si no hay filtro this.selectedClients = [...this.selectedClientsOriginal]; return; } diff --git a/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.css b/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.css index 371b00b..bcf134e 100644 --- a/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.css +++ b/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.css @@ -31,18 +31,11 @@ mat-card { } .client-info { - position: absolute; - top: 20px; - left: 5px; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - background-color: rgba(0, 0, 0, 0); - color: black; text-align: center; - padding: 15px; - box-sizing: border-box; + margin-top: 5px; + font-size: medium; + color: gray; + align-items: center; } .client-name { diff --git a/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html b/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html index 5085467..5485a3d 100644 --- a/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html +++ b/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html @@ -10,12 +10,9 @@
{{ 'clientAlt' | translate }} -
-
{{ client.name }}
-
- {{ client.ip }} -
-
+
+
+ {{ client.name }}
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 fd924bc..56ca328 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,7 @@ - - + +
diff --git a/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html b/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html index c60c785..6cd66de 100644 --- a/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html +++ b/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html @@ -19,4 +19,14 @@ computer
{{ 'clientTitle' | translate }}
+ + + school +
Disponible acceso remoto
+
+ + school +
No disponible acceso remoto
+
+ From 00d9fdf536615d1c0d9ff936814990a869f8c07b Mon Sep 17 00:00:00 2001 From: apuente Date: Tue, 3 Dec 2024 10:37:45 +0100 Subject: [PATCH 08/17] Refactor groups component tests --- .../groups/groups.component.spec.ts | 127 ------------------ 1 file changed, 127 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/groups.component.spec.ts b/ogWebconsole/src/app/components/groups/groups.component.spec.ts index 5fb654f..b5646bf 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.spec.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.spec.ts @@ -113,131 +113,4 @@ describe('GroupsComponent', () => { expect(component.onSelectUnidad).toHaveBeenCalledWith(unidad); }); - it('should call onSelectChild method', () => { - spyOn(component, 'onSelectChild'); - const child = { id: '1', name: 'Test', type: 'unit' } as any; - component.onSelectChild(child); - expect(component.onSelectChild).toHaveBeenCalledWith(child); - }); - - it('should call navigateToBreadcrumb method', () => { - spyOn(component, 'navigateToBreadcrumb'); - component.navigateToBreadcrumb(1); - expect(component.navigateToBreadcrumb).toHaveBeenCalledWith(1); - }); - - it('should call loadChildrenAndClients method', () => { - spyOn(component, 'loadChildrenAndClients'); - component.loadChildrenAndClients('1'); - expect(component.loadChildrenAndClients).toHaveBeenCalledWith('1'); - }); - - it('should call onDeleteClick method', () => { - spyOn(component, 'onDeleteClick'); - const event = new MouseEvent('click'); - component.onDeleteClick(event, 'uuid', 'name', 'client'); - expect(component.onDeleteClick).toHaveBeenCalledWith(event, 'uuid', 'name', 'client'); - }); - - it('should call onEditClick method', () => { - spyOn(component, 'onEditClick'); - const event = new MouseEvent('click'); - component.onEditClick(event, 'client', 'uuid'); - expect(component.onEditClick).toHaveBeenCalledWith(event, 'client', 'uuid'); - }); - - it('should call onShowClick method', () => { - spyOn(component, 'onShowClick'); - const event = new MouseEvent('click'); - component.onShowClick(event, { type: 'unit' }); - expect(component.onShowClick).toHaveBeenCalledWith(event, { type: 'unit' }); - }); - - it('should call onTreeClick method', () => { - spyOn(component, 'onTreeClick'); - const event = new MouseEvent('click'); - component.onTreeClick(event, { type: 'unit' }); - expect(component.onTreeClick).toHaveBeenCalledWith(event, { type: 'unit' }); - }); - - it('should call onExecuteCommand method', () => { - spyOn(component, 'onExecuteCommand'); - const event = new MouseEvent('click'); - component.onExecuteCommand(event, 'child', 'name', 'type'); - expect(component.onExecuteCommand).toHaveBeenCalledWith(event, 'child', 'name', 'type'); - }); - - it('should call openSnackBar method', () => { - spyOn(component, 'openSnackBar'); - component.openSnackBar(true, 'message'); - expect(component.openSnackBar).toHaveBeenCalledWith(true, 'message'); - }); - - it('should call openBottomSheet method', () => { - spyOn(component, 'openBottomSheet'); - component.openBottomSheet(); - expect(component.openBottomSheet).toHaveBeenCalled(); - }); - - it('should call roomMap method', () => { - spyOn(component, 'roomMap'); - component.roomMap(); - expect(component.roomMap).toHaveBeenCalled(); - }); - - it('should call applyFilter method', () => { - spyOn(component, 'applyFilter'); - component.applyFilter(); - expect(component.applyFilter).toHaveBeenCalled(); - }); - - it('should call onPageChange method', () => { - spyOn(component, 'onPageChange'); - const event = { pageIndex: 1, pageSize: 10 } as any; - component.onPageChange(event); - expect(component.onPageChange).toHaveBeenCalledWith(event); - }); - - it('should call saveFilters method', () => { - spyOn(component, 'saveFilters'); - component.saveFilters(); - expect(component.saveFilters).toHaveBeenCalled(); - }); - - it('should call loadSelectedFilter method', () => { - spyOn(component, 'loadSelectedFilter'); - component.loadSelectedFilter(['name', 'uuid']); - expect(component.loadSelectedFilter).toHaveBeenCalledWith(['name', 'uuid']); - }); - - it('should call onCheckboxChange method', () => { - spyOn(component, 'onCheckboxChange'); - const event = { checked: true } as any; - component.onCheckboxChange(event, 'name', 'uuid'); - expect(component.onCheckboxChange).toHaveBeenCalledWith(event, 'name', 'uuid'); - }); - - it('should call toggleSelectAll method', () => { - spyOn(component, 'toggleSelectAll'); - component.toggleSelectAll(); - expect(component.toggleSelectAll).toHaveBeenCalled(); - }); - - it('should call isSelected method', () => { - spyOn(component, 'isSelected'); - component.isSelected('name'); - expect(component.isSelected).toHaveBeenCalledWith('name'); - }); - - it('should call sendActions method', () => { - spyOn(component, 'sendActions'); - component.sendActions(); - expect(component.sendActions).toHaveBeenCalled(); - }); - - it('should call iniciarTour method', () => { - spyOn(component, 'iniciarTour'); - component.iniciarTour(); - expect(component.iniciarTour).toHaveBeenCalled(); - }); }); From 7e9e5c638ca85a3228f121676a1999f07fe3fe2b Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Tue, 3 Dec 2024 11:25:42 +0100 Subject: [PATCH 09/17] refs #917. Partition assistant --- .../task-logs/task-logs.component.ts | 5 + .../client-main-view.component.css | 27 ++ .../client-main-view.component.html | 54 ++-- .../client-main-view.component.ts | 56 +++- .../partition-assistant.component.css | 40 +++ .../partition-assistant.component.html | 143 ++++++---- .../partition-assistant.component.ts | 244 +++++++++++------- .../app/shared/constants/filesystem-types.ts | 26 ++ .../app/shared/constants/partition-types.ts | 72 ++++++ 9 files changed, 487 insertions(+), 180 deletions(-) create mode 100644 ogWebconsole/src/app/shared/constants/filesystem-types.ts create mode 100644 ogWebconsole/src/app/shared/constants/partition-types.ts diff --git a/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.ts b/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.ts index 37b65e1..d605f41 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.ts +++ b/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.ts @@ -50,6 +50,11 @@ export class TaskLogsComponent implements OnInit { header: 'Hilo de trabajo', cell: (trace: any) => `${trace.jobId}` }, + { + columnDef: 'output', + header: 'Logs', + cell: (trace: any) => `${trace.output}` + }, { columnDef: 'executedAt', header: 'Programación de ejecución', diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css index c86bb98..c1c12f0 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css @@ -233,3 +233,30 @@ text-anchor: middle; } +.disk-container { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + gap: 20px; +} + +.table-container { + flex: 3; + overflow-x: auto; +} + +.charts-container { + flex: 2; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.disk-usage { + text-align: center; +} + + + diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html index a78d6f8..c3add36 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html @@ -36,40 +36,44 @@

Discos/Particiones

-
- - - - - - - -
{{ column.header }} - - {{ column.cell(image) }} - - - - {{ (image.size / 1024).toFixed(2) }} GB - - -
-
+
+ +
+ + + + + + + +
{{ column.header }} + + {{ column.cell(image) }} + + + + {{ (image.size / 1024).toFixed(2) }} GB + + +
+
-
- -
+ +
+
+ [doughnut]="true" + >

Disco {{ disk.diskNumber }}

Usado: {{ (disk.used).toFixed(2) }} GB ({{ disk.percentage }}%)

Total: {{ disk.total }} GB

-
- + +
+ 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 78b9a40..6e9728b 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 @@ -6,6 +6,7 @@ import {PartitionAssistantComponent} from "./partition-assistant/partition-assis 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"; interface ClientInfo { property: string; @@ -90,7 +91,8 @@ export class ClientMainViewComponent implements OnInit { constructor( private http: HttpClient, private dialog: MatDialog, - private router: Router + private router: Router, + private toastService: ToastrService ) { const url = window.location.href; const segments = url.split('/'); @@ -192,7 +194,7 @@ export class ClientMainViewComponent implements OnInit { } loadPartitions(): void { - this.http.get(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}`).subscribe({ + this.http.get(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[partitionNumber]=ASC`).subscribe({ next: data => { this.dataSource = data['hydra:member']; this.partitions = data['hydra:member']; @@ -216,6 +218,7 @@ export class ClientMainViewComponent implements OnInit { } onCommandSelect(action: any): void { + console.log(action); if (action === 'partition') { this.openPartitionAssistant(); } @@ -227,6 +230,55 @@ export class ClientMainViewComponent implements OnInit { if (action === 'deploy-image') { this.openDeployImageAssistant(); } + + if (action === 'reboot') { + this.rebootClient(); + } + + if (action === 'power-off') { + this.powerOffClient(); + } + + if (action === 'power-on') { + this.powerOnClient(); + } + } + + rebootClient(): void { + this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/reboot`, {}).subscribe( + response => { + this.toastService.success('Cliente actualizado correctamente'); + }, + error => { + this.toastService.error('Error de conexión con el cliente'); + } + ); + } + + powerOnClient(): void { + const payload = { + client: this.clientData['@id'] + } + + this.http.post(`${this.baseUrl}${this.clientData.repository['@id']}/wol`, payload).subscribe( + response => { + this.toastService.success('Cliente actualizado correctamente'); + }, + error => { + this.toastService.error('Error de conexión con el cliente'); + } + ); + } + + powerOffClient(): void { + this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/power-off`, {}).subscribe( + response => { + this.toastService.success('Cliente actualizado correctamente'); + }, + error => { + this.toastService.error('Error de conexión con el cliente'); + } + ); } openPartitionAssistant(): void { diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css index 284808b..3746bb0 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css @@ -122,3 +122,43 @@ button.remove-btn:hover { border-radius: 4px; margin-top: 10px; } + +.partition-assistant .row { + display: flex; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.form-container { + flex: 0 0 65%; + max-width: 65%; + padding-right: 20px; + box-sizing: border-box; +} + +.chart-container { + flex: 0 0 35%; + max-width: 35%; +} + +.partition-bar { + display: flex; + height: 40px; + margin: 20px 0; +} + +.partition-segment { + display: flex; + justify-content: center; + align-items: center; + text-align: center; + font-size: 10px; + color: white; + height: 100%; +} + +.chart-container ngx-charts-pie-chart { + display: block; + align-content: center; + justify-self: center; +} diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html index af3e4ca..311b8f6 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html @@ -1,67 +1,100 @@ -

Asistente de particionado

+
+

Asistente de particionado

+
+ +
+
+
- + Tamaño: {{ (disk.totalDiskSize / 1024).toFixed(2) }} GB
-
-
- {{ partition.type }} ({{ (partition.size / 1024).toFixed(2) }} GB) +
+
+ {{ partition.partitionCode }} ({{ (partition.size / 1024).toFixed(2) }} GB) +
+
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ParticiónTipo particiónS. ficherosTamaño (MB)Tamaño (%)FormatearEliminar
{{ partition.partitionNumber }} + + + + + + + + + + + +
+
+ +
+ + +
+ - - - - - - - - - - - - - - - - - - - - - - - -
ParticiónTipo particiónTamaño (MB)Uso (%)FormatearEliminar
{{ partition.partitionNumber }} - - - - - - - - - -
-
{{ errorMessage }}
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts index ab0803c..cab33a8 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts @@ -2,18 +2,23 @@ import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/c import { HttpClient } from '@angular/common/http'; import { ToastrService } from 'ngx-toastr'; import {MAT_DIALOG_DATA} from "@angular/material/dialog"; -import {ActivatedRoute} from "@angular/router"; +import {ActivatedRoute, Router} from "@angular/router"; +import { PARTITION_TYPES } from '../../../../../shared/constants/partition-types'; +import { FILESYSTEM_TYPES } from '../../../../../shared/constants/filesystem-types'; +import {toUnredirectedSourceFile} from "@angular/compiler-cli/src/ngtsc/util/src/typescript"; interface Partition { uuid?: string; partitionNumber: number; size: number; - type: string; + partitionCode: string; + filesystem: string; sizeBytes: number; memoryUsage: number; format: boolean; color: string; percentage: number; + removed: boolean; } @Component({ @@ -24,19 +29,27 @@ interface Partition { export class PartitionAssistantComponent implements OnInit { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; @Output() dataChange = new EventEmitter(); - + partitionTypes = PARTITION_TYPES; + filesystemTypes = FILESYSTEM_TYPES; errorMessage = ''; originalPartitions: any[] = []; clientId: string | null = null; + newPartitions: any[] = []; + updateRequests: any[] = []; data: any = {}; - disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[] }[] = []; + disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = []; private apiUrl: string = this.baseUrl + '/partitions'; + view: [number, number] = [400, 300]; + showLegend = true; + showLabels = true; + constructor( private http: HttpClient, private toastService: ToastrService, - private route: ActivatedRoute + private route: ActivatedRoute, + private router: Router, ) {} ngOnInit() { @@ -77,33 +90,81 @@ export class PartitionAssistantComponent implements OnInit { partitionNumber: partition.partitionNumber, size: this.convertBytesToGB(partition.size), memoryUsage: partition.memoryUsage, - type: partition.type || partition.filesystem || 'NTFS', + partitionCode: partition.partitionCode, + filesystem: partition.filesystem, sizeBytes: partition.size, format: false, color: '#1f1b91', - percentage: 0 + percentage: 0, + removed: false }); } }); disksMap.forEach((disk, diskNumber) => { this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); + + const used = this.calculateUsedSpace(disk.partitions); + const percentage = (used / disk.totalDiskSize) * 100; + const chartData = this.generateChartData(disk.partitions); + this.disks.push({ diskNumber: diskNumber, totalDiskSize: disk.totalDiskSize, - partitions: disk.partitions + partitions: disk.partitions, + chartData: chartData, + used: used, + percentage: percentage }); }); + this.disks.forEach((disk) => { + this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); + }); } convertBytesToGB(bytes: number): number { return bytes } + activePartitions(diskNumber: number) { + const disk = this.disks.find((d) => d.diskNumber === diskNumber); + if (disk) { + return disk.partitions.filter((partition) => !partition.removed); + } + + return null; + } + updatePartitionPercentages(partitions: Partition[], totalDiskSize: number) { + let totalUsedPercentage = 0; + partitions.forEach((partition) => { - partition.percentage = (partition.size / totalDiskSize) * 100 + partition.percentage = Number(((partition.size / totalDiskSize) * 100).toFixed(2)) + totalUsedPercentage += partition.percentage; }); + + const unusedPercentage = 100 - totalUsedPercentage; + + const chartData = partitions + .filter((partition) => !partition.removed) + .map((partition) => ({ + name: `Partición ${partition.partitionNumber}`, + value: partition.percentage, + color: partition.color, + })); + + if (unusedPercentage > 0) { + chartData.push({ + name: 'Espacio sin usar', + value: unusedPercentage, + color: '#ffffff' + }); + } + + const disk = this.disks.find(d => d.partitions === partitions); + if (disk) { + disk.chartData = chartData; + } } addPartition(diskNumber: number) { @@ -112,21 +173,26 @@ export class PartitionAssistantComponent implements OnInit { if (disk) { const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize); if (remainingGB > 0) { + const removedPartitions = disk.partitions.filter((p) => !p.removed); const maxPartitionNumber = - disk.partitions.length > 0 ? Math.max(...disk.partitions.map((p) => p.partitionNumber)) : 0; + removedPartitions.length > 0 ? Math.max(...removedPartitions.map((p) => p.partitionNumber)) : 0; const newPartitionNumber = maxPartitionNumber + 1; + disk.partitions.push({ partitionNumber: newPartitionNumber, size: 0, - type: 'NTFS', + partitionCode: 'LINUX', + filesystem: 'EXT4', memoryUsage: 0, sizeBytes: 0, format: false, color: '#' + Math.floor(Math.random() * 16777215).toString(16), - percentage: 0 + percentage: 0, + removed: false }); this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); + this.updateDiskChart(disk); } else { this.errorMessage = 'No hay suficiente espacio libre en el disco para crear una nueva partición.'; } @@ -138,7 +204,9 @@ export class PartitionAssistantComponent implements OnInit { if (disk) { const partition = disk.partitions[index]; const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize) + partition.size; - + if (partition) { + partition.percentage = Number(((size / disk.totalDiskSize) * 100).toFixed(2)); + } if (size > remainingGB) { this.errorMessage = `El tamaño de la partición no puede superar el espacio libre (${remainingGB.toFixed(2)} GB).`; } else { @@ -148,41 +216,16 @@ export class PartitionAssistantComponent implements OnInit { partition.percentage = (size / disk.totalDiskSize) * 100; this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); + this.updateDiskChart(disk); } } } - updatePartitionPercentage(diskNumber: number, index: number, percentage: number) { - const disk = this.disks.find((d) => d.diskNumber === diskNumber); - if (disk) { - const partition = disk.partitions[index]; - - const newSizeMB = (percentage / 100) * disk.totalDiskSize; - - const totalPercentage = disk.partitions.reduce((sum, part) => sum + (part === partition ? percentage : part.percentage), 0); - - if (totalPercentage > 100) { - this.errorMessage = 'El tamaño total en porcentaje de las particiones no puede exceder el 100%'; - partition.percentage = 100 - (totalPercentage - percentage); - } else { - this.errorMessage = ''; - partition.percentage = percentage; - partition.size = newSizeMB; - } - } - } - - - getRemainingGB(partitions: Partition[], totalDiskSize: number): number { const totalUsedGB = partitions.reduce((acc, partition) => acc + partition.size, 0); return Math.max(0, totalDiskSize - totalUsedGB); } - isPartitionModified(original: any, current: Partition): boolean { - return this.convertBytesToGB(original.size) !== current.size || original.type !== current.type; - } - getModifiedOrNewPartitions() { const modifiedPartitions: any[] = []; @@ -191,22 +234,11 @@ export class PartitionAssistantComponent implements OnInit { const originalPartition = this.originalPartitions.find( (p) => p.diskNumber === disk.diskNumber && p.partitionNumber === partition.partitionNumber ); - - if (!originalPartition) { - modifiedPartitions.push({ - partition, - diskNumber: disk.diskNumber, - partitionNumber: partition.partitionNumber, - isNew: true - }); - } else if (this.isPartitionModified(originalPartition, partition)) { - modifiedPartitions.push({ - partition, - diskNumber: disk.diskNumber, - partitionNumber: partition.partitionNumber, - isNew: false - }); - } + modifiedPartitions.push({ + partition, + diskNumber: disk.diskNumber, + partitionNumber: partition.partitionNumber, + }); }); }); @@ -219,6 +251,7 @@ export class PartitionAssistantComponent implements OnInit { return totalPartitionSize > disk.totalDiskSize; }); + console.log(invalidDisks); if (invalidDisks.length > 0) { this.errorMessage = 'El tamaño total de las particiones en uno o más discos excede el tamaño total del disco.'; return; @@ -233,63 +266,78 @@ export class PartitionAssistantComponent implements OnInit { return; } - modifiedPartitions.forEach(({ partition, diskNumber, partitionNumber, isNew }) => { + modifiedPartitions.forEach(({ partition, diskNumber, partitionNumber }) => { const payload = { diskNumber: diskNumber, partitionNumber: partitionNumber, memoryUsage: partition.memoryUsage, size: partition.size, - filesystem: partition.type, - client: `/clients/${this.clientId}` + partitionCode: partition.partitionCode, + filesystem: partition.filesystem, + client: `/clients/${this.clientId}`, + uuid: partition.uuid, + removed: partition.removed || false, + format: partition.format || false, }; - if (isNew) { - this.http.post(this.apiUrl, payload).subscribe( - (response) => { - this.toastService.success('Partición creada exitosamente'); - window.location.reload(); - }, - (error) => { - console.error('Error al crear la partición:', error); - this.toastService.error('Error al crear la partición'); - } - ); - } else if (partition.uuid) { - const patchUrl = `${this.apiUrl}/${partition.uuid}`; - this.http.patch(patchUrl, payload).subscribe( - (response) => { - this.toastService.success('Partición actualizada exitosamente'); - window.location.reload(); - }, - (error) => { - console.error('Error al actualizar la partición:', error); - this.toastService.error('Error al actualizar la partición'); - } - ); - } + this.newPartitions.push(payload); }); + + if (this.newPartitions.length > 0) { + const bulkPayload = { partitions: this.newPartitions }; + + this.http.post(this.apiUrl, bulkPayload).subscribe( + (response) => { + this.toastService.success('Particiones creadas exitosamente'); + this.router.navigate(['/traces']); + }, + (error) => { + console.error('Error al crear las particiones:', error); + this.toastService.error('Error al crear las particiones'); + } + ); + } } removePartition(diskNumber: number, partition: Partition) { const disk = this.disks.find((d) => d.diskNumber === diskNumber); - if (disk) { - const index = disk.partitions.indexOf(partition); - if (index !== -1) { - disk.partitions.splice(index, 1); - this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); - - if (partition.uuid) { - const deleteUrl = `${this.apiUrl}/${partition.uuid}`; - this.http.delete(deleteUrl).subscribe( - (response) => { - this.toastService.success('Partición eliminada exitosamente'); - window.location.reload(); - }, - (error) => {} - ); - } + const partitionToRemove = disk.partitions.find((p) => p === partition); + if (partitionToRemove) { + partitionToRemove.removed = true; } + this.updateDiskChart(disk); + this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); } } + + updatePartitionSizeFromPercentage(diskNumber: number, partitionIndex: number, percentage: number): void { + const disk = this.disks.find(d => d.diskNumber === diskNumber); + if (disk) { + const partition = disk.partitions[partitionIndex]; + if (partition) { + partition.size = (disk.totalDiskSize * percentage) / 100; + } + this.updateDiskChart(disk); + this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); + } + } + + calculateUsedSpace(partitions: Partition[]): number { + return partitions.reduce((acc, partition) => acc + partition.size, 0); + } + + generateChartData(partitions: Partition[]): any[] { + return partitions.map((partition) => ({ + name: `Partición ${partition.partitionNumber}`, + value: partition.percentage, + color: partition.color + })); + } + + updateDiskChart(disk: any) { + disk.chartData = this.generateChartData(disk.partitions); + disk.used = this.calculateUsedSpace(disk.partitions); + disk.percentage = (disk.used / disk.totalDiskSize) * 100; + } } diff --git a/ogWebconsole/src/app/shared/constants/filesystem-types.ts b/ogWebconsole/src/app/shared/constants/filesystem-types.ts new file mode 100644 index 0000000..dfbcc56 --- /dev/null +++ b/ogWebconsole/src/app/shared/constants/filesystem-types.ts @@ -0,0 +1,26 @@ +export const FILESYSTEM_TYPES = [ + { id: 1, name: 'EMPTY', description: 'EMPTY', active: 0 }, + { id: 2, name: 'CACHE', description: 'CACHE', active: 0 }, + { id: 3, name: 'BTRFS', description: 'BTRFS', active: 0 }, + { id: 4, name: 'EXT2', description: 'EXT2', active: 0 }, + { id: 5, name: 'EXT3', description: 'EXT3', active: 0 }, + { id: 6, name: 'EXT4', description: 'EXT4', active: 0 }, + { id: 7, name: 'FAT12', description: 'FAT12', active: 0 }, + { id: 8, name: 'FAT16', description: 'FAT16', active: 0 }, + { id: 9, name: 'FAT32', description: 'FAT32', active: 0 }, + { id: 10, name: 'HFS', description: 'HFS', active: 0 }, + { id: 11, name: 'HFSPLUS', description: 'HFSPLUS', active: 0 }, + { id: 12, name: 'JFS', description: 'JFS', active: 0 }, + { id: 13, name: 'NTFS', description: 'NTFS', active: 0 }, + { id: 14, name: 'REISERFS', description: 'REISERFS', active: 0 }, + { id: 15, name: 'REISER4', description: 'REISER4', active: 0 }, + { id: 16, name: 'UFS', description: 'UFS', active: 0 }, + { id: 17, name: 'XFS', description: 'XFS', active: 0 }, + { id: 18, name: 'EXFAT', description: 'EXFAT', active: 0 }, + { id: 19, name: 'LINUX-SWAP', description: 'LINUX-SWAP', active: 0 }, + { id: 20, name: 'F2FS', description: 'F2FS', active: 0 }, + { id: 21, name: 'NILFS2', description: 'NILFS2', active: 0 }, + { id: 22, name: '0', description: '', active: 0 }, + { id: 23, name: 'LINUX-LVM', description: '', active: 0 }, + { id: 24, name: 'ISO9660', description: '', active: 0 }, +]; diff --git a/ogWebconsole/src/app/shared/constants/partition-types.ts b/ogWebconsole/src/app/shared/constants/partition-types.ts new file mode 100644 index 0000000..1b900ac --- /dev/null +++ b/ogWebconsole/src/app/shared/constants/partition-types.ts @@ -0,0 +1,72 @@ +export const PARTITION_TYPES = [ + { code: 0, name: 'EMPTY', active: false }, + { code: 1, name: 'FAT12', active: true }, + { code: 5, name: 'EXTENDED', active: false }, + { code: 6, name: 'FAT16', active: true }, + { code: 7, name: 'NTFS', active: true }, + { code: 11, name: 'FAT32', active: true }, + { code: 17, name: 'HFAT12', active: true }, + { code: 22, name: 'HFAT16', active: true }, + { code: 23, name: 'HNTFS', active: true }, + { code: 27, name: 'HFAT32', active: true }, + { code: 130, name: 'LINUX-SWAP', active: false }, + { code: 131, name: 'LINUX', active: true }, + { code: 142, name: 'LINUX-LVM', active: true }, + { code: 165, name: 'FREEBSD', active: true }, + { code: 166, name: 'OPENBSD', active: true }, + { code: 169, name: 'NETBSD', active: true }, + { code: 175, name: 'HFS', active: true }, + { code: 190, name: 'SOLARIS-BOOT', active: true }, + { code: 191, name: 'SOLARIS', active: true }, + { code: 202, name: 'CACHE', active: false }, + { code: 218, name: 'DATA', active: true }, + { code: 238, name: 'GPT', active: false }, + { code: 239, name: 'EFI', active: true }, + { code: 251, name: 'VMFS', active: true }, + { code: 253, name: 'LINUX-RAID', active: true }, + { code: 1792, name: 'WINDOWS', active: true }, + { code: 3073, name: 'WIN-RESERV', active: true }, + { code: 9984, name: 'WIN-RECOV', active: true }, + { code: 32512, name: 'CHROMEOS-KRN', active: true }, + { code: 32513, name: 'CHROMEOS', active: true }, + { code: 32514, name: 'CHROMEOS-RESERV', active: true }, + { code: 33280, name: 'LINUX-SWAP', active: false }, + { code: 33536, name: 'LINUX', active: true }, + { code: 33537, name: 'LINUX-RESERV', active: true }, + { code: 33538, name: 'LINUX', active: true }, + { code: 36352, name: 'LINUX-LVM', active: true }, + { code: 42240, name: 'FREEBSD-DISK', active: false }, + { code: 42241, name: 'FREEBSD-BOOT', active: true }, + { code: 42242, name: 'FREEBSD-SWAP', active: false }, + { code: 42243, name: 'FREEBSD', active: true }, + { code: 42244, name: 'FREEBSD', active: true }, + { code: 43265, name: 'NETBSD-SWAP', active: false }, + { code: 43266, name: 'NETBSD', active: true }, + { code: 43267, name: 'NETBSD', active: true }, + { code: 43268, name: 'NETBSD', active: true }, + { code: 43269, name: 'NETBSD', active: true }, + { code: 43270, name: 'NETBSD-RAID', active: true }, + { code: 43776, name: 'HFS-BOOT', active: true }, + { code: 44800, name: 'HFS', active: true }, + { code: 44801, name: 'HFS-RAID', active: true }, + { code: 44802, name: 'HFS-RAID', active: true }, + { code: 48640, name: 'SOLARIS-BOOT', active: true }, + { code: 48896, name: 'SOLARIS', active: true }, + { code: 48897, name: 'SOLARIS', active: true }, + { code: 48898, name: 'SOLARIS-SWAP', active: false }, + { code: 48899, name: 'SOLARIS-DISK', active: true }, + { code: 48900, name: 'SOLARIS', active: true }, + { code: 48901, name: 'SOLARIS', active: true }, + { code: 51712, name: 'CACHE', active: false }, + { code: 61184, name: 'EFI', active: true }, + { code: 61185, name: 'MBR', active: false }, + { code: 61186, name: 'BIOS-BOOT', active: false }, + { code: 64256, name: 'VMFS', active: true }, + { code: 64257, name: 'VMFS-RESERV', active: true }, + { code: 64258, name: 'VMFS-KRN', active: true }, + { code: 64768, name: 'LINUX-RAID', active: true }, + { code: 65535, name: 'UNKNOWN', active: true }, + { code: 65536, name: 'LVM-LV', active: true }, + { code: 65552, name: 'ZFS-VOL', active: true }, + { code: 39, name: 'HNTFS-WINRE', active: true }, +]; From aa9b82cda522b453457c6b06e66030c9c79a0125 Mon Sep 17 00:00:00 2001 From: apuente Date: Tue, 3 Dec 2024 12:33:40 +0100 Subject: [PATCH 10/17] Refactor groups component to add sync functionality --- .../components/groups/groups.component.html | 19 ++++- .../app/components/groups/groups.component.ts | 84 +++++++++---------- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index e25deee..0c1b9fc 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -214,6 +214,11 @@ + + + + +
- + \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 972f0c8..0d64752 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -30,7 +30,7 @@ interface TreeNode { ip?: string; '@id'?: string; hasClients?: boolean; - status?: string ; + status?: string; } interface FlatNode { @@ -68,6 +68,10 @@ export class GroupsComponent implements OnInit { isTreeViewActive: boolean = false; savedFilterNames: any[] = []; selectedTreeFilter: string = ''; + + syncStatus: boolean = false; + syncingClientId: number | null = null; + @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; @@ -109,6 +113,7 @@ export class GroupsComponent implements OnInit { this.updateGridCols(); window.addEventListener('resize', () => this.updateGridCols()); } + toggleView(view: 'card' | 'list') { this.currentView = view; } @@ -146,10 +151,7 @@ export class GroupsComponent implements OnInit { loadSelectedFilter(savedFilter: any) { const url = `${this.baseUrl}/views/` + savedFilter[1]; - console.log('llamando a:', url); - this.dataService.getFilter(savedFilter[1]).subscribe(response => { - console.log('Response from server:', response.filters); if (response) { console.log('Filter1:', response.filters); } @@ -158,7 +160,6 @@ export class GroupsComponent implements OnInit { }); } - search(): void { this.loading = true; this.dataService.getOrganizationalUnits(this.searchTerm).subscribe( @@ -176,29 +177,24 @@ export class GroupsComponent implements OnInit { onSelectUnidad(unidad: any): void { this.selectedUnidad = unidad; this.selectedDetail = unidad; - this.selectedClients = this.collectAllClients(unidad); this.selectedClientsOriginal = [...this.selectedClients]; - this.loadChildrenAndClients(unidad.id).then(fullData => { const treeData = this.convertToTreeData(fullData); this.treeDataSource.data = treeData[0]?.children || []; }); - this.isTreeViewActive = true; } - private collectAllClients(node: any): any[] { - let clients = node.clients || []; + let clients = node.clients || []; if (node.children && node.children.length > 0) { node.children.forEach((child: any) => { - clients = clients.concat(this.collectAllClients(child)); + clients = clients.concat(this.collectAllClients(child)); }); } return clients; } - async loadChildrenAndClients(id: string): Promise { try { @@ -237,8 +233,8 @@ export class GroupsComponent implements OnInit { onNodeClick(node: TreeNode): void { this.selectedNode = node; - this.selectedClients = node.clients || []; - this.selectedClientsOriginal = [...this.selectedClients]; + this.selectedClients = node.clients || []; + this.selectedClientsOriginal = [...this.selectedClients]; if (node.hasClients) { const url = `${this.baseUrl}${node['@id']}`; this.http.get(url).subscribe( @@ -257,7 +253,6 @@ export class GroupsComponent implements OnInit { } getNodeIcon(node: any): string { - console.log('Node:', node); switch (node.type) { case 'organizational-unit': return 'apartment'; case 'classrooms-group': return 'doors'; @@ -275,10 +270,10 @@ export class GroupsComponent implements OnInit { this.dataService.getOrganizationalUnits().subscribe( data => { this.organizationalUnits = data; - this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { + this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { const treeData = this.convertToTreeData(updatedData); this.treeDataSource.data = treeData[0]?.children || []; - }); + }); }, error => console.error('Error fetching unidades organizativas', error) ); @@ -330,8 +325,6 @@ export class GroupsComponent implements OnInit { } onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void { - console.log('Deleting node or client:', node); - const uuid = node && node['@id'] ? node['@id'].split('/').pop() || '' : ''; const name = node?.name || 'Elemento desconocido'; const type = node?.type || ''; @@ -347,15 +340,12 @@ export class GroupsComponent implements OnInit { if (result === true) { this.dataService.deleteElement(uuid, type).subscribe( () => { - console.log('Entity deleted successfully:', uuid); - this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { const treeData = this.convertToTreeData(updatedData); this.treeDataSource.data = treeData[0]?.children || []; }); if (type === 'client' && clientNode) { - console.log('Refreshing clients for node:', clientNode); this.refreshClients(clientNode); } @@ -363,7 +353,7 @@ export class GroupsComponent implements OnInit { data => { this.organizationalUnits = data; }, - error => console.error('Error fetching unidades organizativas:', error) + error => console.error('Error fetching unidades organizativas', error) ); this.toastr.success('Entidad eliminada exitosamente'); @@ -379,22 +369,16 @@ export class GroupsComponent implements OnInit { private refreshClients(node: TreeNode): void { if (!node || !node['@id']) { - console.warn('Node or @id is missing, clearing clients.'); this.selectedClients = []; return; } const url = `${this.baseUrl}${node['@id']}`; - console.log('Fetching clients for node with URL:', url); - this.http.get(url).subscribe( (data: any) => { - console.log('Response data:', data); if (data && Array.isArray(data.clients)) { this.selectedClients = data.clients; - console.log('Clients updated successfully:', this.selectedClients); } else { - console.warn('No "clients" field found in response, clearing clients.'); this.selectedClients = []; } }, @@ -496,53 +480,65 @@ export class GroupsComponent implements OnInit { .map(node => { const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase()); const matchesType = filterType ? node.type.toLowerCase() === filterType.toLowerCase() : true; - const filteredChildren = node.children ? filterNodes(node.children) : []; - if (matchesName && matchesType || filteredChildren.length > 0) { return { ...node, children: filteredChildren }; } return null; }) - .filter(node => node !== null); + .filter(node => node !== null); }; - + const filteredData = filterNodes(this.treeDataSource.data); - this.treeDataSource.data = filteredData; } onTreeFilterInput(event: Event): void { const input = event.target as HTMLInputElement; - const searchTerm = input?.value || ''; + const searchTerm = input?.value || ''; this.filterTree(searchTerm, this.selectedTreeFilter); } onClientFilterInput(event: Event): void { const input = event.target as HTMLInputElement; - const searchTerm = input?.value || ''; + const searchTerm = input?.value || ''; this.filterClients(searchTerm); } - - + filterClients(searchTerm: string): void { if (!searchTerm) { this.selectedClients = [...this.selectedClientsOriginal]; return; } - + const lowerTerm = searchTerm.toLowerCase(); - + this.selectedClients = this.selectedClientsOriginal.filter(client => { const matchesName = client.name.toLowerCase().includes(lowerTerm); const matchesIP = client.ip?.toLowerCase().includes(lowerTerm) || false; const matchesStatus = client.status?.toLowerCase().includes(lowerTerm) || false; const matchesMac = client.mac?.toLowerCase().includes(lowerTerm) || false; - + return matchesName || matchesIP || matchesStatus || matchesMac; }); } - - - + + getStatus(client: any): void { + this.syncingClientId = client.uuid; + this.syncStatus = true; + + this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe( + response => { + this.toastr.success('Cliente actualizado correctamente'); + this.search(); + this.syncStatus = false; + this.syncingClientId = null; + }, + error => { + this.toastr.error('Error de conexión con el cliente'); + this.syncStatus = false; + this.syncingClientId = null; + } + ); + } } From aecc16c33261dbb565dda020f6eb619c0fc2e470 Mon Sep 17 00:00:00 2001 From: apuente Date: Wed, 4 Dec 2024 15:27:43 +0100 Subject: [PATCH 11/17] Refactor groups component to update filter option value --- ogWebconsole/src/app/app.module.ts | 3 +- .../components/groups/groups.component.html | 2 +- .../app/components/groups/groups.component.ts | 633 +++++++++--------- .../src/app/components/groups/model/model.ts | 45 +- 4 files changed, 363 insertions(+), 320 deletions(-) diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index 9ec2f42..1245eeb 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -123,7 +123,7 @@ import { JoyrideModule } from 'ngx-joyride'; 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'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './locale/', '.json'); } @@ -234,6 +234,7 @@ export function HttpLoaderFactory(http: HttpClient) { MatDatepickerModule, MatNativeDateModule, MatSliderModule, + MatSortModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 0c1b9fc..85c1ad6 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -40,7 +40,7 @@ Filtrar por tipo Todos - Grupos de aulas + Grupos de aulas Aulas Grupos de ordenadores diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 0d64752..96cf096 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; @@ -8,8 +8,10 @@ import { ToastrService } from 'ngx-toastr'; import { JoyrideService } from 'ngx-joyride'; import { FlatTreeControl } from '@angular/cdk/tree'; import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; +import { Subscription } from 'rxjs'; + import { DataService } from './services/data.service'; -import { UnidadOrganizativa } from './model/model'; +import { UnidadOrganizativa, Client, TreeNode, FlatNode, Command, Filter } from './model/model'; import { CreateOrganizationalUnitComponent } from './shared/organizational-units/create-organizational-unit/create-organizational-unit.component'; import { CreateClientComponent } from './shared/clients/create-client/create-client.component'; import { EditOrganizationalUnitComponent } from './shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component'; @@ -22,86 +24,67 @@ import { OrganizationalUnitTabViewComponent } from './components/organizational- import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component'; import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal'; -interface TreeNode { - clients?: any[]; - name: string; - type: string; - children?: TreeNode[]; - ip?: string; - '@id'?: string; - hasClients?: boolean; - status?: string; -} - -interface FlatNode { - name: string; - type: string; - level: number; - expandable: boolean; - ip?: string; - hasClients?: boolean; +enum NodeType { + OrganizationalUnit = 'organizational-unit', + ClassroomsGroup = 'classrooms-group', + Classroom = 'classroom', + ClientsGroup = 'clients-group', + Client = 'client', } @Component({ selector: 'app-groups', templateUrl: './groups.component.html', - styleUrls: ['./groups.component.css'] + styleUrls: ['./groups.component.css'], }) -export class GroupsComponent implements OnInit { +export class GroupsComponent implements OnInit, OnDestroy { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; organizationalUnits: UnidadOrganizativa[] = []; selectedUnidad: UnidadOrganizativa | null = null; - selectedDetail: any | null = null; - loading: boolean = false; - loadingChildren: boolean = false; - searchTerm: string = ''; + selectedDetail: UnidadOrganizativa | null = null; + loading = false; + searchTerm = ''; treeControl: FlatTreeControl; treeFlattener: MatTreeFlattener; treeDataSource: MatTreeFlatDataSource; selectedNode: TreeNode | null = null; - commands: any[] = []; - commandsLoading: boolean = false; - selectedClients: any[] = []; - cols: number = 4; - selectedClientsOriginal: any[] = []; + commands: Command[] = []; + commandsLoading = false; + selectedClients: Client[] = []; + cols = 4; + selectedClientsOriginal: Client[] = []; currentView: 'card' | 'list' = 'list'; - isTreeViewActive: boolean = false; - savedFilterNames: any[] = []; - selectedTreeFilter: string = ''; - - syncStatus: boolean = false; - syncingClientId: number | null = null; + isTreeViewActive = false; + savedFilterNames: [string, string][] = []; + selectedTreeFilter = ''; + syncStatus = false; + syncingClientId: string | null = null; + private originalTreeData: TreeNode[] = []; @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; + private subscriptions: Subscription = new Subscription(); + constructor( private http: HttpClient, private router: Router, private dataService: DataService, public dialog: MatDialog, - private _bottomSheet: MatBottomSheet, + private bottomSheet: MatBottomSheet, private joyrideService: JoyrideService, private toastr: ToastrService ) { this.treeFlattener = new MatTreeFlattener( - (node: TreeNode, level: number) => ({ - name: node.name, - type: node.type, - level, - expandable: !!node.children?.length, - hasClients: node.hasClients, - ip: node.ip, - ['@id']: node['@id'] - }), - node => node.level, - node => node.expandable, - node => node.children + this.transformer, + (node) => node.level, + (node) => node.expandable, + (node) => node.children ); this.treeControl = new FlatTreeControl( - node => node.level, - node => node.expandable + (node) => node.level, + (node) => node.expandable ); this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); @@ -111,17 +94,32 @@ export class GroupsComponent implements OnInit { this.search(); this.getFilters(); this.updateGridCols(); - window.addEventListener('resize', () => this.updateGridCols()); + window.addEventListener('resize', this.updateGridCols); } - toggleView(view: 'card' | 'list') { + ngOnDestroy(): void { + window.removeEventListener('resize', this.updateGridCols); + this.subscriptions.unsubscribe(); + } + + private transformer = (node: TreeNode, level: number): FlatNode => ({ + name: node.name, + type: node.type, + level, + expandable: !!node.children?.length, + hasClients: node.hasClients, + ip: node.ip, + '@id': node['@id'], + }); + + toggleView(view: 'card' | 'list'): void { this.currentView = view; } - updateGridCols(): void { + updateGridCols = (): void => { const width = window.innerWidth; this.cols = width <= 600 ? 1 : width <= 960 ? 2 : width <= 1280 ? 3 : 4; - } + }; clearSelection(): void { this.selectedUnidad = null; @@ -139,112 +137,122 @@ export class GroupsComponent implements OnInit { } getFilters(): void { - this.dataService.getFilters().subscribe( - data => { - this.savedFilterNames = data.map((filter: any) => [filter.name, filter.uuid]); - }, - error => { - console.error('Error fetching filters:', error); - } + this.subscriptions.add( + this.dataService.getFilters().subscribe( + (data) => { + this.savedFilterNames = data.map((filter: { name: string; uuid: string; }) => [filter.name, filter.uuid]); + }, + (error) => { + console.error('Error fetching filters:', error); + } + ) ); } - loadSelectedFilter(savedFilter: any) { - const url = `${this.baseUrl}/views/` + savedFilter[1]; - this.dataService.getFilter(savedFilter[1]).subscribe(response => { - if (response) { - console.log('Filter1:', response.filters); - } - }, error => { - console.error('Error:', error); - }); + loadSelectedFilter(savedFilter: [string, string]): void { + this.subscriptions.add( + this.dataService.getFilter(savedFilter[1]).subscribe( + (response) => { + if (response) { + console.log('Filter:', response.filters); + } + }, + (error) => { + console.error('Error:', error); + } + ) + ); } search(): void { this.loading = true; - this.dataService.getOrganizationalUnits(this.searchTerm).subscribe( - data => { - this.organizationalUnits = data; - this.loading = false; - }, - error => { - console.error('Error fetching unidades organizativas', error); - this.loading = false; - } + this.subscriptions.add( + this.dataService.getOrganizationalUnits(this.searchTerm).subscribe( + (data) => { + this.organizationalUnits = data; + this.loading = false; + }, + (error) => { + console.error('Error fetching organizational units', error); + this.loading = false; + } + ) ); } - onSelectUnidad(unidad: any): void { + onSelectUnidad(unidad: UnidadOrganizativa): void { this.selectedUnidad = unidad; this.selectedDetail = unidad; this.selectedClients = this.collectAllClients(unidad); this.selectedClientsOriginal = [...this.selectedClients]; - this.loadChildrenAndClients(unidad.id).then(fullData => { + this.loadChildrenAndClients(unidad.id).then((fullData) => { const treeData = this.convertToTreeData(fullData); this.treeDataSource.data = treeData[0]?.children || []; }); this.isTreeViewActive = true; } - private collectAllClients(node: any): any[] { + private collectAllClients(node: UnidadOrganizativa): Client[] { let clients = node.clients || []; - if (node.children && node.children.length > 0) { - node.children.forEach((child: any) => { + if (node.children) { + node.children.forEach((child) => { clients = clients.concat(this.collectAllClients(child)); }); } return clients; } - async loadChildrenAndClients(id: string): Promise { + private async loadChildrenAndClients(id: string): Promise { try { const childrenData = await this.dataService.getChildren(id).toPromise(); - const processHierarchy = (nodes: UnidadOrganizativa[]): TreeNode[] => { - return nodes.map(node => ({ - name: node.name, - type: node.type, - '@id': node['@id'], + + const processHierarchy = (nodes: UnidadOrganizativa[]): UnidadOrganizativa[] => { + return nodes.map((node) => ({ + ...node, children: node.children ? processHierarchy(node.children) : [], - clients: node.clients || [] })); }; - + return { - ...this.selectedUnidad, - children: childrenData ? processHierarchy(childrenData) : [] + ...this.selectedUnidad!, + children: childrenData ? processHierarchy(childrenData) : [], }; } catch (error) { console.error('Error loading children:', error); - return this.selectedUnidad; + return this.selectedUnidad!; } } + - convertToTreeData(data: any): TreeNode[] { - const processNode = (node: UnidadOrganizativa): TreeNode => ({ - name: node.name, - type: node.type, - '@id': node['@id'], - children: node.children?.map(processNode) || [], - hasClients: node.clients && node.clients.length > 0, - }); - - return [processNode(data)]; - } + private convertToTreeData(data: UnidadOrganizativa): TreeNode[] { + const processNode = (node: UnidadOrganizativa): TreeNode => ({ + name: node.name, + type: node.type, + '@id': node['@id'], + children: node.children?.map(processNode) || [], + hasClients: (node.clients?.length ?? 0) > 0, + }); + return [processNode(data)]; +} + onNodeClick(node: TreeNode): void { this.selectedNode = node; - this.selectedClients = node.clients || []; - this.selectedClientsOriginal = [...this.selectedClients]; - if (node.hasClients) { - const url = `${this.baseUrl}${node['@id']}`; - this.http.get(url).subscribe( - (data: any) => { - this.selectedClientsOriginal = [...data.clients]; - this.selectedClients = data.clients || []; - }, - (error) => { - console.error('Error fetching clients:', error); - } + this.fetchClientsForNode(node); + } + + private fetchClientsForNode(node: TreeNode): void { + if (node.hasClients && node['@id']) { + this.subscriptions.add( + this.http.get<{ clients: Client[] }>(`${this.baseUrl}${node['@id']}`).subscribe( + (data) => { + this.selectedClientsOriginal = [...data.clients]; + this.selectedClients = data.clients || []; + }, + (error) => { + console.error('Error fetching clients:', error); + } + ) ); } else { this.selectedClients = []; @@ -252,222 +260,207 @@ export class GroupsComponent implements OnInit { } } - getNodeIcon(node: any): string { + getNodeIcon(node: TreeNode): string { switch (node.type) { - case 'organizational-unit': return 'apartment'; - case 'classrooms-group': return 'doors'; - case 'classroom': return 'school'; - case 'clients-group': return 'lan'; - case 'client': return 'computer'; - default: return 'group'; + case NodeType.OrganizationalUnit: + return 'apartment'; + case NodeType.ClassroomsGroup: + return 'doors'; + case NodeType.Classroom: + return 'school'; + case NodeType.ClientsGroup: + return 'lan'; + case NodeType.Client: + return 'computer'; + default: + return 'group'; } } - addOU(event: MouseEvent, parent: any = null): void { + addOU(event: MouseEvent, parent: TreeNode | null = null): void { event.stopPropagation(); - const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px' }); + const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { + data: { parent }, + width: '900px', + }); dialogRef.afterClosed().subscribe(() => { - this.dataService.getOrganizationalUnits().subscribe( - data => { - this.organizationalUnits = data; - this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { - const treeData = this.convertToTreeData(updatedData); - this.treeDataSource.data = treeData[0]?.children || []; - }); - }, - error => console.error('Error fetching unidades organizativas', error) - ); + this.refreshOrganizationalUnits(); }); } - addClient(event: MouseEvent, organizationalUnit: any = null): void { + addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void { event.stopPropagation(); - const dialogRef = this.dialog.open(CreateClientComponent, { data: { organizationalUnit }, width: '900px' }); + const dialogRef = this.dialog.open(CreateClientComponent, { + data: { organizationalUnit }, + width: '900px', + }); dialogRef.afterClosed().subscribe(() => { + this.refreshOrganizationalUnits(); + if (organizationalUnit && organizationalUnit['@id']) { + this.refreshClientsForNode(organizationalUnit); + } + }); + } + + private refreshOrganizationalUnits(): void { + this.subscriptions.add( this.dataService.getOrganizationalUnits().subscribe( - data => { + (data) => { this.organizationalUnits = data; - this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { - const treeData = this.convertToTreeData(updatedData); - this.treeDataSource.data = treeData[0]?.children || []; - }); - if (organizationalUnit && organizationalUnit.id) { - this.loadChildrenAndClients(organizationalUnit.id); - this.refreshClients(organizationalUnit); - } - }, - error => console.error('Error fetching unidades organizativas', error) - ); - }); - } - - setSelectedNode(node: TreeNode): void { - this.selectedNode = node; - } - - onEditNode(event: MouseEvent, node: TreeNode | null): void { - if (!node) return; - - const uuid = node['@id'] ? node['@id'].split('/').pop() : ''; - const type = node.type; - - event.stopPropagation(); - - if (type !== 'client') { - this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); - } else { - this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); - } - } - - onDelete(node: TreeNode | null): void { - console.log('Deleting node:', node); - } - - onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void { - const uuid = node && node['@id'] ? node['@id'].split('/').pop() || '' : ''; - const name = node?.name || 'Elemento desconocido'; - const type = node?.type || ''; - - event.stopPropagation(); - - const dialogRef = this.dialog.open(DeleteModalComponent, { - width: '400px', - data: { name } - }); - - dialogRef.afterClosed().subscribe(result => { - if (result === true) { - this.dataService.deleteElement(uuid, type).subscribe( - () => { - this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { + if (this.selectedUnidad) { + this.loadChildrenAndClients(this.selectedUnidad?.id || '').then((updatedData) => { + this.selectedUnidad = updatedData; const treeData = this.convertToTreeData(updatedData); - this.treeDataSource.data = treeData[0]?.children || []; - }); - - if (type === 'client' && clientNode) { - this.refreshClients(clientNode); - } - - this.dataService.getOrganizationalUnits().subscribe( - data => { - this.organizationalUnits = data; - }, - error => console.error('Error fetching unidades organizativas', error) - ); - - this.toastr.success('Entidad eliminada exitosamente'); - }, - error => { - console.error('Error deleting entity:', error); - this.toastr.error('Error al eliminar la entidad', error.message); + this.originalTreeData = treeData[0]?.children || []; + this.treeDataSource.data = [...this.originalTreeData]; + }); } - ); - } - }); - } - - private refreshClients(node: TreeNode): void { - if (!node || !node['@id']) { - this.selectedClients = []; - return; - } - - const url = `${this.baseUrl}${node['@id']}`; - this.http.get(url).subscribe( - (data: any) => { - if (data && Array.isArray(data.clients)) { - this.selectedClients = data.clients; - } else { - this.selectedClients = []; - } - }, - error => { - console.error('Error refreshing clients:', error); - const errorMessage = error.status === 404 - ? 'No se encontraron clientes para este nodo.' - : 'Error al comunicarse con el servidor.'; - this.toastr.error(errorMessage); - } + }, + (error) => console.error('Error fetching organizational units', error) + ) ); } - onEditClick(event: MouseEvent, type: any, uuid: string): void { + onEditNode(event: MouseEvent, node: TreeNode | null): void { event.stopPropagation(); - if (type != 'client') { + const uuid = node ? this.extractUuid(node['@id']) : null; + if (!uuid) return; + + if (node && node.type !== NodeType.Client) { this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); } else { this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); } } - onRoomMap(room: any): void { - this.http.get(`${this.baseUrl}`+ room['@id']).subscribe( - (response: any) => { - this.dialog.open(ClassroomViewDialogComponent, { - width: '90vw', - data: { clients: response.clients } - }); - }, - (error: any) => { - console.error('Error en la solicitud HTTP:', error); + onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void{ + event.stopPropagation(); + const uuid = node ? this.extractUuid(node['@id']) : null; + if (!uuid) return; + + if (!node) return; + const dialogRef = this.dialog.open(DeleteModalComponent, { + width: '400px', + data: { name: node.name }, + }); + + dialogRef.afterClosed().subscribe((result) => { + if (result === true) { + this.deleteEntity(uuid, node.type, node); } + }); + } + + private deleteEntity(uuid: string, type: string, node: TreeNode): void { + this.subscriptions.add( + this.dataService.deleteElement(uuid, type).subscribe( + () => { + this.refreshOrganizationalUnits(); + if (type === NodeType.Client) { + this.refreshClientsForNode(node); + } + this.toastr.success('Entidad eliminada exitosamente'); + }, + (error) => { + console.error('Error deleting entity:', error); + this.toastr.error('Error al eliminar la entidad', error.message); + } + ) + ); + } + + private refreshClientsForNode(node: TreeNode): void { + if (!node['@id']) { + this.selectedClients = []; + return; + } + this.fetchClientsForNode(node); + } + + onEditClick(event: MouseEvent, type: string, uuid: string): void { + event.stopPropagation(); + if (type !== NodeType.Client) { + this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); + } else { + this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); + } + } + + onRoomMap(room: TreeNode | null): void { + if (!room || !room['@id']) return; + this.subscriptions.add( + this.http.get<{ clients: Client[] }>(`${this.baseUrl}${room['@id']}`).subscribe( + (response) => { + this.dialog.open(ClassroomViewDialogComponent, { + width: '90vw', + data: { clients: response.clients }, + }); + }, + (error) => { + console.error('Error fetching room data:', error); + } + ) ); } fetchCommands(): void { this.commandsLoading = true; - this.http.get(`${this.baseUrl}`+'/commands?page=1&itemsPerPage=30').subscribe( - (response: any) => { - this.commands = response['hydra:member']; - this.commandsLoading = false; - }, - (error) => { - console.error('Error fetching commands:', error); - this.commandsLoading = false; - } + this.subscriptions.add( + this.http.get<{ 'hydra:member': Command[] }>(`${this.baseUrl}/commands?page=1&itemsPerPage=30`).subscribe( + (response) => { + this.commands = response['hydra:member']; + this.commandsLoading = false; + }, + (error) => { + console.error('Error fetching commands:', error); + this.commandsLoading = false; + } + ) ); } - executeCommand(command: any, selectedNode: any): void { - this.toastr.success('Ejecutando comando: ' + command.name + " en " + selectedNode.name); + executeCommand(command: Command, selectedNode: TreeNode | null): void { + + if (!selectedNode) { + this.toastr.error('No hay un nodo seleccionado.'); + return; + } else { + this.toastr.success(`Ejecutando comando: ${command.name} en ${selectedNode.name}`); + } } - onClientActions(client: any): void { - console.log('Client actions:', client); - } - - onShowClientDetail(event: MouseEvent, client: any): void { + onShowClientDetail(event: MouseEvent, client: Client): void { event.stopPropagation(); this.router.navigate(['clients', client.uuid], { state: { clientData: client } }); } - onShowDetailsClick(event: MouseEvent, data: any): void { + onShowDetailsClick(event: MouseEvent, data: TreeNode | null): void { event.stopPropagation(); - if (data.type != 'client') { + if (data && data.type !== NodeType.Client) { this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px' }); - } - if (data.type == 'client') { - this.router.navigate(['clients', data['@id'].split('/').pop()], { state: { clientData: data } }); + } else { + if (data) { + this.router.navigate(['clients', this.extractUuid(data['@id'])], { state: { clientData: data } }); + } } } - onTreeClick(event: MouseEvent, data: any): void { + onTreeClick(event: MouseEvent, data: TreeNode): void { event.stopPropagation(); - if (data.type != 'client') { + if (data.type !== NodeType.Client) { this.dialog.open(TreeViewComponent, { data: { data }, width: '800px' }); } } openBottomSheet(): void { - this._bottomSheet.open(LegendComponent); + this.bottomSheet.open(LegendComponent); } iniciarTour(): void { this.joyrideService.startTour({ steps: ['groupsTitleStepText', 'addStep', 'keyStep', 'unitStep', 'elementsStep', 'tabsStep'], showPrevButton: true, - themeColor: '#3f51b5' + themeColor: '#3f51b5', }); } @@ -475,23 +468,25 @@ export class GroupsComponent implements OnInit { isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable; filterTree(searchTerm: string, filterType: string): void { - const filterNodes = (nodes: any[]): any[] => { - return nodes - .map(node => { - const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase()); - const matchesType = filterType ? node.type.toLowerCase() === filterType.toLowerCase() : true; - const filteredChildren = node.children ? filterNodes(node.children) : []; - if (matchesName && matchesType || filteredChildren.length > 0) { - return { ...node, children: filteredChildren }; - } - return null; - }) - .filter(node => node !== null); + const filterNodes = (nodes: TreeNode[]): TreeNode[] => { + const filteredNodes: TreeNode[] = []; + for (const node of nodes) { + const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesType = filterType ? node.type.toLowerCase() === filterType.toLowerCase() : true; + const filteredChildren = node.children ? filterNodes(node.children) : []; + + if ((matchesName && matchesType) || filteredChildren.length > 0) { + filteredNodes.push({ ...node, children: filteredChildren }); + } + } + return filteredNodes; }; - - const filteredData = filterNodes(this.treeDataSource.data); + + const filteredData = filterNodes(this.originalTreeData); this.treeDataSource.data = filteredData; } + + onTreeFilterInput(event: Event): void { const input = event.target as HTMLInputElement; @@ -513,32 +508,44 @@ export class GroupsComponent implements OnInit { const lowerTerm = searchTerm.toLowerCase(); - this.selectedClients = this.selectedClientsOriginal.filter(client => { - const matchesName = client.name.toLowerCase().includes(lowerTerm); - const matchesIP = client.ip?.toLowerCase().includes(lowerTerm) || false; - const matchesStatus = client.status?.toLowerCase().includes(lowerTerm) || false; - const matchesMac = client.mac?.toLowerCase().includes(lowerTerm) || false; - - return matchesName || matchesIP || matchesStatus || matchesMac; + this.selectedClients = this.selectedClientsOriginal.filter((client) => { + return ( + client.name.toLowerCase().includes(lowerTerm) || + client.ip?.toLowerCase().includes(lowerTerm) || + client.status?.toLowerCase().includes(lowerTerm) || + client.mac?.toLowerCase().includes(lowerTerm) + ); }); } - getStatus(client: any): void { + public setSelectedNode(node: TreeNode): void { + this.selectedNode = node; + } + + getStatus(client: Client): void { + if (!client.uuid || !client['@id']) return; + this.syncingClientId = client.uuid; this.syncStatus = true; - this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe( - response => { - this.toastr.success('Cliente actualizado correctamente'); - this.search(); - this.syncStatus = false; - this.syncingClientId = null; - }, - error => { - this.toastr.error('Error de conexión con el cliente'); - this.syncStatus = false; - this.syncingClientId = null; - } + this.subscriptions.add( + this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe( + () => { + this.toastr.success('Cliente actualizado correctamente'); + this.search(); + this.syncStatus = false; + this.syncingClientId = null; + }, + () => { + this.toastr.error('Error de conexión con el cliente'); + this.syncStatus = false; + this.syncingClientId = null; + } + ) ); } + + private extractUuid(idPath: string | undefined): string | null { + return idPath ? idPath.split('/').pop() || null : null; + } } diff --git a/ogWebconsole/src/app/components/groups/model/model.ts b/ogWebconsole/src/app/components/groups/model/model.ts index 289a796..7789157 100644 --- a/ogWebconsole/src/app/components/groups/model/model.ts +++ b/ogWebconsole/src/app/components/groups/model/model.ts @@ -9,14 +9,14 @@ export interface Aula { } export interface UnidadOrganizativa { - clients: any[]; - children: UnidadOrganizativa[]; - '@id'?: string; id: string; - name: string; uuid: string; + name: string; type: string; - parent: UnidadOrganizativa[]; + '@id': string; + clients?: Client[]; + children?: UnidadOrganizativa[]; + parent?: UnidadOrganizativa; } export interface OrganizationalUnit { @@ -29,6 +29,9 @@ export interface OrganizationalUnit { } export interface Client { + mac: any; + status: any; + ip: any; "@id": string; "@type": string; id: number; @@ -54,3 +57,35 @@ export interface ClientCollection { "@type": string; }; } + +export interface TreeNode { + name: string; + type: string; + '@id'?: string; + children?: TreeNode[]; + hasClients?: boolean; + clients?: Client[]; + ip?: string; +} + +export interface FlatNode { + name: string; + type: string; + level: number; + expandable: boolean; + hasClients?: boolean; + ip?: string; + '@id'?: string; +} + + +export interface Command { + name: string; + description?: string; +} + +export interface Filter { + name: string; + uuid: string; +} + From e3cf6a85d0edd7ebbcb4a9e0d7aabec21891781b Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 5 Dec 2024 12:50:15 +0100 Subject: [PATCH 12/17] Added router in subnet. Advanced bootfile changes, and partition assistant updates --- .../task-logs/task-logs.component.css | 4 +- .../client-main-view.component.css | 3 +- .../client-main-view.component.ts | 33 ++++---- .../deploy-image/deploy-image.component.html | 2 +- .../deploy-image/deploy-image.component.ts | 21 ++++- .../partition-assistant.component.ts | 2 +- .../components/groups/groups.component.html | 6 +- .../groups/services/data.service.ts | 42 +++++++++- .../create-organizational-unit.component.html | 16 ++++ .../create-organizational-unit.component.ts | 31 ++++++++ .../edit-organizational-unit.component.html | 20 ++++- .../edit-organizational-unit.component.ts | 46 ++++++++--- .../show-organizational-unit.component.ts | 62 ++++++++++----- .../components/images/images.component.css | 18 +++-- .../components/images/images.component.html | 9 ++- .../pxe-boot-files.component.html | 44 +++++------ .../pxe-boot-files.component.ts | 78 +++++-------------- .../create-subnet.component.html | 6 +- .../create-subnet/create-subnet.component.ts | 9 ++- .../og-dhcp-subnets.component.ts | 1 + ogWebconsole/src/locale/en.json | 1 + ogWebconsole/src/locale/es.json | 1 + 22 files changed, 296 insertions(+), 159 deletions(-) diff --git a/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.css b/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.css index 64e2e55..53254bd 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.css +++ b/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.css @@ -71,12 +71,12 @@ table { } .chip-failed { - background-color: #f15d5d !important; + background-color: #e87979 !important; color: white; } .chip-success { - background-color: #32c532 !important; + background-color: #46c446 !important; color: white; } diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css index c1c12f0..533ab69 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.css @@ -247,9 +247,8 @@ } .charts-container { - flex: 2; + flex: 1; display: flex; - flex-direction: column; align-items: center; gap: 10px; } 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 6e9728b..95a3563 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 @@ -34,7 +34,7 @@ export class ClientMainViewComponent implements OnInit { partitions: any[] = []; commands: any[] = []; chartDisk: any[] = []; - view: [number, number] = [600, 300]; + view: [number, number] = [300, 200]; showLegend: boolean = true; arrayCommands: any[] = [ @@ -100,15 +100,28 @@ export class ClientMainViewComponent implements OnInit { } ngOnInit() { - this.clientData = history.state.clientData; - this.loadPartitions() - this.updateGeneralData(); - this.updateNetworkData(); + this.clientData = history.state.clientData['@id']; + this.loadClient(this.clientData) this.loadCommands() this.calculateDiskUsage(); this.loading = false; } + + loadClient = (uuid: string) => { + this.http.get(`${this.baseUrl}${uuid}`).subscribe({ + next: data => { + this.clientData = data; + this.updateGeneralData(); + this.updateNetworkData(); + this.loadPartitions() + this.loading = false; + }, + error: error => { + console.error('Error al obtener el cliente:', error); + } + }); + } updateGeneralData() { this.generalData = [ { property: 'Nombre', value: this.clientData?.name || '' }, @@ -184,15 +197,6 @@ export class ClientMainViewComponent implements OnInit { dialogRef.afterClosed().subscribe(); } - getStrokeOffset(partitions: any[], index: number): number { - const totalSize = partitions.reduce((acc, part) => acc + (part.size / 1024), 0); - - const offset = partitions.slice(0, index).reduce((acc, part) => acc + (part.size / 1024), 0); - console.log(offset, totalSize) - - return totalSize > 0 ? (offset / totalSize) : 0; - } - loadPartitions(): void { this.http.get(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[partitionNumber]=ASC`).subscribe({ next: data => { @@ -218,7 +222,6 @@ export class ClientMainViewComponent implements OnInit { } onCommandSelect(action: any): void { - console.log(action); if (action === 'partition') { this.openPartitionAssistant(); } diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html index e514d0b..eb9611c 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html @@ -25,7 +25,7 @@ Seleccione método de deploy - {{ method }} + {{ method }} diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts index 2c48a74..2e9e25b 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts @@ -20,7 +20,7 @@ export class DeployImageComponent { images: any[] = []; clientName: string = ''; selectedImage: string | null = null; - selectedOption: string | null = null; + selectedOption: string | null = 'deploy-image'; selectedMethod: string | null = null; selectedPartition: any = null; mcastIp: string = ''; @@ -98,7 +98,7 @@ export class DeployImageComponent { ngOnInit() { this.clientId = this.route.snapshot.paramMap.get('id'); - + this.selectedOption = 'deploy-image'; this.loadPartitions(); this.loadImages(); } @@ -147,6 +147,21 @@ export class DeployImageComponent { } save(): void { + if (!this.selectedImage) { + this.toastService.error('Debe seleccionar una imagen'); + return; + } + + if (!this.selectedMethod) { + this.toastService.error('Debe seleccionar un método'); + return; + } + + if (!this.selectedPartition) { + this.toastService.error('Debe seleccionar una partición'); + return; + } + const payload = { client: `/clients/${this.clientId}`, method: this.selectedMethod, @@ -163,7 +178,7 @@ export class DeployImageComponent { .subscribe({ next: (response) => { this.toastService.success('Imagen creada exitosamente'); - this.router.navigate(['/images']); + this.router.navigate(['/commmands-logs']) }, error: (error) => { console.error('Error:', error); diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts index cab33a8..752a376 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts @@ -289,7 +289,7 @@ export class PartitionAssistantComponent implements OnInit { this.http.post(this.apiUrl, bulkPayload).subscribe( (response) => { this.toastService.success('Particiones creadas exitosamente'); - this.router.navigate(['/traces']); + this.router.navigate(['/commands-logs']); }, (error) => { console.error('Error al crear las particiones:', error); diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 85c1ad6..2dc00e3 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -218,7 +218,7 @@ sync Sincronizar - + - + - -
+
+
diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.ts b/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.ts index 1bc65e3..7f293ef 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.ts @@ -1,8 +1,8 @@ -import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { CreateOrganizationalUnitComponent } from '../create-organizational-unit/create-organizational-unit.component'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; +import {Component, EventEmitter, Inject, OnInit, Output} from '@angular/core'; +import {FormBuilder, FormGroup} from '@angular/forms'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {CreateOrganizationalUnitComponent} from '../create-organizational-unit/create-organizational-unit.component'; import {DataService} from "../../../services/data.service"; import {ToastrService} from "ngx-toastr"; @@ -23,6 +23,8 @@ export class EditOrganizationalUnitComponent implements OnInit { hardwareProfiles: any[] = []; isEditMode: boolean; currentCalendar: any = []; + ogLives: any[] = []; + repositories: any[] = []; protected p2pModeOptions = [ {"name": 'Leecher', "value": "p2p-mode-leecher"}, {"name": 'Peer', "value": "p2p-mode-peer"}, @@ -57,6 +59,8 @@ export class EditOrganizationalUnitComponent implements OnInit { }); this.networkSettingsFormGroup = this._formBuilder.group({ + ogLive: [null], + repository: [null], proxy: [null], dns: [null], netmask: [null], @@ -90,6 +94,8 @@ export class EditOrganizationalUnitComponent implements OnInit { this.loadParentUnits(); this.loadHardwareProfiles(); this.loadCalendars(); + this.loadOgLives(); + this.loadRepositories(); } loadParentUnits() { @@ -116,6 +122,20 @@ export class EditOrganizationalUnitComponent implements OnInit { ); } + loadOgLives() { + this.dataService.getOgLives().subscribe( + (data: any[]) => this.ogLives = data, + error => console.error('Error fetching ogLives', error) + ); + } + + loadRepositories() { + this.dataService.getRepositories().subscribe( + (data: any[]) => this.repositories = data, + error => console.error('Error fetching repositories', error) + ); + } + loadCalendars() { const apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=30`; this.http.get(apiUrl).subscribe( @@ -138,9 +158,15 @@ export class EditOrganizationalUnitComponent implements OnInit { );} onCalendarChange(event: any) { - const selectedCalendarId = event.value; - console.log('Selected calendar ID:', selectedCalendarId); - this.generalFormGroup.value.remoteCalendar = selectedCalendarId; + this.generalFormGroup.value.remoteCalendar = event.value; + } + + onOgLiveChange(event: any) { + this.networkSettingsFormGroup.value.ogLive = event.value; + } + + onRepositoryChange(event: any) { + this.networkSettingsFormGroup.value.repository = event.value } loadData(uuid: string) { @@ -171,6 +197,8 @@ export class EditOrganizationalUnitComponent implements OnInit { mcastMode: data.networkSettings.mcastMode, menu: data.networkSettings.menu ? data.networkSettings.menu['@id'] : null, hardwareProfile: data.networkSettings.hardwareProfile ? data.networkSettings.hardwareProfile['@id'] : null, + ogLive: data.networkSettings.ogLive ? data.networkSettings.ogLive['@id'] : null, + repository: data.networkSettings.repository ? data.networkSettings.repository['@id'] : null, validation: data.networkSettings.validation }); this.classroomInfoFormGroup.patchValue({ @@ -245,5 +273,5 @@ export class EditOrganizationalUnitComponent implements OnInit { this.dialogRef.close(); } - + } diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component.ts b/ogWebconsole/src/app/components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component.ts index a62e1ee..14be025 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component.ts @@ -11,9 +11,9 @@ export class ShowOrganizationalUnitComponent { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; displayedColumns: string[] = ['property', 'value']; currentCalendar: any; + ou: any; generalData: any[] = []; - networkData = [ - ]; + networkData: any[] = []; constructor( @Inject(MAT_DIALOG_DATA) public data: any, @@ -22,11 +22,22 @@ export class ShowOrganizationalUnitComponent { } ngOnInit(): void { - if (this.data.data.remoteCalendar) { - this.loadCurrentCalendar(this.data.data.remoteCalendar.uuid); - } else { - this.initializeGeneralData(); - } + this.loadOrganizationalUnit(this.data.data['@id']); + } + + loadOrganizationalUnit(uuid: string): void { + console.log(this.data['@id']) + const apiUrl = `${this.baseUrl}${uuid}`; + this.http.get(apiUrl).subscribe( + response => { + this.ou = response; + console.log('Organizational unit', this.ou); + this.initializeData(); + }, + error => { + console.error('Error loading organizational unit', error); + this.initializeData(); + }); } loadCurrentCalendar(uuid: string): void { @@ -34,26 +45,39 @@ export class ShowOrganizationalUnitComponent { this.http.get(apiUrl).subscribe( response => { this.currentCalendar = response.name; - this.initializeGeneralData(); + this.initializeData(); }, error => { console.error('Error loading current calendar', error); - this.initializeGeneralData(); + this.initializeData(); } ); } - initializeGeneralData(): void { + initializeData(): void { this.generalData = [ - { property: 'Nombre', value: this.data.data.name }, - { property: 'Uuid', value: this.data.data.uuid }, - { property: 'Descripción', value: this.data.data.description }, - { property: 'Comentarios', value: this.data.data.comments }, - { property: 'Tipo', value: this.data.data.type }, - { property: 'Unidad organizativa superior', value: this.data.data.parent ? this.data.data.parent.name : '-' }, - { property: 'Creado por', value: this.data.data.createdBy }, - { property: 'Creado el', value: this.data.data.createdAt }, - { property: 'Calendario asociado', value: this.data.data.remoteCalendar ? this.currentCalendar : '-' } + { property: 'Nombre', value: this.ou.name }, + { property: 'Uuid', value: this.ou.uuid }, + { property: 'Descripción', value: this.ou.description }, + { property: 'Comentarios', value: this.ou.comments }, + { property: 'Tipo', value: this.ou.type }, + { property: 'Unidad organizativa superior', value: this.ou.parent ? this.ou.parent.name : '-' }, + { property: 'Creado el', value: this.ou.createdAt }, ]; + + this.networkData = [ + { property: 'Calendario asociado', value: this.ou.remoteCalendar ? this.currentCalendar : '-' }, + { property: 'Aforo', value: this.ou.capacity }, + { property: 'Localización', value: this.ou.location }, + { property: 'Calendario', value: this.ou.calendar ? this.ou.calendar.name : '-' }, + { property: 'Proyector', value: this.ou.projector }, + { property: 'Pizarra', value: this.ou.board }, + { property: 'OgLive', value: this.ou.networkSettings.ogLive ? this.ou.networkSettings.ogLive.name : '-' }, + { property: 'Repositorio', value: this.ou.networkSettings.repository ? this.ou.networkSettings.repository.name : '-' }, + { property: 'OgLog', value: this.ou.networkSettings.oglog }, + { property: 'OgShare', value: this.ou.networkSettings.ogshare }, + { property: 'Perfil de hardware', value: this.ou.networkSettings.hardwareProfile ? this.ou.networkSettings.hardwareProfile.name : '-' }, + { property: 'Máscara de red', value: this.ou.networkSettings.netmask }, + ] } } diff --git a/ogWebconsole/src/app/components/images/images.component.css b/ogWebconsole/src/app/components/images/images.component.css index 6c78292..50f0f3a 100644 --- a/ogWebconsole/src/app/components/images/images.component.css +++ b/ogWebconsole/src/app/components/images/images.component.css @@ -71,19 +71,23 @@ table { margin: 8px 8px 8px 0; } -.status-success { - background-color: #4caf50; /* Verde */ +.chip-failed { + background-color: #e87979 !important; color: white; } -.status-failed { - background-color: #f44336; /* Rojo */ +.chip-success { + background-color: #46c446 !important; color: white; } -.status-pending { - background-color: #ff9800; /* Naranja */ +.chip-pending { + background-color: lightgrey !important; + color: black; +} + +.chip-in-progress { + background-color: #f5a623 !important; color: white; } - diff --git a/ogWebconsole/src/app/components/images/images.component.html b/ogWebconsole/src/app/components/images/images.component.html index 071d165..235fdb9 100644 --- a/ogWebconsole/src/app/components/images/images.component.html +++ b/ogWebconsole/src/app/components/images/images.component.html @@ -32,7 +32,12 @@ - + {{ getStatusLabel(image[column.columnDef]) }} @@ -54,7 +59,7 @@ menu - + diff --git a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html index c9ae5c5..19cc3ac 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html +++ b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html @@ -8,30 +8,14 @@
- - {{ 'selectUnitLabel' | translate }} - - {{ 'loadingUnitsOption' | translate }} - - {{ unit.name }} - - - - {{ 'requiredFieldError' | translate }} - - - - - {{ 'selectClassLabel' | translate }} - - - {{ 'noClassesOption' | translate }} - - - {{ child.name }} - - - +
+ + {{ 'selectClassLabel' | translate }} + + {{ unit.name }} + + +
@@ -49,7 +33,7 @@ - + diff --git a/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/create-subnet/create-subnet.component.ts b/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/create-subnet/create-subnet.component.ts index a0d5ef4..ada8414 100644 --- a/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/create-subnet/create-subnet.component.ts +++ b/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/create-subnet/create-subnet.component.ts @@ -17,6 +17,7 @@ export class CreateSubnetComponent implements OnInit { ipAddress: string = ''; nextServer: string = ''; bootFileName: string = ''; + router: string = ''; syncronized: boolean = false; serverId: number = 0; clients: any[] = []; @@ -44,6 +45,7 @@ export class CreateSubnetComponent implements OnInit { this.ipAddress = this.data.ipAddress; this.nextServer = this.data.nextServer; this.bootFileName = this.data.bootFileName; + this.router = this.data.router; this.syncronized = this.data.syncronized; this.serverId = this.data.serverId; this.clients = this.data.clients @@ -53,13 +55,14 @@ export class CreateSubnetComponent implements OnInit { this.dialogRef.close(); } - addNetworkConfig(): void { + save(): void { const payload = { name: this.name, netmask: this.netmask, ipAddress: this.ipAddress, - nextServer: this.nextServer || null, - bootFileName: this.bootFileName || null + router: this.router || null, + nextServer: this.nextServer || null, + bootFileName: this.bootFileName || null }; if (!this.data){ diff --git a/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component.ts b/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component.ts index dfec859..6305bda 100644 --- a/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component.ts +++ b/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component.ts @@ -18,6 +18,7 @@ export interface Subnet { netmask: string; ipAddress: string; nextServer: string; + router: string; bootFileName: string; synchronized: boolean; serverId: number; diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index 59fe4cd..fd2e26c 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -232,6 +232,7 @@ "editClientDialogTitle": "Edit Client", "organizationalUnitLabel": "Parent", "ogLiveLabel": "OgLive", + "repositoryLabel": "Repository", "serialNumberLabel": "Serial Number", "netifaceLabel": "Network interface", "netDriverLabel": "Network driver", diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index b13e8b4..e571e59 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -233,6 +233,7 @@ "editClientDialogTitle": "Editar Cliente", "organizationalUnitLabel": "Padre", "ogLiveLabel": "OgLive", + "repositoryLabel": "Repositorio", "serialNumberLabel": "Número de Serie", "netifaceLabel": "Interfaz de red", "netDriverLabel": "Controlador de red", From e9b4411ea71191c841ca680d3004a54520d4f294 Mon Sep 17 00:00:00 2001 From: apuente Date: Thu, 5 Dec 2024 13:09:22 +0100 Subject: [PATCH 13/17] Refactor groups component to update filter option value and add sync functionality --- .../components/groups/groups.component.html | 85 ++++++----- .../app/components/groups/groups.component.ts | 141 +++++++++++------- .../src/app/components/groups/model/model.ts | 1 + ogWebconsole/src/locale/en.json | 3 +- ogWebconsole/src/locale/es.json | 18 ++- 5 files changed, 156 insertions(+), 92 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 85c1ad6..f7aa9d2 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -22,33 +22,34 @@ - Filtros + {{ 'filters' | translate }}
- + {{ 'searchClient' | translate }} + + + + {{ savedFilter[0] }} - Buscar en el árbol - + {{ 'searchTree' | translate }} + - Filtrar por tipo - - Todos - Grupos de aulas - Aulas - Grupos de ordenadores + {{ 'filterByType' | translate }} + + {{ 'all' | translate }} + {{ 'classroomsGroup' | translate }} + {{ 'classrooms' | translate }} + {{ 'computerGroups' | translate }} - - Buscar cliente - - +
@@ -166,7 +167,7 @@ -
-

Clientes {{ selectedNode?.name ? 'del ' + selectedNode?.name : '' }}

+
+

{{ 'clients' | translate }} {{ selectedNode?.name ? ('del ' + selectedNode?.name) : '' }}

-
+
Client Icon
@@ -216,20 +217,20 @@
@@ -238,25 +239,25 @@
- +
- + - + - + - + - + + - + - + + + + + + + - + - - + +
Nombre Nombre {{ client.name }} IP IP {{ client.ip }} MAC MAC {{ client.mac }} OG Live OG Live {{ client.oglive }} Estado Estado - Mantenimiento Mantenimiento {{ client.mantenimiento }} Subred Subred {{ client.subnet }} Plantilla PXE Plantilla PXE {{ client.pxeTemplate }} Padre {{ client.parentName }} Acciones Acciones
diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 96cf096..8f57a06 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -9,9 +9,8 @@ import { JoyrideService } from 'ngx-joyride'; import { FlatTreeControl } from '@angular/cdk/tree'; import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; import { Subscription } from 'rxjs'; - import { DataService } from './services/data.service'; -import { UnidadOrganizativa, Client, TreeNode, FlatNode, Command, Filter } from './model/model'; +import { UnidadOrganizativa, Client, TreeNode, FlatNode, Command } from './model/model'; import { CreateOrganizationalUnitComponent } from './shared/organizational-units/create-organizational-unit/create-organizational-unit.component'; import { CreateClientComponent } from './shared/clients/create-client/create-client.component'; import { EditOrganizationalUnitComponent } from './shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component'; @@ -23,6 +22,9 @@ import { ClientTabViewComponent } from './components/client-tab-view/client-tab- import { OrganizationalUnitTabViewComponent } from './components/organizational-unit-tab-view/organizational-unit-tab-view.component'; import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component'; import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { MatPaginator } from '@angular/material/paginator'; enum NodeType { OrganizationalUnit = 'organizational-unit', @@ -50,7 +52,7 @@ export class GroupsComponent implements OnInit, OnDestroy { selectedNode: TreeNode | null = null; commands: Command[] = []; commandsLoading = false; - selectedClients: Client[] = []; + selectedClients = new MatTableDataSource([]); cols = 4; selectedClientsOriginal: Client[] = []; currentView: 'card' | 'list' = 'list'; @@ -61,6 +63,28 @@ export class GroupsComponent implements OnInit, OnDestroy { syncingClientId: string | null = null; private originalTreeData: TreeNode[] = []; + + + displayedColumns: string[] = ['name', 'ip', 'mac', 'oglive', 'status', 'maintenace', 'subnet', 'pxeTemplate', 'parentName', 'actions']; + + private _sort!: MatSort; + private _paginator!: MatPaginator; + + @ViewChild(MatSort) + set matSort(ms: MatSort) { + this._sort = ms; + if (this.selectedClients) { + this.selectedClients.sort = this._sort; + } + } + + @ViewChild(MatPaginator) + set matPaginator(mp: MatPaginator) { + this._paginator = mp; + if (this.selectedClients) { + this.selectedClients.paginator = this._paginator; + } + } @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; @@ -95,6 +119,16 @@ export class GroupsComponent implements OnInit, OnDestroy { this.getFilters(); this.updateGridCols(); window.addEventListener('resize', this.updateGridCols); + + this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => { + const lowerTerm = filter.toLowerCase(); + return ( + client.name.toLowerCase().includes(lowerTerm) || + client.ip?.toLowerCase().includes(lowerTerm) || + client.status?.toLowerCase().includes(lowerTerm) || + client.mac?.toLowerCase().includes(lowerTerm) + ); + }; } ngOnDestroy(): void { @@ -124,7 +158,7 @@ export class GroupsComponent implements OnInit, OnDestroy { clearSelection(): void { this.selectedUnidad = null; this.selectedDetail = null; - this.selectedClients = []; + this.selectedClients.data = []; this.isTreeViewActive = false; } @@ -183,36 +217,46 @@ export class GroupsComponent implements OnInit, OnDestroy { onSelectUnidad(unidad: UnidadOrganizativa): void { this.selectedUnidad = unidad; this.selectedDetail = unidad; - this.selectedClients = this.collectAllClients(unidad); - this.selectedClientsOriginal = [...this.selectedClients]; + this.selectedClients.data = this.collectAllClients(unidad); + this.selectedClientsOriginal = [...this.selectedClients.data]; this.loadChildrenAndClients(unidad.id).then((fullData) => { const treeData = this.convertToTreeData(fullData); this.treeDataSource.data = treeData[0]?.children || []; }); this.isTreeViewActive = true; + + console.log('Selected unidad:', unidad); } private collectAllClients(node: UnidadOrganizativa): Client[] { - let clients = node.clients || []; + let clients = (node.clients || []).map(client => ({ + ...client, + parentName: node.name + })); + if (node.children) { node.children.forEach((child) => { - clients = clients.concat(this.collectAllClients(child)); + clients = clients.concat(this.collectAllClients(child).map(client => ({ + ...client, + parentName: client.parentName || '' + }))); }); } return clients; } + private async loadChildrenAndClients(id: string): Promise { try { const childrenData = await this.dataService.getChildren(id).toPromise(); - + const processHierarchy = (nodes: UnidadOrganizativa[]): UnidadOrganizativa[] => { return nodes.map((node) => ({ ...node, children: node.children ? processHierarchy(node.children) : [], })); }; - + return { ...this.selectedUnidad!, children: childrenData ? processHierarchy(childrenData) : [], @@ -222,19 +266,19 @@ export class GroupsComponent implements OnInit, OnDestroy { return this.selectedUnidad!; } } - + private convertToTreeData(data: UnidadOrganizativa): TreeNode[] { - const processNode = (node: UnidadOrganizativa): TreeNode => ({ - name: node.name, - type: node.type, - '@id': node['@id'], - children: node.children?.map(processNode) || [], - hasClients: (node.clients?.length ?? 0) > 0, - }); - return [processNode(data)]; -} - + const processNode = (node: UnidadOrganizativa): TreeNode => ({ + name: node.name, + type: node.type, + '@id': node['@id'], + children: node.children?.map(processNode) || [], + hasClients: (node.clients?.length ?? 0) > 0, + }); + return [processNode(data)]; + } + onNodeClick(node: TreeNode): void { this.selectedNode = node; @@ -246,8 +290,15 @@ export class GroupsComponent implements OnInit, OnDestroy { this.subscriptions.add( this.http.get<{ clients: Client[] }>(`${this.baseUrl}${node['@id']}`).subscribe( (data) => { - this.selectedClientsOriginal = [...data.clients]; - this.selectedClients = data.clients || []; + const clientsWithParentName = (data.clients || []).map(client => ({ + ...client, + parentName: node.name + })); + this.selectedClients.data = clientsWithParentName; + this.selectedClients._updateChangeSubscription(); + if (this._paginator) { + this._paginator.firstPage(); + } }, (error) => { console.error('Error fetching clients:', error); @@ -255,11 +306,11 @@ export class GroupsComponent implements OnInit, OnDestroy { ) ); } else { - this.selectedClients = []; - this.selectedClientsOriginal = []; + this.selectedClients.data = []; + this.selectedClients._updateChangeSubscription(); } } - + getNodeIcon(node: TreeNode): string { switch (node.type) { case NodeType.OrganizationalUnit: @@ -309,11 +360,11 @@ export class GroupsComponent implements OnInit, OnDestroy { this.organizationalUnits = data; if (this.selectedUnidad) { this.loadChildrenAndClients(this.selectedUnidad?.id || '').then((updatedData) => { - this.selectedUnidad = updatedData; + this.selectedUnidad = updatedData; const treeData = this.convertToTreeData(updatedData); this.originalTreeData = treeData[0]?.children || []; this.treeDataSource.data = [...this.originalTreeData]; - }); + }); } }, (error) => console.error('Error fetching organizational units', error) @@ -333,9 +384,9 @@ export class GroupsComponent implements OnInit, OnDestroy { } } - onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void{ + onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void { event.stopPropagation(); - const uuid = node ? this.extractUuid(node['@id']) : null; + const uuid = node ? this.extractUuid(node['@id']) : null; if (!uuid) return; if (!node) return; @@ -371,7 +422,7 @@ export class GroupsComponent implements OnInit, OnDestroy { private refreshClientsForNode(node: TreeNode): void { if (!node['@id']) { - this.selectedClients = []; + this.selectedClients.data = []; return; } this.fetchClientsForNode(node); @@ -420,7 +471,7 @@ export class GroupsComponent implements OnInit, OnDestroy { } executeCommand(command: Command, selectedNode: TreeNode | null): void { - + if (!selectedNode) { this.toastr.error('No hay un nodo seleccionado.'); return; @@ -474,19 +525,19 @@ export class GroupsComponent implements OnInit, OnDestroy { const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase()); const matchesType = filterType ? node.type.toLowerCase() === filterType.toLowerCase() : true; const filteredChildren = node.children ? filterNodes(node.children) : []; - + if ((matchesName && matchesType) || filteredChildren.length > 0) { filteredNodes.push({ ...node, children: filteredChildren }); } } return filteredNodes; }; - + const filteredData = filterNodes(this.originalTreeData); this.treeDataSource.data = filteredData; } - - + + onTreeFilterInput(event: Event): void { const input = event.target as HTMLInputElement; @@ -501,23 +552,11 @@ export class GroupsComponent implements OnInit, OnDestroy { } filterClients(searchTerm: string): void { - if (!searchTerm) { - this.selectedClients = [...this.selectedClientsOriginal]; - return; - } - - const lowerTerm = searchTerm.toLowerCase(); - - this.selectedClients = this.selectedClientsOriginal.filter((client) => { - return ( - client.name.toLowerCase().includes(lowerTerm) || - client.ip?.toLowerCase().includes(lowerTerm) || - client.status?.toLowerCase().includes(lowerTerm) || - client.mac?.toLowerCase().includes(lowerTerm) - ); - }); + this.searchTerm = searchTerm.trim().toLowerCase(); + this.selectedClients.filter = this.searchTerm; } + public setSelectedNode(node: TreeNode): void { this.selectedNode = node; } diff --git a/ogWebconsole/src/app/components/groups/model/model.ts b/ogWebconsole/src/app/components/groups/model/model.ts index 7789157..39f747a 100644 --- a/ogWebconsole/src/app/components/groups/model/model.ts +++ b/ogWebconsole/src/app/components/groups/model/model.ts @@ -44,6 +44,7 @@ export interface Client { createdAt: string; createdBy: string; uuid: string; + parentName?: string; } export interface ClientCollection { diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index 59fe4cd..d73f805 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -420,5 +420,6 @@ "TOOLTIP_MENUS": "Menu management (option disabled)", "search": "Search", "TOOLTIP_SEARCH": "Search function (option disabled)", - "detailsOf": "Details of" + "detailsOf": "Details of", + "filters": "Filters" } diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index b13e8b4..eec7c10 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -424,5 +424,21 @@ "detailsOf": "Detalles de", "editUnitMenu": "Editar", "addInternalUnitMenu": "Añadir", - "addClientMenu": "Añadir cliente" + "addClientMenu": "Añadir cliente", + "filters": "Filtros", + "searchClient": "Buscar cliente", + "searchTree": "Buscar en árbol", + "filterByType": "Filtrar por tipo", + "all": "Todos", + "classroomsGroup": "Grupos de aulas", + "classrooms": "Aulas", + "computerGroups": "Grupos de PCs", + "executeCommand": "Ejecutar comando", + "roomMap": "Plano de aula", + "addOrganizationalUnit": "Añadir unidad organizativa", + "edit": "Editar", + "delete": "Eliminar", + "clients": "Clientes", + "sync": "Sincronizar", + "viewDetails": "Ver detalles" } From 81e20ec9f9def3d99e699fe2d721a66f58fdd2d7 Mon Sep 17 00:00:00 2001 From: apuente Date: Thu, 5 Dec 2024 14:16:23 +0100 Subject: [PATCH 14/17] Refactor translation keys in HTML templates --- .../admin/users/users/users.component.html | 2 +- .../components/groups/groups.component.html | 24 ++++++++--------- .../create-client.component.html | 4 +-- ogWebconsole/src/locale/en.json | 26 +++++++++++++++++-- ogWebconsole/src/locale/es.json | 7 ++++- 5 files changed, 45 insertions(+), 18 deletions(-) diff --git a/ogWebconsole/src/app/components/admin/users/users/users.component.html b/ogWebconsole/src/app/components/admin/users/users/users.component.html index 66c6869..f26e1ff 100644 --- a/ogWebconsole/src/app/components/admin/users/users/users.component.html +++ b/ogWebconsole/src/app/components/admin/users/users/users.component.html @@ -1,5 +1,5 @@
-

{{ 'adminImagesTitle' | translate }}

+

{{ 'adminUsersTitle' | translate }}

@@ -241,7 +241,7 @@
- + @@ -257,7 +257,7 @@ - + + - + - + - + - + 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 56ca328..c724c43 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 @@ -18,10 +18,10 @@

Añadir múltiples clientes

- +

o añadelos manualmente:

- +
diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index 126dc38..5e538c7 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -232,7 +232,6 @@ "editClientDialogTitle": "Edit Client", "organizationalUnitLabel": "Parent", "ogLiveLabel": "OgLive", - "repositoryLabel": "Repository", "serialNumberLabel": "Serial Number", "netifaceLabel": "Network interface", "netDriverLabel": "Network driver", @@ -422,5 +421,28 @@ "search": "Search", "TOOLTIP_SEARCH": "Search function (option disabled)", "detailsOf": "Details of", - "filters": "Filters" + "editUnitMenu": "Edit", + "addInternalUnitMenu": "Add", + "addClientMenu": "Add client", + "filters": "Filters", + "searchClient": "Search client", + "searchTree": "Search in tree", + "filterByType": "Filter by type", + "all": "All", + "classroomsGroup": "Classroom groups", + "classrooms": "Classrooms", + "computerGroups": "PC groups", + "executeCommand": "Execute command", + "roomMap": "Classroom map", + "addOrganizationalUnit": "Add organizational unit", + "edit": "Edit", + "delete": "Delete", + "clients": "Clients", + "sync": "Sync", + "viewDetails": "View details", + "name": "Name", + "maintenance": "Maintenance", + "subnet": "Subnet", + "parent": "Parent", + "adminUsersTitle": "Manage users" } diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index 5ae954a..217a88c 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -441,5 +441,10 @@ "delete": "Eliminar", "clients": "Clientes", "sync": "Sincronizar", - "viewDetails": "Ver detalles" + "viewDetails": "Ver detalles", + "name": "Nombre", + "maintenance": "Mantenimiento", + "subnet": "Subred", + "parent": "Padre", + "adminUsersTitle": "Administrar usuarios" } From 13cea10946010c02ecb826483efd3d1fda1a5794 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 5 Dec 2024 14:56:02 +0100 Subject: [PATCH 15/17] Added router in subnet. Advanced bootfile changes, and partition assistant updates --- .../components/groups/groups.component.css | 55 +++++++++--- .../components/groups/groups.component.html | 85 +++++++++++-------- .../app/components/groups/groups.component.ts | 10 +-- ogWebconsole/src/locale/en.json | 1 + ogWebconsole/src/locale/es.json | 1 + 5 files changed, 98 insertions(+), 54 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index 01d8902..d0d9fe2 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -186,7 +186,6 @@ button[mat-raised-button] { mat-tree { background-color: #f9f9f9; - border-right: 1px solid #ddd; padding: 10px; } @@ -317,29 +316,34 @@ mat-tree mat-tree-node.disabled:hover { margin-bottom: 16px; } -.pc-og-live { - color: #4caf50; +.chip-busy { + background-color: indianred !important; + color: black; } -.pc-busy { - color: #ff9800; +.chip-og-live { + background-color: yellow !important; + color: black; } -.pc-windows { - color: #0078d7; +.chip-windows, +.chip-windows-session, +.chip-macos { + background-color: cornflowerblue !important; + color: white; } -.pc-linux { - color: #f0ad4e; +.chip-linux, +.chip-linux-session { + background-color: mediumpurple !important; + color: white; } -.pc-macos { - color: #999999; +.chip-off { + background-color: darkgrey !important; + color: white; } -.pc-off { - color: #f44336; -} .clients-card-container { display: flex; @@ -553,7 +557,30 @@ button[mat-raised-button] { padding: 0 16px; } +.mat-elevation-z8 { + box-shadow: 0px 0px 0px rgba(0,0,0,0.2); +} + .filters-container mat-form-field { flex: 1 1 300px; max-width: 300px; } + +.client-info { + display: flex; + flex-direction: column; + gap: 3px; + margin: 5px; +} + +.client-name { + font-size: 16px; + font-weight: bold; +} + +.flex { + display: flex; + justify-self: center; + align-items: center; +} + diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index fb854e8..bb3f037 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -49,7 +49,7 @@ {{ 'computerGroups' | translate }} - + @@ -159,6 +159,8 @@ + + - - + + + +
Nombre {{ 'name' | translate }} {{ client.name }} {{ client.oglive }} Estado {{ 'status' | translate }} - Mantenimiento {{ 'maintenance' | translate }} {{ client.mantenimiento }} Subred {{ 'subnet' | translate }} {{ client.subnet }} Plantilla PXE {{ 'pxeTemplate' | translate }} {{ client.pxeTemplate }} Padre {{ 'parent' | translate }} {{ client.parentName }} Acciones {{ 'actions' | translate }}
- - - - - - - - - + - + @@ -268,12 +277,25 @@ 'chip-off': client.status === 'off' }"> {{ client.status || 'off' }} + + + + - + @@ -281,14 +303,14 @@ - + - + + + + @@ -322,6 +325,17 @@ + + + + + +
{{ 'name' | translate }} {{ client.name }} IP {{ client.ip }} MAC {{ client.mac }} +
+
{{ client.name }}
+
{{ client.ip }}
+
{{ client.mac }}
+
+
OG Live {{ client.oglive }} {{ client.ogLive?.name }} {{ 'status' | translate }} {{ 'maintenance' | translate }} {{ client.mantenimiento }} {{ client.maintenance }} {{ 'subnet' | translate }} {{ 'pxeTemplate' | translate }} {{ client.pxeTemplate }} {{ client.template?.name }} {{ 'parent' | translate }} {{ client.parentName }} {{ 'actions' | translate }} @@ -297,11 +319,6 @@ - - + + + + + + + + + {{ 'maintenance' | translate }} {{ client.maintenance }}