From 361b98d22118068368c0f34fac331b62265c2002 Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Thu, 17 Sep 2026 12:21:56 +0200 Subject: [PATCH] feat: UI search filters, network speeds and createImage for data partitions --- .../create-image/create-image.component.ts | 3 +- .../components/groups/groups.component.css | 38 +++++ .../components/groups/groups.component.html | 138 ++++++++---------- .../app/components/groups/groups.component.ts | 99 +++++++------ .../pxe-boot-files.component.html | 10 +- .../pxe-boot-files.component.ts | 20 ++- .../add-clients-to-subnet.component.css | 1 - .../add-clients-to-subnet.component.html | 34 +++-- .../add-clients-to-subnet.component.ts | 15 +- ogWebconsole/src/styles.css | 17 +++ 10 files changed, 236 insertions(+), 139 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts index fffa10a..a752bae 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts @@ -66,7 +66,8 @@ export class CreateClientImageComponent implements OnInit{ } isPartitionDisabled = (row: any): boolean => { - return !row.operativeSystem; + // Se debe permitir crear imagen de particiones de sólo datos, tan sólo evitamos partición CACHE + return row.filesystem === 'CACHE'; }; columns = [ diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index 3730d39..cb4b5a4 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -1617,6 +1617,44 @@ mat-button-toggle-group { height: 14px; } +/* 2. MODIFICADORES DE COLOR (Fondo claro + Texto oscuro de la misma gama) */ + +/* 10 Gbps - Morado (Destaca como velocidad ultra-rápida/premium) */ +.badge-net-speed.speed-10000 { + background-color: #f3e5f5; /* Purple 50 */ + color: #7b1fa2; /* Purple 700 */ +} + +/* 2.5 Gbps / 5 Gbps - Azul (El que tú ya tenías) */ +.badge-net-speed.speed-5000, .badge-net-speed.speed-2500 { + background-color: #e3f2fd; /* Blue 50 */ + color: #1976d2; /* Blue 700 */ +} + +/* 1 Gbps - Verde (El estándar de red saludable) */ +.badge-net-speed.speed-1000 { + background-color: #e8f5e9; /* Green 50 */ + color: #388e3c; /* Green 700 */ +} + +/* 100 Mbps - Naranja (Aviso de enlace inferior/cuello de botella) */ +.badge-net-speed.speed-100 { + background-color: #fff3e0; /* Orange 50 */ + color: #f57c00; /* Orange 700 */ +} + +/* 10 Mbps - Rojo (Aviso crítico / Posible cable roto) */ +.badge-net-speed.speed-10 { + background-color: #ffebee; /* Red 50 */ + color: #d32f2f; /* Red 700 */ +} + +/* Desconectado / Down - Gris (Neutro) */ +.badge-net-speed.speed-down { + background-color: #f5f5f5; /* Grey 100 */ + color: #616161; /* Grey 700 */ +} + .list-view-badge { display: inline-flex; margin-top: 0; diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 568509c..aab9067 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -73,76 +73,7 @@
- -
-
- - {{ 'searchTree' | translate }} - - @if (treeSearchInput.value) { - - } - - - {{ 'searchClient' | translate }} - - @if (clientSearchInput.value) { - - } - - - - @for (option of status; track option) { - - {{ option.name }} - - } - - @if (clientSearchStatusInput.value) { - - } - -
- - {{ 'directClientsToggle' | translate }} - -
- - -
-
- +

@@ -150,14 +81,69 @@ {{ 'organizationalStructure' | translate }}

- -
+ + + @if (showSearchFilters) { +
+
+ + {{ 'searchTree' | translate }} + + @if (treeSearchInput.value) { + + } + + + {{ 'searchClient' | translate }} + + @if (clientSearchInput.value) { + + } + + + + @for (option of status; track option) { + + {{ option.name }} + + } + + @if (clientSearchStatusInput.value) { + + } + +
+ + {{ 'directClientsToggle' | translate }} + +
+
+
+ } @@ -420,7 +406,7 @@ {{ client.ip }} {{ client.mac }} @if (client.netSpeed) { - + speed {{ client.netSpeed }} Mbps } @@ -546,7 +532,7 @@ {{ client.ip }} {{ client.mac }} @if (client.netSpeed) { - + speed {{ client.netSpeed }} Mbps } diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 984718d..7b0eb55 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -30,7 +30,7 @@ import { MatMenuTrigger } from '@angular/material/menu'; import { ClientDetailsComponent } from './shared/client-details/client-details.component'; import { PartitionTypeOrganizatorComponent } from './shared/partition-type-organizator/partition-type-organizator.component'; import { ClientTaskLogsComponent } from '../task-logs/client-task-logs/client-task-logs.component'; -import {ChangeParentComponent} from "./shared/change-parent/change-parent.component"; +import { ChangeParentComponent } from "./shared/change-parent/change-parent.component"; import { AuthService } from '@services/auth.service'; import { ClientPendingTasksComponent } from '../task-logs/client-pending-tasks/client-pending-tasks.component'; import { ClientLogsModalComponent } from './shared/client-logs-modal/client-logs-modal.component'; @@ -88,6 +88,7 @@ export class GroupsComponent implements OnInit, OnDestroy { private clientFilterSubject = new Subject(); loading = false; showOnlyDirectClients: boolean = false; + showSearchFilters: boolean = false; // Nuevas propiedades para funcionalidades mejoradas selectedClient: any = null; @@ -102,11 +103,11 @@ export class GroupsComponent implements OnInit, OnDestroy { online: number; busy: number; } = { - total: 0, - off: 0, - online: 0, - busy: 0 - }; + total: 0, + off: 0, + online: 0, + busy: 0 + }; // Tipos de firmware disponibles firmwareTypes: string[] = []; @@ -163,6 +164,15 @@ export class GroupsComponent implements OnInit, OnDestroy { if (!this.pageSizeOptions.includes(this.itemsPerPage)) { this.pageSizeOptions = [...this.pageSizeOptions, this.itemsPerPage].sort((a, b) => a - b); } + + // Load search filters visibility state from localStorage, default false + const savedShowSearch = localStorage.getItem('showSearchFilters'); + this.showSearchFilters = savedShowSearch ? JSON.parse(savedShowSearch) : false; + } + + toggleSearchFilters(): void { + this.showSearchFilters = !this.showSearchFilters; + localStorage.setItem('showSearchFilters', JSON.stringify(this.showSearchFilters)); } @@ -485,7 +495,7 @@ export class GroupsComponent implements OnInit, OnDestroy { this.selection.select(client); } }); - + if (this.isCheckingNodeStatus) { members.forEach((c: Client) => c.uuid && this.syncingClientIds.add(c.uuid)); } @@ -533,7 +543,7 @@ export class GroupsComponent implements OnInit, OnDestroy { } this.loading = false; }); - + } @@ -886,27 +896,27 @@ export class GroupsComponent implements OnInit, OnDestroy { this.selectedClients.data.forEach(c => c.uuid && this.syncingClientIds.add(c.uuid)); this.checkStatusSubscription = this.http.post<{ results: Array<{ uuid: string; ip: string; status: string; success: boolean }> }>( - `${this.baseUrl}/organizational-units/${node.uuid}/agent/status`, - {} - ).subscribe({ - next: (response) => { - this.isCheckingNodeStatus = false; - const statusMap = new Map(response.results.map(r => [r.uuid, r.status])); - response.results.forEach(r => this.syncingClientIds.delete(r.uuid)); - this.selectedClients.data = this.selectedClients.data.map(client => { - const newStatus = statusMap.get(client.uuid); - return newStatus ? { ...client, status: newStatus } : client; - }); - this.arrayClients = this.selectedClients.data; - this.calculateLocalStats(); - this.toastr.success(this.translate.instant('classroomStatusUpdated')); - }, - error: () => { - this.isCheckingNodeStatus = false; - this.selectedClients.data.forEach(c => c.uuid && this.syncingClientIds.delete(c.uuid)); - this.toastr.error(this.translate.instant('errorCheckingStatus')); - }, - }); + `${this.baseUrl}/organizational-units/${node.uuid}/agent/status`, + {} + ).subscribe({ + next: (response) => { + this.isCheckingNodeStatus = false; + const statusMap = new Map(response.results.map(r => [r.uuid, r.status])); + response.results.forEach(r => this.syncingClientIds.delete(r.uuid)); + this.selectedClients.data = this.selectedClients.data.map(client => { + const newStatus = statusMap.get(client.uuid); + return newStatus ? { ...client, status: newStatus } : client; + }); + this.arrayClients = this.selectedClients.data; + this.calculateLocalStats(); + this.toastr.success(this.translate.instant('classroomStatusUpdated')); + }, + error: () => { + this.isCheckingNodeStatus = false; + this.selectedClients.data.forEach(c => c.uuid && this.syncingClientIds.delete(c.uuid)); + this.toastr.error(this.translate.instant('errorCheckingStatus')); + }, + }); } @@ -988,11 +998,11 @@ export class GroupsComponent implements OnInit, OnDestroy { const selectedClientNames = clientData.map(client => client.name); if ( - this.selectedNode && + this.selectedNode && clientData.length === this.selectedClients.data.length && selectedClientNames.every(name => this.selectedClients.data.some(c => c.name === name)) ) { - return this.selectedNode; + return this.selectedNode; } else if (clientData.length === 1) { return clientData[0]; } else if (clientData.length > 1) { @@ -1017,7 +1027,7 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } - changeParent(event: MouseEvent, ): void { + changeParent(event: MouseEvent,): void { event.stopPropagation(); const dialogRef = this.dialog.open(ChangeParentComponent, { @@ -1128,7 +1138,7 @@ export class GroupsComponent implements OnInit, OnDestroy { this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=1&itemsPerPage=10000`).subscribe({ next: (response) => { const allClients = response['hydra:member'] || []; - + if (allClients.length === 0) { this.toastr.warning(this.translate.instant('thisOrganizationalUnitDoesNotHave')); return; @@ -1252,7 +1262,7 @@ export class GroupsComponent implements OnInit, OnDestroy { } getStatusCount(status: string): number { - switch(status) { + switch (status) { case 'off': return this.totalStats.off; case 'online': @@ -1266,12 +1276,15 @@ export class GroupsComponent implements OnInit, OnDestroy { // Métodos para el árbol mejorado - expandAll(): void { - this.treeControl.expandAll(); - } + isTreeExpanded: boolean = false; - collapseAll(): void { - this.treeControl.collapseAll(); + toggleExpandAll(): void { + if (this.isTreeExpanded) { + this.treeControl.collapseAll(); + } else { + this.treeControl.expandAll(); + } + this.isTreeExpanded = !this.isTreeExpanded; } getNodeTypeTooltip(nodeType: string): string { @@ -1358,7 +1371,7 @@ export class GroupsComponent implements OnInit, OnDestroy { deleteMultipleClients(event: MouseEvent): void { event.stopPropagation(); - + if (this.selection.selected.length === 0) { this.toastr.warning(this.translate.instant('thereAreNoClientsSelectedFor')); return; @@ -1366,10 +1379,10 @@ export class GroupsComponent implements OnInit, OnDestroy { const selectedClients = this.selection.selected; const clientNames = selectedClients.map(client => client.name).join(', '); - + const dialogRef = this.dialog.open(DeleteModalComponent, { width: '500px', - data: { + data: { name: `${selectedClients.length} cliente${selectedClients.length > 1 ? 's' : ''}`, message: `¿Está seguro de que desea eliminar los siguientes clientes?\n\n${clientNames}`, isMultiple: true @@ -1391,7 +1404,7 @@ export class GroupsComponent implements OnInit, OnDestroy { next: (response) => { this.loading = false; this.showDeletionResults(response, selectedClients); - + this.refreshData(this.selectedNode?.id); this.selection.clear(); }, 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 1affb96..3c8447c 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 @@ -28,9 +28,13 @@ {{ 'labelSelectClass' | translate }} - - @for (unit of units; track unit.id) { - {{ unit.name }} + + + @for (unit of getFilteredUnits(); track unit.id) { + {{ unit.displayName }} } diff --git a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts index f9e0b6a..3e06504 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts @@ -32,6 +32,15 @@ export class PxeBootFilesComponent implements OnInit { isFromDashboard: boolean = false; ogLiveOptions: any[] = []; globalOgLive: string | null = null; + unitSearchText: string = ''; + + getFilteredUnits(): any[] { + if (!this.unitSearchText) { + return this.units; + } + const searchText = this.unitSearchText.toLowerCase(); + return this.units.filter(unit => unit.displayName.toLowerCase().includes(searchText)); + } length: number = 0; itemsPerPage: number = 10; page: number = 0; @@ -97,7 +106,11 @@ export class PxeBootFilesComponent implements OnInit { loadUnits() { this.http.get(`${this.baseUrl}/organizational-units?type=classroom&page=1&itemsPerPage=50`).subscribe( response => { - this.units = response['hydra:member']; + this.units = response['hydra:member'].map((unit: any) => ({ + ...unit, + displayName: unit.parent && unit.parent.name ? `${unit.parent.name} / ${unit.name}` : unit.name + })); + this.units.sort((a, b) => a.displayName.localeCompare(b.displayName)); }, error => console.error(this.translate.instant('errorFetchingOrganizationalUnits1'), error) ); @@ -125,7 +138,10 @@ export class PxeBootFilesComponent implements OnInit { if (!this.selectedUnit) return; this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${this.selectedUnit.id}`).subscribe( response => { - this.dataSource.data = response['hydra:member']; + this.dataSource.data = response['hydra:member'].map((client: any) => ({ + ...client, + ogLive: client.pxeTemplate ? client.pxeTemplate['@id'] : null + })); }, error => console.error(this.translate.instant('errorFetchingChildUnits'), error) ); diff --git a/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.css b/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.css index b0c1411..1eab271 100644 --- a/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.css +++ b/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.css @@ -39,4 +39,3 @@ mat-dialog-actions { display: block; margin-bottom: 10px; } - diff --git a/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.html b/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.html index 7c6c685..fc92271 100644 --- a/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.html +++ b/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.html @@ -5,8 +5,14 @@ {{ 'selectClassroomPrompt' | translate }} - - {{ unit.name }} + + + @for (unit of getFilteredUnits(); track unit.id) { + {{ unit.displayName }} + } @@ -17,16 +23,20 @@ {{ allSelected ? 'Deseleccionar todos' : 'Seleccionar todos' }} -
- - {{ client.name }} - -
-
-

{{ 'noClientsMessage' | translate }}

-
+ @if (clients.length > 0) { +
+ @for (client of clients; track client.uuid) { + + {{ client.name }} + + } +
+ } @else { +
+

{{ 'noClientsMessage' | translate }}

+
+ }
diff --git a/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.ts b/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.ts index c4bb1ef..c48c57f 100644 --- a/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.ts +++ b/ogWebconsole/src/app/components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component.ts @@ -23,6 +23,15 @@ export class AddClientsToSubnetComponent implements OnInit { unitControl = new FormControl(); childUnitControl = new FormControl(); allSelected: boolean = false; + unitSearchText: string = ''; + + getFilteredUnits(): any[] { + if (!this.unitSearchText) { + return this.units; + } + const searchText = this.unitSearchText.toLowerCase(); + return this.units.filter(unit => unit.displayName.toLowerCase().includes(searchText)); + } constructor( private http: HttpClient, @@ -43,7 +52,11 @@ export class AddClientsToSubnetComponent implements OnInit { loadUnits() { this.http.get(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=500`).subscribe( response => { - this.units = response['hydra:member']; + this.units = response['hydra:member'].map((unit: any) => ({ + ...unit, + displayName: unit.parent && unit.parent.name ? `${unit.parent.name} / ${unit.name}` : unit.name + })); + this.units.sort((a, b) => a.displayName.localeCompare(b.displayName)); this.loading = false; }, error => console.error(this.translate.instant('errorFetchingOrganizationalUnits1'), error) diff --git a/ogWebconsole/src/styles.css b/ogWebconsole/src/styles.css index d399c19..e389072 100644 --- a/ogWebconsole/src/styles.css +++ b/ogWebconsole/src/styles.css @@ -197,3 +197,20 @@ mat-toolbar button[mat-icon-button] { .modal-overlay-blur.error { background: rgba(220, 53, 69, 0.8); } + +/* Dropdown search elements for material select */ +.dropdown-search-wrapper { + padding: 10px; + position: sticky; + top: 0; + background-color: white; + z-index: 100; +} + +.dropdown-search-input { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +}