Refactor groups component to update filter option value and add sync functionality

pull/10/head
Alvaro Puente Mella 2024-12-05 13:09:22 +01:00
parent aecc16c332
commit e9b4411ea7
5 changed files with 156 additions and 92 deletions

View File

@ -22,33 +22,34 @@
<mat-expansion-panel *ngIf="isTreeViewActive" class="filters-panel">
<mat-expansion-panel-header>
<mat-panel-title>Filtros</mat-panel-title>
<mat-panel-title>{{ 'filters' | translate }}</mat-panel-title>
</mat-expansion-panel-header>
<div class="filters-container">
<mat-form-field appearance="outline">
<mat-select (selectionChange)="loadSelectedFilter($event.value)" placeholder="Cargar filtro">
<mat-label>{{ 'searchClient' | translate }}</mat-label>
<input matInput (input)="onClientFilterInput($event)" placeholder="Buscar nombre, IP, estado o MAC">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-select (selectionChange)="loadSelectedFilter($event.value)" placeholder="Cargar filtro" disabled>
<mat-option *ngFor="let savedFilter of savedFilterNames" [value]="savedFilter">
{{ savedFilter[0] }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Buscar en el árbol</mat-label>
<input matInput (input)="onTreeFilterInput($event)" placeholder="Buscar nombre o tipo">
<mat-label>{{ 'searchTree' | translate }}</mat-label>
<input matInput (input)="onTreeFilterInput($event)" placeholder="Buscar nombre o tipo" disabled>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Filtrar por tipo</mat-label>
<mat-select [(value)]="selectedTreeFilter" (selectionChange)="filterTree(searchTerm, $event.value)">
<mat-option [value]="">Todos</mat-option>
<mat-option value="classrooms-group">Grupos de aulas</mat-option>
<mat-option value="classroom">Aulas</mat-option>
<mat-option value="group">Grupos de ordenadores</mat-option>
<mat-label>{{ 'filterByType' | translate }}</mat-label>
<mat-select [(value)]="selectedTreeFilter" (selectionChange)="filterTree(searchTerm, $event.value)" disabled>
<mat-option [value]=""> {{ 'all' | translate }} </mat-option>
<mat-option value="classrooms-group">{{ 'classroomsGroup' | translate }}</mat-option>
<mat-option value="classroom">{{ 'classrooms' | translate }}</mat-option>
<mat-option value="group">{{ 'computerGroups' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Buscar cliente</mat-label>
<input matInput (input)="onClientFilterInput($event)" placeholder="Buscar nombre, IP, estado o MAC">
</mat-form-field>
</div>
</mat-expansion-panel>
@ -166,7 +167,7 @@
<mat-menu #menu="matMenu">
<button *ngIf="selectedNode?.type === 'classroom'" mat-menu-item [matMenuTriggerFor]="commandMenu" (click)="fetchCommands()">
<mat-icon>play_arrow</mat-icon>
<span>Ejecutar Comando</span>
<span>{{ 'executeCommand' | translate }}</span>
</button>
<button mat-menu-item (click)="onShowDetailsClick($event, selectedNode)">
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
@ -174,29 +175,29 @@
</button>
<button *ngIf="selectedNode?.type === 'classroom'" mat-menu-item (click)="onRoomMap(selectedNode)">
<mat-icon>map</mat-icon>
<span>Plano aula</span>
<span>{{ 'roomMap' | translate }}</span>
</button>
<button mat-menu-item (click)="addClient($event, selectedNode)">
<mat-icon>add</mat-icon>
<span>Añadir clientes</span>
<span>{{ 'addClientMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="addOU($event, selectedNode)">
<mat-icon>playlist_add</mat-icon>
<span>Añadir unidad organizativa</span>
<span>{{ 'addOrganizationalUnit' | translate }}</span>
</button>
<button mat-menu-item (click)="onEditNode($event, selectedNode)">
<mat-icon>edit</mat-icon>
<span>Edit</span>
<span>{{ 'edit' | translate }}</span>
</button>
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)">
<mat-icon>delete</mat-icon>
<span>Delete</span>
<span>{{ 'delete' | translate }}</span>
</button>
</mat-menu>
<div class="clients-container" *ngIf="selectedClients.length > 0">
<h3>Clientes {{ selectedNode?.name ? 'del ' + selectedNode?.name : '' }}</h3>
<div class="clients-container" *ngIf="(selectedClients.data?.length || 0) > 0">
<h3>{{ 'clients' | translate }} {{ selectedNode?.name ? ('del ' + selectedNode?.name) : '' }}</h3>
<div class="clients-grid" *ngIf="currentView === 'card'">
<div *ngFor="let client of selectedClients" class="client-item">
<div *ngFor="let client of selectedClients.data" class="client-item">
<div class="client-card">
<img src="assets/images/client.png" alt="Client Icon" class="client-image" />
<div class="client-details">
@ -216,20 +217,20 @@
<mat-menu #clientMenu="matMenu">
<button mat-menu-item *ngIf="(!syncStatus || syncingClientId !== client.uuid)" (click)="getStatus(client)">
<mat-icon>sync</mat-icon>
<span>Sincronizar</span>
<span>{{ 'sync' | translate }}</span>
</button>
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
<mat-icon>edit</mat-icon>
<span>Edit</span>
<span>{{ 'edit' | translate }}</span>
</button>
<button mat-menu-item (click)="onShowClientDetail($event, client)">
<mat-icon>visibility</mat-icon>
<span>Ver detalles</span>
<span>{{ 'viewDetails' | translate }}</span>
</button>
<button mat-menu-item (click)="onDeleteClick($event, client, selectedNode)">
<mat-icon>delete</mat-icon>
<span>Delete</span>
<span>{{ 'delete' | translate }}</span>
</button>
</mat-menu>
</div>
@ -238,25 +239,25 @@
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5, 10, 20]" showFirstLastButtons></mat-paginator>
</div>
<div class="clients-table" *ngIf="currentView === 'list'">
<table mat-table [dataSource]="selectedClients" class="mat-elevation-z8">
<table mat-table matSort [dataSource]="selectedClients" class="mat-elevation-z8">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Nombre </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header> Nombre </th>
<td mat-cell *matCellDef="let client"> {{ client.name }} </td>
</ng-container>
<ng-container matColumnDef="ip">
<th mat-header-cell *matHeaderCellDef> IP </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header> IP </th>
<td mat-cell *matCellDef="let client"> {{ client.ip }} </td>
</ng-container>
<ng-container matColumnDef="mac">
<th mat-header-cell *matHeaderCellDef> MAC </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header> MAC </th>
<td mat-cell *matCellDef="let client"> {{ client.mac }} </td>
</ng-container>
<ng-container matColumnDef="oglive">
<th mat-header-cell *matHeaderCellDef> OG Live </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
<td mat-cell *matCellDef="let client"> {{ client.oglive }} </td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef> Estado </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header> Estado </th>
<td mat-cell *matCellDef="let client">
<mat-chip [ngClass]="{
'chip-og-live': client.status === 'og-live',
@ -271,19 +272,25 @@
</td>
</ng-container>
<ng-container matColumnDef="maintenace">
<th mat-header-cell *matHeaderCellDef> Mantenimiento </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header> Mantenimiento </th>
<td mat-cell *matCellDef="let client"> {{ client.mantenimiento }} </td>
</ng-container>
<ng-container matColumnDef="subnet">
<th mat-header-cell *matHeaderCellDef> Subred </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header> Subred </th>
<td mat-cell *matCellDef="let client"> {{ client.subnet }} </td>
</ng-container>
<ng-container matColumnDef="pxeTemplate">
<th mat-header-cell *matHeaderCellDef> Plantilla PXE </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header> Plantilla PXE </th>
<td mat-cell *matCellDef="let client"> {{ client.pxeTemplate }} </td>
</ng-container>
<ng-container matColumnDef="parentName">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Padre </th>
<td mat-cell *matCellDef="let client"> {{ client.parentName }} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> Acciones </th>
<th mat-header-cell *matHeaderCellDef mat-sort-header> Acciones </th>
<td mat-cell *matCellDef="let client">
<button mat-icon-button [matMenuTriggerFor]="clientMenu">
<mat-icon>more_vert</mat-icon>
@ -310,8 +317,8 @@
</mat-menu>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="['name', 'ip', 'mac', 'oglive', 'status', 'maintenace', 'subnet', 'pxeTemplate', 'actions']"></tr>
<tr mat-row *matRowDef="let row; columns: ['name', 'ip', 'mac', 'oglive', 'status', 'maintenace', 'subnet', 'pxeTemplate', 'actions'];"></tr>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5, 10, 20]" showFirstLastButtons></mat-paginator>
</div>

View File

@ -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<Client>([]);
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<UnidadOrganizativa> {
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;
}

View File

@ -44,6 +44,7 @@ export interface Client {
createdAt: string;
createdBy: string;
uuid: string;
parentName?: string;
}
export interface ClientCollection {

View File

@ -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"
}

View File

@ -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"
}