@@ -84,12 +117,12 @@
Máximo Clientes
-
+
Tiempo Máximo de Espera
-
+
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.spec.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.spec.ts
index a6a2e2b..f38ab00 100644
--- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.spec.ts
+++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.spec.ts
@@ -17,6 +17,8 @@ import { TranslateModule } from '@ngx-translate/core';
import { ToastrModule, ToastrService } from 'ngx-toastr';
import { provideRouter } from '@angular/router';
import { MatSelectModule } from '@angular/material/select';
+import {MatExpansionModule} from "@angular/material/expansion";
+import {LoadingComponent} from "../../../../../shared/loading/loading.component";
describe('DeployImageComponent', () => {
let component: DeployImageComponent;
@@ -24,7 +26,7 @@ describe('DeployImageComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- declarations: [DeployImageComponent],
+ declarations: [DeployImageComponent, LoadingComponent],
imports: [
ReactiveFormsModule,
FormsModule,
@@ -32,6 +34,7 @@ describe('DeployImageComponent', () => {
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
+ MatExpansionModule,
MatButtonModule,
MatTableModule,
MatDividerModule,
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 e5dc0c5..30be7b3 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
@@ -1,4 +1,4 @@
-import {Component, EventEmitter, Output} from '@angular/core';
+import {Component, EventEmitter, Input, Output} from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {SelectionModel} from "@angular/cdk/collections";
import {HttpClient} from "@angular/common/http";
@@ -33,11 +33,13 @@ export class DeployImageComponent {
p2pTime: Number = 0;
name: string = '';
client: any = null;
+ clientData: any = [];
+ loading: boolean = false;
protected p2pModeOptions = [
- { name: 'Leecher', value: 'p2p-mode-leecher' },
- { name: 'Peer', value: 'p2p-mode-peer' },
- { name: 'Seeder', value: 'p2p-mode-seeder' },
+ { name: 'Leecher', value: 'leecher' },
+ { name: 'Peer', value: 'peer' },
+ { name: 'Seeder', value: 'seeder' },
];
protected multicastModeOptions = [
{ name: 'Half duplex', value: "half"},
@@ -47,7 +49,6 @@ export class DeployImageComponent {
allMethods = [
'uftp',
'udpcast',
- 'multicast-direct',
'unicast',
'unicast-direct',
'p2p'
@@ -56,7 +57,6 @@ export class DeployImageComponent {
updateCacheMethods = [
'uftp',
'udpcast',
- 'multicast',
'unicast',
'p2p'
];
@@ -98,13 +98,12 @@ export class DeployImageComponent {
private toastService: ToastrService,
private route: ActivatedRoute,
private router: Router,
- ) {}
-
- ngOnInit() {
- this.clientId = this.route.snapshot.paramMap.get('id');
- this.selectedOption = 'deploy-image';
- this.loadPartitions();
+ ) {
+ const navigation = this.router.getCurrentNavigation();
+ this.clientData = navigation?.extras?.state?.['clientData'];
+ this.clientId = this.clientData?.[0]['@id'];
this.loadImages();
+ this.loadPartitions()
}
get deployMethods() {
@@ -116,7 +115,7 @@ export class DeployImageComponent {
}
loadPartitions() {
- const url = `${this.baseUrl}/clients/${this.clientId}`;
+ const url = `${this.baseUrl}${this.clientId}`;
this.http.get(url).subscribe(
(response: any) => {
if (response.partitions) {
@@ -151,11 +150,9 @@ export class DeployImageComponent {
);
}
- back() {
- this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
- }
-
save(): void {
+ this.loading = true;
+
if (!this.selectedImage) {
this.toastService.error('Debe seleccionar una imagen');
return;
@@ -171,26 +168,44 @@ export class DeployImageComponent {
return;
}
+ this.toastService.info('Preparando petición de despliegue');
+
+
const payload = {
- client: `/clients/${this.clientId}`,
+ clients: this.clientData.map((client: any) => client['@id']),
method: this.selectedMethod,
- partition: this.selectedPartition['@id'],
+ // partition: this.selectedPartition['@id'],
+ diskNumber: this.selectedPartition.diskNumber,
+ partitionNumber: this.selectedPartition.partitionNumber,
p2pMode: this.p2pMode,
p2pTime: this.p2pTime,
mcastIp: this.mcastIp,
mcastPort: this.mcastPort,
mcastMode: this.mcastMode,
mcastSpeed: this.mcastSpeed,
+ maxTime: this.mcastMaxTime,
+ maxClients: this.mcastMaxClients,
};
this.http.post(`${this.baseUrl}${this.selectedImage}/deploy-image`, payload)
.subscribe({
next: (response) => {
this.toastService.success('Petición de despliegue enviada correctamente');
+ this.loading = false;
+ this.router.navigate(['/commands-logs']);
},
error: (error) => {
console.error('Error:', error);
- this.toastService.error(error.error['hydra:description']);
+ this.toastService.error(error.error['hydra:description'], 'Se ha detectado un error en el despliegue de imágenes.', {
+ "closeButton": true,
+ "newestOnTop": false,
+ "progressBar": false,
+ "positionClass": "toast-bottom-right",
+ "timeOut": 0,
+ "extendedTimeOut": 0,
+ "tapToDismiss": false
+ });
+ this.loading = false;
}
}
);
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 385218d..782c974 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
@@ -167,3 +167,58 @@ button.remove-btn:hover {
padding: 20px;
margin: 10px auto;
}
+
+.clients-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: 8px;
+}
+
+.client-item {
+ position: relative;
+}
+
+.client-card {
+ background: #ffffff;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ position: relative;
+ padding: 8px;
+ text-align: center;
+}
+
+.client-details {
+ margin-top: 4px;
+}
+
+.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;
+}
+
+.header-container-title {
+ flex-grow: 1;
+ text-align: left;
+ padding-left: 1em;
+}
+
+.select-container {
+ margin-top: 20px;
+ align-items: center;
+ width: 100%;
+ padding: 0 5px;
+ box-sizing: border-box;
+ padding-left: 1em;
+}
+
+
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 736e158..7c09394 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,12 +1,45 @@
+
+
+
+
+
+ Clientes
+ Listado de clientes donde se realizará el particionado
+
+
+
+
+
+
![Client Icon]()
+
+
+ {{ client.name }}
+ {{ client.ip }}
+ {{ client.mac }}
+
+
+
+
+
+
+
+
+
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 80ab9af..94c85da 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
@@ -26,7 +26,7 @@ interface Partition {
templateUrl: './partition-assistant.component.html',
styleUrls: ['./partition-assistant.component.css']
})
-export class PartitionAssistantComponent implements OnInit {
+export class PartitionAssistantComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
@Output() dataChange = new EventEmitter();
partitionTypes = PARTITION_TYPES;
@@ -39,6 +39,8 @@ export class PartitionAssistantComponent implements OnInit {
updateRequests: any[] = [];
data: any = {};
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = [];
+ clientData: any = [];
+ loading: boolean = false;
private apiUrl: string = this.baseUrl + '/partitions';
@@ -51,11 +53,12 @@ export class PartitionAssistantComponent implements OnInit {
private toastService: ToastrService,
private route: ActivatedRoute,
private router: Router,
- ) {}
-
- ngOnInit() {
- this.clientId = this.route.snapshot.paramMap.get('id');
+ ) {
+ const navigation = this.router.getCurrentNavigation();
+ this.clientData = navigation?.extras?.state?.['clientData'];
+ this.clientId = this.clientData[0]['@id'];
this.loadPartitions();
+
}
get selectedDisk():any {
@@ -63,7 +66,7 @@ export class PartitionAssistantComponent implements OnInit {
}
loadPartitions() {
- const url = `${this.baseUrl}/clients/${this.clientId}`;
+ const url = `${this.baseUrl}${this.clientId}`;
this.http.get(url).subscribe(
(response) => {
this.data = response;
@@ -250,16 +253,14 @@ export class PartitionAssistantComponent implements OnInit {
return modifiedPartitions;
}
- back() {
- this.router.navigate(['clients', this.data.uuid], { state: { clientData: this.data } });
- }
-
save() {
if (!this.selectedDisk) {
this.errorMessage = 'Por favor selecciona un disco antes de guardar.';
return;
}
+ this.loading = true;
+
const totalPartitionSize = this.selectedDisk.partitions.reduce((sum: any, partition: { size: any; }) => sum + partition.size, 0);
if (totalPartitionSize > this.selectedDisk.totalDiskSize) {
@@ -283,22 +284,26 @@ export class PartitionAssistantComponent implements OnInit {
size: partition.size,
partitionCode: partition.partitionCode,
filesystem: partition.filesystem,
- client: `/clients/${this.clientId}`,
uuid: partition.uuid,
removed: partition.removed || false,
format: partition.format || false,
}));
if (newPartitions.length > 0) {
- const bulkPayload = { partitions: newPartitions };
+ const bulkPayload = {
+ partitions: newPartitions,
+ clients: this.clientData.map((client: any) => client['@id']),
+ };
this.http.post(this.apiUrl, bulkPayload).subscribe(
(response) => {
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
+ this.loading = false;
this.router.navigate(['/commands-logs']);
},
(error) => {
console.error('Error al crear las particiones:', error);
+ this.loading = false;
this.toastService.error('Error al crear las particiones.');
}
);
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/restore-image/restore-image.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/restore-image/restore-image.component.html
deleted file mode 100644
index 2bd354f..0000000
--- a/ogWebconsole/src/app/components/groups/components/client-main-view/restore-image/restore-image.component.html
+++ /dev/null
@@ -1,39 +0,0 @@
-{{ 'diskImageAssistantTitle' | translate }}
-
-
-
-
-
-
- {{ 'partitionColumn' | translate }} |
- {{ 'isoImageColumn' | translate }} |
- {{ 'ogliveColumn' | translate }} |
-
-
-
-
- {{ partition.partitionNumber }} |
-
-
- |
-
-
- |
-
-
-
-
-
-
-
-
diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css
index 1120910..ae08699 100644
--- a/ogWebconsole/src/app/components/groups/groups.component.css
+++ b/ogWebconsole/src/app/components/groups/groups.component.css
@@ -3,8 +3,7 @@
display: flex;
justify-content: space-between;
align-items: center;
- padding: 20px;
- background-color: #f5f5f5;
+ padding: 10px 10px;
border-bottom: 1px solid #ddd;
}
@@ -33,58 +32,6 @@ button[mat-raised-button] {
font-size: 16px;
}
-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;
- padding: 16px;
- font-size: 14px;
- display: flex;
- 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 {
color: #757575;
cursor: pointer;
@@ -186,7 +133,7 @@ button[mat-raised-button] {
mat-tree {
background-color: #f9f9f9;
- padding: 10px;
+ padding: 0px 10px 10px 10px;
}
mat-tree mat-tree-node {
@@ -298,18 +245,14 @@ mat-tree mat-tree-node.disabled:hover {
.filters-container {
display: flex;
- flex-wrap: wrap;
- gap: 16px;
- margin-bottom: 16px;
+ justify-content: start;
+ gap: 1rem;
+ margin: 2rem 0px 0.7rem 10px;
}
.filters-container mat-form-field {
flex: 1 1 100%;
- max-width: 300px;
-}
-
-.filter-container {
- margin-bottom: 16px;
+ max-width: 250px;
}
.chip-busy {
@@ -394,24 +337,17 @@ mat-tree mat-tree-node.disabled:hover {
.tree-container {
width: 25%;
- padding: 16px;
overflow-x: hidden;
overflow-y: auto;
}
.clients-container {
width: 75%;
- padding: 16px;
+ padding: 0px 16px 16px 16px;
box-sizing: border-box;
overflow-y: auto;
}
-.clients-container h3 {
- margin-bottom: 15px;
- font-size: 1.5em;
- color: #333;
-}
-
.client-item {
display: flex;
justify-content: center;
@@ -516,23 +452,10 @@ button[mat-raised-button] {
flex-shrink: 0;
}
-.filters-container {
- display: flex;
- flex-wrap: wrap;
- gap: 16px;
- margin: 16px 0;
- 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;
@@ -586,7 +509,7 @@ button[mat-raised-button] {
.clients-title-name {
font-size: x-large;
display: block;
- padding: 1rem 1rem 1rem 15px;
+ padding: 1rem 1rem 1rem 13px;
}
.no-clients-info {
@@ -594,4 +517,5 @@ button[mat-raised-button] {
align-items: center;
gap: 10px;
margin-top: 1.5rem;
+ margin-left: 16px;
}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html
index acb8172..c0e817c 100644
--- a/ogWebconsole/src/app/components/groups/groups.component.html
+++ b/ogWebconsole/src/app/components/groups/groups.component.html
@@ -15,8 +15,8 @@
-
-
+
+
- error_outline
{{ 'noClients' | translate }}
+ error_outline
diff --git a/ogWebconsole/src/app/components/groups/groups.component.spec.ts b/ogWebconsole/src/app/components/groups/groups.component.spec.ts
index 60f8c01..3da4b0e 100644
--- a/ogWebconsole/src/app/components/groups/groups.component.spec.ts
+++ b/ogWebconsole/src/app/components/groups/groups.component.spec.ts
@@ -24,6 +24,8 @@ import { TranslateModule } from '@ngx-translate/core';
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";
describe('GroupsComponent', () => {
let component: GroupsComponent;
@@ -31,7 +33,7 @@ describe('GroupsComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- declarations: [GroupsComponent],
+ declarations: [GroupsComponent, ExecuteCommandComponent],
imports: [
HttpClientTestingModule,
ToastrModule.forRoot(),
@@ -80,21 +82,43 @@ describe('GroupsComponent', () => {
expect(component.search).toHaveBeenCalled();
});
- it('should call getFilters on ngOnInit', () => {
- spyOn(component, 'getFilters');
- component.ngOnInit();
- expect(component.getFilters).toHaveBeenCalled();
- });
-
it('should call search method', () => {
spyOn(component, 'search');
component.search();
expect(component.search).toHaveBeenCalled();
});
- it('should call getFilters method', () => {
- spyOn(component, 'getFilters');
- component.getFilters();
- expect(component.getFilters).toHaveBeenCalled();
+ it('should clear selection', () => {
+ spyOn(component, 'clearSelection');
+ component.clearSelection();
+ expect(component.clearSelection).toHaveBeenCalled();
+ });
+
+ it('should toggle view', () => {
+ component.toggleView('card');
+ expect(component.currentView).toBe('card');
+ component.toggleView('list');
+ expect(component.currentView).toBe('list');
+ });
+
+ it('should filter tree', () => {
+ const searchTerm = 'test';
+ spyOn(component, 'filterTree');
+ component.filterTree(searchTerm);
+ expect(component.filterTree).toHaveBeenCalledWith(searchTerm);
+ });
+
+ it('should add multiple clients', () => {
+ spyOn(component, 'addMultipleClients');
+ const event = new MouseEvent('click');
+ component.addMultipleClients(event);
+ expect(component.addMultipleClients).toHaveBeenCalledWith(event);
+ });
+
+ it('should expand path to node', () => {
+ const node: TreeNode = { id: '1', name: 'Node 1', type: 'type', children: [] };
+ spyOn(component, 'expandPathToNode');
+ component.expandPathToNode(node);
+ expect(component.expandPathToNode).toHaveBeenCalledWith(node);
});
});
diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts
index 10ad8ae..a6e7486 100644
--- a/ogWebconsole/src/app/components/groups/groups.component.ts
+++ b/ogWebconsole/src/app/components/groups/groups.component.ts
@@ -3,7 +3,6 @@ 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 { ToastrService } from 'ngx-toastr';
import { JoyrideService } from 'ngx-joyride';
import { FlatTreeControl } from '@angular/cdk/tree';
@@ -16,7 +15,6 @@ import { CreateClientComponent } from './shared/clients/create-client/create-cli
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 { TreeViewComponent } from './shared/tree-view/tree-view.component';
import { LegendComponent } from './shared/legend/legend.component';
import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component';
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
@@ -24,6 +22,7 @@ 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";
enum NodeType {
OrganizationalUnit = 'organizational-unit',
@@ -53,17 +52,17 @@ export class GroupsComponent implements OnInit, OnDestroy {
commands: Command[] = [];
commandsLoading = false;
selectedClients = new MatTableDataSource([]);
+ selection = new SelectionModel(true, []);
cols = 4;
- selectedClientsOriginal: Client[] = [];
currentView: 'card' | 'list' = 'list';
- isTreeViewActive = false;
savedFilterNames: [string, string][] = [];
selectedTreeFilter = '';
syncStatus = false;
syncingClientId: string | null = null;
private originalTreeData: TreeNode[] = [];
+ arrayClients: any[] = [];
- displayedColumns: string[] = ['status','sync', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
+ displayedColumns: string[] = ['select', 'status','sync', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
private _sort!: MatSort;
private _paginator!: MatPaginator;
@@ -112,9 +111,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.search();
- this.getFilters();
this.updateGridCols();
- this.loadOrganizationalUnits();
+ this.refreshData();
window.addEventListener('resize', this.updateGridCols);
this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => {
@@ -144,40 +142,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
'@id': node['@id'],
});
- private loadOrganizationalUnits(): void {
- this.loading = true;
- this.isLoadingClients = true;
- this.dataService.getOrganizationalUnits().subscribe(
- (data) => {
- this.organizationalUnits = data;
- this.loading = false;
-
- if (this.organizationalUnits.length > 0) {
- const treeData = this.organizationalUnits.map((unidad) => this.convertToTreeData(unidad));
- this.treeDataSource.data = treeData.flat();
-
- this.isTreeViewActive = true;
-
- const firstNode = this.treeDataSource.data[0];
- if (firstNode) {
- this.selectedNode = firstNode;
- this.fetchClientsForNode(firstNode);
- }
- } else {
- this.toastr.info('No existen unidades organizativas');
- this.isTreeViewActive = false;
- this.isLoadingClients = false;
- return;
- }
- },
- (error) => {
- console.error('Error fetching organizational units', error);
- this.loading = false;
- }
- );
- }
-
-
toggleView(view: 'card' | 'list'): void {
this.currentView = view;
}
@@ -191,10 +155,22 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.selectedUnidad = null;
this.selectedDetail = null;
this.selectedClients.data = [];
- this.isTreeViewActive = false;
this.selectedNode = null;
}
+ // Función para obtener los filtros guardados actualmente deshabilitada
+ // getFilters(): void {
+ // 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);
+ // }
+ // )
+ // );
+ // }
getFilters(): void {
this.subscriptions.add(
this.dataService.getFilters().subscribe(
@@ -208,21 +184,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
);
}
- 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.subscriptions.add(
@@ -239,49 +200,112 @@ export class GroupsComponent implements OnInit, OnDestroy {
);
}
- 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) : [],
- };
- } catch (error) {
- console.error('Error loading children:', error);
- return this.selectedUnidad!;
- }
- }
-
-
- private convertToTreeData(data: UnidadOrganizativa): TreeNode[] {
+ 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,
});
- return [processNode(data)];
+ return processNode(data);
}
+ private refreshData(selectedNodeIdOrUuid?: string): void {
+ this.loading = true;
+ this.isLoadingClients = !!selectedNodeIdOrUuid;
+
+ 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);
+
+ if (this.selectedNode) {
+ this.treeControl.collapseAll();
+ this.expandPathToNode(this.selectedNode);
+ this.fetchClientsForNode(this.selectedNode);
+ }
+ } else {
+ this.treeControl.collapseAll();
+ if (this.treeDataSource.data.length > 0) {
+ this.selectedNode = this.treeDataSource.data[0];
+ this.fetchClientsForNode(this.selectedNode);
+ } else {
+ this.selectedNode = null;
+ 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;
+
+ while (currentNode) {
+ path.unshift(currentNode);
+ currentNode = currentNode.id ? this.findParentNode(this.treeDataSource.data, currentNode.id) : null;
+ }
+
+ path.forEach((pathNode) => {
+ const flatNode = this.treeControl.dataNodes?.find((n) => n.id === pathNode.id);
+ if (flatNode) {
+ this.treeControl.expand(flatNode);
+ }
+ });
+ }
+
+ private findParentNode(treeData: TreeNode[], childId: string): TreeNode | null {
+ for (const node of treeData) {
+ if (node.children?.some((child) => child.id === childId)) {
+ return node;
+ }
+
+ if (node.children && node.children.length > 0) {
+ const parent = this.findParentNode(node.children, childId);
+ if (parent) {
+ return parent;
+ }
+ }
+ }
+ return null;
+ }
+
+ private findNodeByIdOrUuid(treeData: TreeNode[], identifier: string): TreeNode | null {
+ const search = (nodes: TreeNode[]): TreeNode | null => {
+ for (const node of nodes) {
+ if (node.id === identifier || node.uuid === identifier) return node;
+ if (node.children && node.children.length > 0) {
+ const found = search(node.children);
+ if (found) return found;
+ }
+ }
+ return null;
+ };
+ return search(treeData);
+ }
onNodeClick(node: TreeNode): void {
- console.log('Node clicked:', node);
this.selectedNode = node;
this.fetchClientsForNode(node);
}
private fetchClientsForNode(node: TreeNode): void {
- console.log('Node:', node);
this.isLoadingClients = true;
this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({
next: (response) => {
@@ -294,110 +318,82 @@ export class GroupsComponent implements OnInit, OnDestroy {
});
}
- getNodeIcon(node: TreeNode): string {
- switch (node.type) {
- 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: TreeNode | null = null): void {
event.stopPropagation();
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, {
data: { parent },
width: '900px',
});
- dialogRef.afterClosed().subscribe(() => {
- this.refreshOrganizationalUnits();
+ dialogRef.afterClosed().subscribe((newUnit) => {
+ if (newUnit?.uuid) {
+ console.log('Unidad organizativa creada:', newUnit);
+ this.refreshData(newUnit.uuid);
+ }
});
}
addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
event.stopPropagation();
+ const targetNode = organizationalUnit || this.selectedNode;
const dialogRef = this.dialog.open(CreateClientComponent, {
- data: { organizationalUnit },
+ data: { organizationalUnit: targetNode },
width: '900px',
});
- dialogRef.afterClosed().subscribe(() => {
- this.refreshOrganizationalUnits();
- if (organizationalUnit && organizationalUnit['@id']) {
- this.refreshClientsForNode(organizationalUnit);
+
+ dialogRef.afterClosed().subscribe((result) => {
+ if (result?.client && result?.organizationalUnit) {
+ const organizationalUnitUrl = result.organizationalUnit;
+ const uuid = organizationalUnitUrl.split('/')[2];
+ const parentNode = this.findNodeByIdOrUuid(this.treeDataSource.data, uuid);
+
+ if (parentNode) {
+ this.refreshData(parentNode.uuid);
+ }
}
});
}
addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
event.stopPropagation();
+ const targetNode = organizationalUnit || this.selectedNode;
+
const dialogRef = this.dialog.open(CreateMultipleClientComponent, {
- data: { organizationalUnit },
+ data: { organizationalUnit: targetNode },
width: '900px',
});
- dialogRef.afterClosed().subscribe(() => {
- this.refreshOrganizationalUnits();
- if (organizationalUnit && organizationalUnit['@id']) {
- this.refreshClientsForNode(organizationalUnit);
+ dialogRef.afterClosed().subscribe((result) => {
+ if (result?.success) {
+ const organizationalUnitUrl = result.organizationalUnit;
+ const uuid = organizationalUnitUrl.split('/')[2];
+ const parentNode = this.findNodeByIdOrUuid(this.treeDataSource.data, uuid);
+
+ if (parentNode) {
+ console.log('Nodo padre encontrado para actualización:', parentNode);
+ this.refreshData(parentNode.uuid);
+ } else {
+ console.error('No se encontró el nodo padre después de la creación masiva.');
+ }
}
});
}
- private refreshOrganizationalUnits(): void {
- const expandedNodeIds = this.treeControl.dataNodes
- ? this.treeControl.dataNodes
- .filter(node => this.treeControl.isExpanded(node))
- .map(node => this.extractUuid(node['@id']))
- : [];
-
- this.subscriptions.add(
- this.dataService.getOrganizationalUnits().subscribe(
- (data) => {
- this.organizationalUnits = data;
- if (this.selectedUnidad) {
- this.loadChildrenAndClients(this.selectedUnidad?.id || '').then((updatedData) => {
- this.selectedUnidad = updatedData;
- const treeData = this.convertToTreeData(updatedData);
- this.originalTreeData = treeData[0]?.children || [];
- this.treeDataSource.data = [...this.originalTreeData];
-
- setTimeout(() => {
- this.treeControl.dataNodes.forEach(node => {
- const nodeId = this.extractUuid(node['@id']);
- if (nodeId && expandedNodeIds.includes(nodeId)) {
- this.treeControl.expand(node);
- }
- });
- });
- });
- }
- },
- (error) => console.error('Error fetching organizational units', error)
- )
- );
- }
-
-
onEditNode(event: MouseEvent, node: TreeNode | null): void {
event.stopPropagation();
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' });
- }
+ const dialogRef = node?.type !== NodeType.Client
+ ? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
+ : this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
+
+ dialogRef.afterClosed().subscribe(() => {
+ if (node) {
+ this.refreshData(node.id);
+ }
+ });
}
- onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void {
+ onDeleteClick(event: MouseEvent, node: TreeNode | null): void {
event.stopPropagation();
const uuid = node ? this.extractUuid(node['@id']) : null;
if (!uuid) return;
@@ -410,44 +406,50 @@ export class GroupsComponent implements OnInit, OnDestroy {
dialogRef.afterClosed().subscribe((result) => {
if (result === true) {
- this.deleteEntity(uuid, node.type, node);
+ this.deleteEntityorClient(uuid, node?.type);
}
});
}
- 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 deleteEntityorClient(uuid: string, type: string): void {
+ if (!this.selectedNode) return;
- private refreshClientsForNode(node: TreeNode): void {
- if (!node['@id']) {
- this.selectedClients.data = [];
- return;
- }
- this.fetchClientsForNode(node);
+ const parentNode = this.selectedNode?.id
+ ? this.findParentNode(this.treeDataSource.data, this.selectedNode.id)
+ : null;
+
+ this.dataService.deleteElement(uuid, type).subscribe({
+ next: () => {
+ const entityType = type === NodeType.Client ? 'Cliente' : 'Entidad';
+ const verb = type === NodeType.Client ? 'eliminado' : 'eliminada';
+
+ this.toastr.success(`${entityType} ${verb} exitosamente`);
+
+ if (type === NodeType.Client) {
+ this.refreshData(this.selectedNode?.id);
+ } else if (parentNode) {
+ this.refreshData(parentNode.id);
+ } else {
+ this.refreshData();
+ }
+ },
+ error: (error) => {
+ console.error('Error deleting entity:', error);
+ const entityType = type === NodeType.Client ? 'cliente' : 'entidad';
+ this.toastr.error(`Error al eliminar el ${entityType}`);
+ },
+ });
}
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' });
- }
+ const dialogRef = type !== NodeType.Client
+ ? 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 {
@@ -467,22 +469,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
);
}
- fetchCommands(): void {
- this.commandsLoading = true;
- 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: Command, selectedNode: TreeNode | null): void {
if (!selectedNode) {
@@ -509,13 +495,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
}
}
- onTreeClick(event: MouseEvent, data: TreeNode): void {
- event.stopPropagation();
- if (data.type !== NodeType.Client) {
- this.dialog.open(TreeViewComponent, { data: { data }, width: '800px' });
- }
- }
-
openBottomSheet(): void {
this.bottomSheet.open(LegendComponent);
}
@@ -531,31 +510,54 @@ export class GroupsComponent implements OnInit, OnDestroy {
hasChild = (_: number, node: FlatNode): boolean => node.expandable;
isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable;
- filterTree(searchTerm: string, filterType: string): void {
- 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) : [];
+ filterTree(searchTerm: string): void {
+ const expandPaths: TreeNode[][] = [];
- if ((matchesName && matchesType) || filteredChildren.length > 0) {
- filteredNodes.push({ ...node, children: filteredChildren });
- }
- }
- return filteredNodes;
+ const filterNodes = (nodes: TreeNode[], parentPath: TreeNode[] = []): TreeNode[] => {
+ return nodes
+ .map((node) => {
+ const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase());
+ const filteredChildren = node.children ? filterNodes(node.children, [...parentPath, node]) : [];
+
+ if (matchesName) {
+ expandPaths.push([...parentPath, node]);
+ return {
+ ...node,
+ children: node.children,
+ } as TreeNode;
+ } else if (filteredChildren.length > 0) {
+ return {
+ ...node,
+ children: filteredChildren,
+ } as TreeNode;
+ }
+ return null;
+ })
+ .filter((node): node is TreeNode => node !== null);
};
- const filteredData = filterNodes(this.originalTreeData);
- this.treeDataSource.data = filteredData;
+ if (!searchTerm) {
+ this.treeDataSource.data = [...this.originalTreeData];
+ this.treeControl.collapseAll();
+ } else {
+ this.treeDataSource.data = filterNodes(this.originalTreeData);
+ expandPaths.forEach((path) => this.expandPath(path));
+ }
}
-
+ private expandPath(path: TreeNode[]): void {
+ path.forEach((pathNode) => {
+ const flatNode = this.treeControl.dataNodes?.find((n) => n.id === pathNode.id);
+ if (flatNode) {
+ this.treeControl.expand(flatNode);
+ }
+ });
+ }
onTreeFilterInput(event: Event): void {
const input = event.target as HTMLInputElement;
- const searchTerm = input?.value || '';
- this.filterTree(searchTerm, this.selectedTreeFilter);
+ const searchTerm = input?.value.trim() || '';
+ this.filterTree(searchTerm);
}
onClientFilterInput(event: Event): void {
@@ -569,7 +571,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.selectedClients.filter = this.searchTerm;
}
-
public setSelectedNode(node: TreeNode): void {
this.selectedNode = node;
}
@@ -586,19 +587,68 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.toastr.success('Cliente actualizado correctamente');
this.syncStatus = false;
this.syncingClientId = null;
- this.search()
+ this.refreshData()
},
() => {
this.toastr.error('Error de conexión con el cliente');
this.syncStatus = false;
this.syncingClientId = null;
- this.search()
+ this.refreshData()
}
)
);
}
+ isAllSelected() {
+ const numSelected = this.selection.selected.length;
+ const numRows = this.selectedClients.data.length;
+ return numSelected === numRows;
+ }
+
+ toggleAllRows() {
+ if (this.isAllSelected()) {
+ this.selection.clear();
+ this.arrayClients = []
+ return;
+ }
+
+ this.selection.select(...this.selectedClients.data);
+ 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('');
+ }
+
+ clearClientSearch(inputElement: HTMLInputElement): void {
+ inputElement.value = '';
+ this.filterClients('');
+ }
+
}
diff --git a/ogWebconsole/src/app/components/groups/model/model.ts b/ogWebconsole/src/app/components/groups/model/model.ts
index 0bada34..aaa5b3e 100644
--- a/ogWebconsole/src/app/components/groups/model/model.ts
+++ b/ogWebconsole/src/app/components/groups/model/model.ts
@@ -61,6 +61,7 @@ export interface ClientCollection {
export interface TreeNode {
id?: string
+ uuid?: string;
name: string;
type: string;
'@id'?: string;
diff --git a/ogWebconsole/src/app/components/groups/services/data.service.ts b/ogWebconsole/src/app/components/groups/services/data.service.ts
index 997c149..34b0bc3 100644
--- a/ogWebconsole/src/app/components/groups/services/data.service.ts
+++ b/ogWebconsole/src/app/components/groups/services/data.service.ts
@@ -231,5 +231,15 @@ export class DataService {
);
}
+ getOrganizationalUnitPath(unit: UnidadOrganizativa, units: UnidadOrganizativa[]): string {
+ const path: string[] = [];
+ let currentUnit: UnidadOrganizativa | undefined = unit;
+ while (currentUnit) {
+ path.unshift(currentUnit.name);
+ currentUnit = units.find(u => u['@id'] === currentUnit?.parent?.['@id']);
+ }
+
+ return path.join(' / ');
+ }
}
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 84e406d..3ef6251 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
@@ -6,9 +6,12 @@
Padre
-
+
+ {{ getSelectedParentName() }}
+
+
{{ unit.name }}
- {{ unit.path }}
+ {{ unit.path }}
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 138f0ad..514893b 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
@@ -15,6 +15,7 @@ export class CreateClientComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
clientForm!: FormGroup;
parentUnits: any[] = [];
+ parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
hardwareProfiles: any[] = [];
ogLives: any[] = [];
menus: any[] = [];
@@ -80,6 +81,11 @@ export class CreateClientComponent implements OnInit {
this.http.get(url).subscribe(
response => {
this.parentUnits = response['hydra:member'];
+ this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
+ id: unit['@id'],
+ name: unit.name,
+ path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits)
+ }));
this.loading = false;
},
error => {
@@ -89,6 +95,11 @@ export class CreateClientComponent implements OnInit {
);
}
+ getSelectedParentName(): string | undefined {
+ const parentId = this.clientForm.get('organizationalUnit')?.value;
+ return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
+ }
+
loadHardwareProfiles(): void {
this.dataService.getHardwareProfiles().subscribe(
(data: any[]) => {
@@ -157,15 +168,21 @@ export class CreateClientComponent implements OnInit {
onSubmit(): void {
if (this.clientForm.valid) {
const formData = this.clientForm.value;
+
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
- response => {
+ (response) => {
this.toastService.success('Cliente creado exitosamente', 'Éxito');
- this.dialogRef.close(response);
+ this.dialogRef.close({
+ client: response,
+ organizationalUnit: formData.organizationalUnit,
+ });
},
- error => {
- this.toastService.error('Error al crear el cliente', 'Error');
+ (error) => {
+ this.toastService.error(error.error['hydra:description'], 'Error al crear el cliente');
}
);
+ } else {
+ this.toastService.error('Formulario inválido. Por favor, revise los campos obligatorios.', 'Error');
}
}
diff --git a/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.html
index fd678aa..1b34d9c 100644
--- a/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.html
+++ b/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.html
@@ -6,9 +6,13 @@