diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index c0e817c..c2154e8 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -13,7 +13,8 @@ matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000"> {{ 'newOrganizationalUnitButton' | translate }} - + @@ -26,332 +27,309 @@ - -
-
- - {{ 'searchTree' | translate }} - - - - - {{ 'searchClient' | translate }} - - - - - - -
+
+
- -
- -
- - - - - {{ - 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.name }} - - - IP: {{ node.ip }} - - - - + + + + +
- - - - - - - - - - - - - - - - - -
-
- {{ 'clients' | translate }} - {{ selectedNode?.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.name }} + + - IP: {{ node.ip }} + + + +
-
- -
-
-
- Client Icon + -
- {{ client.name }} - {{ client.ip }} - {{ client.mac }} + + + + + + + + + + + + + -
- + +
+
+ + {{ 'clients' | translate }} + {{ selectedNode?.name }} + +
+ + + +
+
- +
+ +
- - +
+
+ +
+
+
+ Client Icon + +
+ {{ client.name }} + {{ client.ip }} + {{ client.mac }} + +
+ + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + {{ 'status' | translate }} + Client Icon + {{ 'sync' | translate }} + + + + {{ 'name' | translate }} +
+
{{ client.name }}
+
{{ client.ip }}
+
{{ client.mac }}
+
+
OG Live {{ (client.ogLive?.filename || '').slice(0, 15) }}{{ + (client.ogLive?.filename?.length > 15) ? '...' : '' }} {{ 'maintenance' | translate }} {{ client.maintenance }} {{ 'subnet' | translate }} {{ client.subnet }} {{ 'pxeTemplate' | translate }} {{ client.template?.name }} {{ 'parent' | translate }} {{ client.parentName }} {{ 'actions' | translate }} + + + + + + + +
+ +
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - {{ 'status' | translate }} - Client Icon - {{ 'sync' | translate }} - - - - {{ 'name' | translate }} -
-
{{ client.name }}
-
{{ client.ip }}
-
{{ client.mac }}
-
-
OG Live {{ (client.ogLive?.filename || '').slice(0, 15) }}{{ (client.ogLive?.filename?.length > 15) ? '...' : '' }} {{ 'maintenance' | translate }} {{ client.maintenance }} {{ 'subnet' | translate }} {{ client.subnet }} {{ 'pxeTemplate' | translate }} {{ client.template?.name }} {{ 'parent' | translate }} {{ client.parentName }} {{ 'actions' | translate }} - - - - - - - -
- + + +
+ {{ 'noClients' | translate }} + error_outline +
+
- - - -
- -
-
- {{ 'noClients' | translate }} - error_outline -
-
- -
- + \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/groups.component.spec.ts b/ogWebconsole/src/app/components/groups/groups.component.spec.ts index 3da4b0e..cf1f2f7 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.spec.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.spec.ts @@ -25,7 +25,8 @@ import { JoyrideModule } from 'ngx-joyride'; import { MatMenuModule } from '@angular/material/menu'; import { MatTreeModule } from '@angular/material/tree'; import { TreeNode } from './model/model'; -import {ExecuteCommandComponent} from "../commands/main-commands/execute-command/execute-command.component"; +import { LoadingComponent } from '../../shared/loading/loading.component'; // Importa el componente LoadingComponent +import { ExecuteCommandComponent } from '../commands/main-commands/execute-command/execute-command.component'; describe('GroupsComponent', () => { let component: GroupsComponent; @@ -33,7 +34,7 @@ describe('GroupsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [GroupsComponent, ExecuteCommandComponent], + declarations: [GroupsComponent, ExecuteCommandComponent, LoadingComponent], // Declara LoadingComponent imports: [ HttpClientTestingModule, ToastrModule.forRoot(), @@ -64,8 +65,7 @@ describe('GroupsComponent', () => { { provide: MatDialogRef, useValue: {} }, { provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } } ] - }) - .compileComponents(); + }).compileComponents(); fixture = TestBed.createComponent(GroupsComponent); component = fixture.componentInstance; @@ -76,18 +76,6 @@ describe('GroupsComponent', () => { expect(component).toBeTruthy(); }); - it('should call search on ngOnInit', () => { - spyOn(component, 'search'); - component.ngOnInit(); - expect(component.search).toHaveBeenCalled(); - }); - - it('should call search method', () => { - spyOn(component, 'search'); - component.search(); - expect(component.search).toHaveBeenCalled(); - }); - it('should clear selection', () => { spyOn(component, 'clearSelection'); component.clearSelection(); @@ -121,4 +109,20 @@ describe('GroupsComponent', () => { component.expandPathToNode(node); expect(component.expandPathToNode).toHaveBeenCalledWith(node); }); -}); + + it('should handle node click', () => { + const node: TreeNode = { id: '1', name: 'Node 1', type: 'type', children: [] }; + spyOn(component, 'fetchClientsForNode'); + component.onNodeClick(node); + expect(component.selectedNode).toBe(node); + expect(component['fetchClientsForNode']).toHaveBeenCalledWith(node); + }); + + it('should fetch clients for node', () => { + const node: TreeNode = { id: '1', name: 'Node 1', type: 'type', children: [] }; + spyOn(component['http'], 'get').and.callThrough(); + component.fetchClientsForNode(node); + expect(component.isLoadingClients).toBeTrue(); + expect(component['http'].get).toHaveBeenCalledWith(`${component.baseUrl}/clients?organizationalUnit.id=${node.id}`); + }); +}); \ 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 a6e7486..ef3ce4f 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -21,8 +21,8 @@ import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom- import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatPaginator } from '@angular/material/paginator'; -import {CreateMultipleClientComponent} from "./shared/clients/create-multiple-client/create-multiple-client.component"; -import {SelectionModel} from "@angular/cdk/collections"; +import { CreateMultipleClientComponent } from "./shared/clients/create-multiple-client/create-multiple-client.component"; +import { SelectionModel } from "@angular/cdk/collections"; enum NodeType { OrganizationalUnit = 'organizational-unit', @@ -42,13 +42,14 @@ export class GroupsComponent implements OnInit, OnDestroy { organizationalUnits: UnidadOrganizativa[] = []; selectedUnidad: UnidadOrganizativa | null = null; selectedDetail: UnidadOrganizativa | null = null; - loading = false; + initialLoading: boolean = true; isLoadingClients: boolean = false; searchTerm = ''; treeControl: FlatTreeControl; treeFlattener: MatTreeFlattener; treeDataSource: MatTreeFlatDataSource; selectedNode: TreeNode | null = null; + hasClients: boolean = false; commands: Command[] = []; commandsLoading = false; selectedClients = new MatTableDataSource([]); @@ -62,7 +63,7 @@ export class GroupsComponent implements OnInit, OnDestroy { private originalTreeData: TreeNode[] = []; arrayClients: any[] = []; - displayedColumns: string[] = ['select', 'status','sync', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions']; + displayedColumns: string[] = ['select', 'status', 'sync', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions']; private _sort!: MatSort; private _paginator!: MatPaginator; @@ -109,8 +110,8 @@ export class GroupsComponent implements OnInit, OnDestroy { this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); } + ngOnInit(): void { - this.search(); this.updateGridCols(); this.refreshData(); window.addEventListener('resize', this.updateGridCols); @@ -126,11 +127,13 @@ export class GroupsComponent implements OnInit, OnDestroy { }; } + ngOnDestroy(): void { window.removeEventListener('resize', this.updateGridCols); this.subscriptions.unsubscribe(); } + private transformer = (node: TreeNode, level: number): FlatNode => ({ id: node.id, name: node.name, @@ -142,15 +145,18 @@ export class GroupsComponent implements OnInit, OnDestroy { '@id': node['@id'], }); + toggleView(view: 'card' | 'list'): void { this.currentView = view; } + updateGridCols = (): void => { const width = window.innerWidth; this.cols = width <= 600 ? 1 : width <= 960 ? 2 : width <= 1280 ? 3 : 4; }; + clearSelection(): void { this.selectedUnidad = null; this.selectedDetail = null; @@ -158,6 +164,7 @@ export class GroupsComponent implements OnInit, OnDestroy { this.selectedNode = null; } + // Función para obtener los filtros guardados actualmente deshabilitada // getFilters(): void { // this.subscriptions.add( @@ -171,6 +178,8 @@ export class GroupsComponent implements OnInit, OnDestroy { // ) // ); // } + + getFilters(): void { this.subscriptions.add( this.dataService.getFilters().subscribe( @@ -184,44 +193,32 @@ export class GroupsComponent implements OnInit, OnDestroy { ); } - search(): void { - this.loading = true; - 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; - } - ) - ); - } private convertToTreeData(data: UnidadOrganizativa): TreeNode { - const processNode = (node: UnidadOrganizativa): TreeNode => ({ - id: node.id, - uuid: node.uuid, - name: node.name, - type: node.type, - '@id': node['@id'], - children: node.children?.map(processNode) || [], - hasClients: (node.clients?.length ?? 0) > 0, - }); + const processNode = (node: UnidadOrganizativa): TreeNode => { + const children = node.children?.map(processNode) || []; + const hasClients = (node.clients?.length ?? 0) > 0 || children.some(child => child.hasClients); + + return { + id: node.id, + uuid: node.uuid, + name: node.name, + type: node.type, + '@id': node['@id'], + children: children, + hasClients: hasClients, + }; + }; + return processNode(data); } - private refreshData(selectedNodeIdOrUuid?: string): void { - this.loading = true; - this.isLoadingClients = !!selectedNodeIdOrUuid; + private refreshData(selectedNodeIdOrUuid?: string): void { this.dataService.getOrganizationalUnits().subscribe({ next: (data) => { this.originalTreeData = data.map((unidad) => this.convertToTreeData(unidad)); this.treeDataSource.data = [...this.originalTreeData]; - if (selectedNodeIdOrUuid) { this.selectedNode = this.findNodeByIdOrUuid(this.treeDataSource.data, selectedNodeIdOrUuid); @@ -240,19 +237,15 @@ export class GroupsComponent implements OnInit, OnDestroy { this.selectedClients.data = []; } } - - this.loading = false; - this.isLoadingClients = false; }, error: (error) => { console.error('Error fetching organizational units', error); this.toastr.error('Ocurrió un error al cargar las unidades organizativas'); - this.loading = false; - this.isLoadingClients = false; }, }); } + expandPathToNode(node: TreeNode): void { const path: TreeNode[] = []; let currentNode: TreeNode | null = node; @@ -270,6 +263,7 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } + private findParentNode(treeData: TreeNode[], childId: string): TreeNode | null { for (const node of treeData) { if (node.children?.some((child) => child.id === childId)) { @@ -286,6 +280,7 @@ export class GroupsComponent implements OnInit, OnDestroy { return null; } + private findNodeByIdOrUuid(treeData: TreeNode[], identifier: string): TreeNode | null { const search = (nodes: TreeNode[]): TreeNode | null => { for (const node of nodes) { @@ -300,24 +295,30 @@ export class GroupsComponent implements OnInit, OnDestroy { return search(treeData); } + onNodeClick(node: TreeNode): void { this.selectedNode = node; this.fetchClientsForNode(node); } - private fetchClientsForNode(node: TreeNode): void { + + public fetchClientsForNode(node: TreeNode): void { this.isLoadingClients = true; this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({ next: (response) => { this.selectedClients.data = response['hydra:member']; + this.hasClients = node.hasClients ?? false; this.isLoadingClients = false; + this.initialLoading = false; }, error: () => { this.isLoadingClients = false; + this.initialLoading = false; } }); } + addOU(event: MouseEvent, parent: TreeNode | null = null): void { event.stopPropagation(); const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { @@ -332,6 +333,7 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } + addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void { event.stopPropagation(); const targetNode = organizationalUnit || this.selectedNode; @@ -353,6 +355,7 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } + addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void { event.stopPropagation(); const targetNode = organizationalUnit || this.selectedNode; @@ -377,14 +380,15 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } + onEditNode(event: MouseEvent, node: TreeNode | null): void { event.stopPropagation(); const uuid = node ? this.extractUuid(node['@id']) : null; if (!uuid) return; const dialogRef = node?.type !== NodeType.Client - ? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) - : this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); + ? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) + : this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); dialogRef.afterClosed().subscribe(() => { if (node) { @@ -393,6 +397,7 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } + onDeleteClick(event: MouseEvent, node: TreeNode | null): void { event.stopPropagation(); const uuid = node ? this.extractUuid(node['@id']) : null; @@ -411,6 +416,7 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } + private deleteEntityorClient(uuid: string, type: string): void { if (!this.selectedNode) return; @@ -441,17 +447,19 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } + onEditClick(event: MouseEvent, type: string, uuid: string): void { event.stopPropagation(); const dialogRef = type !== NodeType.Client - ? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) - : this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); + ? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) + : this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); dialogRef.afterClosed().subscribe(() => { this.refreshData(this.selectedNode?.id); }); } + onRoomMap(room: TreeNode | null): void { if (!room || !room['@id']) return; this.subscriptions.add( @@ -469,6 +477,7 @@ export class GroupsComponent implements OnInit, OnDestroy { ); } + executeCommand(command: Command, selectedNode: TreeNode | null): void { if (!selectedNode) { @@ -479,11 +488,13 @@ export class GroupsComponent implements OnInit, OnDestroy { } } + onShowClientDetail(event: MouseEvent, client: Client): void { event.stopPropagation(); this.router.navigate(['clients', client.uuid], { state: { clientData: client } }); } + onShowDetailsClick(event: MouseEvent, data: TreeNode | null): void { event.stopPropagation(); if (data && data.type !== NodeType.Client) { @@ -495,10 +506,12 @@ export class GroupsComponent implements OnInit, OnDestroy { } } + openBottomSheet(): void { this.bottomSheet.open(LegendComponent); } + iniciarTour(): void { this.joyrideService.startTour({ steps: ['groupsTitleStepText', 'filtersPanelStep', 'addStep', 'keyStep', 'tabsStep'], @@ -507,9 +520,11 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } + hasChild = (_: number, node: FlatNode): boolean => node.expandable; isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable; + filterTree(searchTerm: string): void { const expandPaths: TreeNode[][] = []; @@ -545,6 +560,7 @@ export class GroupsComponent implements OnInit, OnDestroy { } } + private expandPath(path: TreeNode[]): void { path.forEach((pathNode) => { const flatNode = this.treeControl.dataNodes?.find((n) => n.id === pathNode.id); @@ -554,27 +570,32 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } + onTreeFilterInput(event: Event): void { const input = event.target as HTMLInputElement; const searchTerm = input?.value.trim() || ''; this.filterTree(searchTerm); } + onClientFilterInput(event: Event): void { const input = event.target as HTMLInputElement; const searchTerm = input?.value || ''; this.filterClients(searchTerm); } + filterClients(searchTerm: string): void { this.searchTerm = searchTerm.trim().toLowerCase(); this.selectedClients.filter = this.searchTerm; } + public setSelectedNode(node: TreeNode): void { this.selectedNode = node; } + getStatus(client: Client, node: any): void { if (!client.uuid || !client['@id']) return; @@ -599,12 +620,14 @@ export class GroupsComponent implements OnInit, OnDestroy { ); } + isAllSelected() { const numSelected = this.selection.selected.length; const numRows = this.selectedClients.data.length; return numSelected === numRows; } + toggleAllRows() { if (this.isAllSelected()) { this.selection.clear(); @@ -616,39 +639,44 @@ export class GroupsComponent implements OnInit, OnDestroy { this.arrayClients = [...this.selection.selected]; } + toggleRow(row: any) { this.selection.toggle(row); this.updateSelectedClients(); } + updateSelectedClients() { this.arrayClients = [...this.selection.selected]; } + getClientPath(client: Client): string { const path: string[] = []; let currentNode: TreeNode | null = this.findNodeByIdOrUuid(this.treeDataSource.data, client.organizationalUnit.uuid); - + while (currentNode) { path.unshift(currentNode.name); currentNode = currentNode.id ? this.findParentNode(this.treeDataSource.data, currentNode.id) : null; } - + return path.join(' / '); } + private extractUuid(idPath: string | undefined): string | null { return idPath ? idPath.split('/').pop() || null : null; } + clearTreeSearch(inputElement: HTMLInputElement): void { - inputElement.value = ''; - this.filterTree(''); + inputElement.value = ''; + this.filterTree(''); } - + + clearClientSearch(inputElement: HTMLInputElement): void { inputElement.value = ''; - this.filterClients(''); + this.filterClients(''); } - } diff --git a/ogWebconsole/src/app/shared/loading/loading.component.css b/ogWebconsole/src/app/shared/loading/loading.component.css index 04b222d..ece88c8 100644 --- a/ogWebconsole/src/app/shared/loading/loading.component.css +++ b/ogWebconsole/src/app/shared/loading/loading.component.css @@ -4,7 +4,7 @@ left: 0; width: 100vw; height: 100vh; - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.2); display: flex; justify-content: center; align-items: center;