develop #51

Merged
maranda merged 2 commits from develop into main 2025-10-21 16:10:51 +02:00
12 changed files with 699 additions and 13 deletions

View File

@ -1,4 +1,17 @@
# Changelog
## [1.2.0] - 2025-10-21
### Added
- Se ha añadido una nueva funcionalidad la cual permite poder borrar varios equipos a la vez.
### Improved
- Ahora no se pueden añadir equipos si no hay OUs creadas en el sistema.
- Ahora aparecen los logs del cliente en tiempo real en estados mas alla de "og-live".
- Se ha añadido el campo "mac" en el ogbrowser
### Fixed
- Se ha corregido un error de sintaxis a la hora de cargar los iframe del ogLog.
---
## [1.1.0] - 2025-10-17
### Added
- Se han incorporado opciones para poder seleccionar tamaños de pantalla para visualizar en los menu ogBrowser

View File

@ -36,6 +36,7 @@ import { MatDividerModule } from '@angular/material/divider';
import { MatStepperModule } from '@angular/material/stepper';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { DeleteModalComponent } from './shared/delete_modal/delete-modal/delete-modal.component';
import { DeleteResultsModalComponent } from './components/groups/shared/delete-results-modal/delete-results-modal.component';
import { ClassroomViewComponent } from './components/groups/shared/classroom-view/classroom-view.component';
import { MatProgressSpinner } from "@angular/material/progress-spinner";
import { MatProgressBarModule } from '@angular/material/progress-bar';
@ -198,6 +199,7 @@ registerLocaleData(localeEs, 'es-ES');
GroupsComponent,
ManageClientComponent,
DeleteModalComponent,
DeleteResultsModalComponent,
QueueConfirmationModalComponent,
ClassroomViewComponent,
ClientViewComponent,

View File

@ -1742,3 +1742,27 @@ mat-button-toggle-group {
}
}
.delete-button {
background-color: #f44336 !important;
color: white !important;
display: flex;
align-items: center;
gap: 8px;
}
.delete-button:hover:not(:disabled) {
background-color: #d32f2f !important;
}
.delete-button:disabled {
background-color: #ffcdd2 !important;
color: #9e9e9e !important;
cursor: not-allowed;
}
.delete-button mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}

View File

@ -20,11 +20,14 @@
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
{{ 'newOrganizationalUnitButton' | translate }}
</button>
<button class="action-button" [matMenuTriggerFor]="menuClients" *ngIf="auth.userCategory !== 'ou-minimal'">{{
<button class="action-button" [matMenuTriggerFor]="menuClients" *ngIf="auth.userCategory !== 'ou-minimal'"
[disabled]="!hasOrganizationalUnits()"
matTooltip="{{ hasOrganizationalUnits() ? ('newClientButtonTooltip' | translate) : ('noOrganizationalUnitsTooltip' | translate) }}"
matTooltipShowDelay="1000">{{
'newClientButton' | translate }}</button>
<mat-menu #menuClients="matMenu">
<button mat-menu-item (click)="addClient($event)">{{ 'newSingleClientButton' | translate }}</button>
<button mat-menu-item (click)="addMultipleClients($event)">{{ 'newMultipleClientButton' | translate
<button mat-menu-item (click)="addClient($event)" [disabled]="!hasOrganizationalUnits()">{{ 'newSingleClientButton' | translate }}</button>
<button mat-menu-item (click)="addMultipleClients($event)" [disabled]="!hasOrganizationalUnits()">{{ 'newMultipleClientButton' | translate
}}</button>
</mat-menu>
</div>
@ -43,10 +46,10 @@
<button mat-menu-item (click)="addOU($event)">
{{ 'newOrganizationalUnitButton' | translate }}
</button>
<button mat-menu-item (click)="addClient($event)">
<button mat-menu-item (click)="addClient($event)" [disabled]="!hasOrganizationalUnits()">
{{ 'newSingleClientButton' | translate }}
</button>
<button mat-menu-item (click)="addMultipleClients($event)">
<button mat-menu-item (click)="addMultipleClients($event)" [disabled]="!hasOrganizationalUnits()">
{{ 'newMultipleClientButton' | translate }}
</button>
<button mat-menu-item (click)="openBottomSheet()">
@ -243,11 +246,11 @@
<mat-icon>map</mat-icon>
<span>{{ 'roomMap' | translate }}</span>
</button>
<button mat-menu-item (click)="addClient($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'">
<button mat-menu-item (click)="addClient($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'" [disabled]="!hasOrganizationalUnits()">
<mat-icon>add</mat-icon>
<span>{{ 'newSingleClientButton' | translate }}</span>
</button>
<button mat-menu-item (click)="addMultipleClients($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'">
<button mat-menu-item (click)="addMultipleClients($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'" [disabled]="!hasOrganizationalUnits()">
<mat-icon>playlist_add</mat-icon>
<span>{{ 'newMultipleClientButton' | translate }}</span>
</button>
@ -295,11 +298,16 @@
matTooltip="{{ 'moveClientsTooltip' | translate }}" matTooltipShowDelay="1000">
{{ 'changeOU' | translate }}
</button>
<div joyrideStep="executeCommandStep" text="{{ 'executeCommandStepText' | translate }}">
<app-execute-command [clientData]="selection.selected" [buttonType]="'text'"
[buttonText]="'ejecutarComandos' | translate" [disabled]="selection.selected.length === 0"
[runScriptContext]="getRunScriptContext(selection.selected)"></app-execute-command>
</div>
<button class="action-button delete-button" [disabled]="selection.selected.length === 0" (click)="deleteMultipleClients($event)"
matTooltip="{{ 'deleteMultipleClientsTooltip' | translate }}" matTooltipShowDelay="1000">
<mat-icon>delete</mat-icon>
</button>
<mat-button-toggle-group name="viewType" aria-label="View Type" [hideSingleSelectionIndicator]="true"
(change)="toggleView($event.value)" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
@ -418,7 +426,7 @@
<mat-icon>pending_actions</mat-icon>
<span>{{ 'colaAcciones' | translate }}</span>
</button>
<button mat-menu-item *ngIf="client.status === 'og-live'" (click)="openClientLogsInNewTab($event, client)">
<button mat-menu-item (click)="openClientLogsInNewTab($event, client)">
<mat-icon>article</mat-icon>
<span>Logs en tiempo real</span>
</button>

View File

@ -32,6 +32,7 @@ import {ChangeParentComponent} from "./shared/change-parent/change-parent.compon
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';
import { DeleteResultsModalComponent } from './shared/delete-results-modal/delete-results-modal.component';
enum NodeType {
OrganizationalUnit = 'organizational-unit',
@ -1228,6 +1229,10 @@ export class GroupsComponent implements OnInit, OnDestroy {
return total;
}
hasOrganizationalUnits(): boolean {
return this.originalTreeData && this.originalTreeData.length > 0;
}
getTotalClassrooms(): number {
let total = 0;
const countClassrooms = (nodes: TreeNode[]) => {
@ -1244,10 +1249,69 @@ export class GroupsComponent implements OnInit, OnDestroy {
return total;
}
// Función para actualizar estadísticas cuando cambian los datos
private updateDashboardStats(): void {
// Las estadísticas de equipos ya se calculan en calculateLocalStats()
// Solo necesitamos asegurar que se actualicen cuando cambian los datos
this.calculateLocalStats();
}
deleteMultipleClients(event: MouseEvent): void {
event.stopPropagation();
if (this.selection.selected.length === 0) {
this.toastr.warning('No hay clientes seleccionados para eliminar');
return;
}
const selectedClients = this.selection.selected;
const clientNames = selectedClients.map(client => client.name).join(', ');
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '500px',
data: {
name: `${selectedClients.length} cliente${selectedClients.length > 1 ? 's' : ''}`,
message: `¿Está seguro de que desea eliminar los siguientes clientes?\n\n${clientNames}`,
isMultiple: true
},
});
dialogRef.afterClosed().subscribe((result) => {
if (result === true) {
this.performMultipleDeletion(selectedClients);
}
});
}
private performMultipleDeletion(selectedClients: any[]): void {
this.loading = true;
const clientUuids = selectedClients.map((client: any) => client['@id']);
this.dataService.deleteMultipleClients(clientUuids).subscribe({
next: (response) => {
this.loading = false;
this.showDeletionResults(response, selectedClients);
this.refreshData(this.selectedNode?.id);
this.selection.clear();
},
error: (error) => {
this.loading = false;
console.error('Error deleting multiple clients:', error);
this.toastr.error('Error al eliminar los clientes seleccionados');
}
});
}
private showDeletionResults(response: any, selectedClients: any[]): void {
const dialogRef = this.dialog.open(DeleteResultsModalComponent, {
width: '600px',
data: {
response: response,
selectedClients: selectedClients,
totalSelected: selectedClients.length
}
});
dialogRef.afterClosed().subscribe(() => {
});
}
}

View File

@ -246,4 +246,18 @@ export class DataService {
return path.join(' / ');
}
deleteMultipleClients(clientUuids: string[]): Observable<any> {
const url = `${this.baseUrl}/clients/delete-multiple`;
const body = {
clients: clientUuids
};
return this.http.post<any>(url, body).pipe(
catchError(error => {
console.error('Error deleting multiple clients', error);
return throwError(error);
})
);
}
}

View File

@ -20,7 +20,7 @@
<div class="iframe-container">
<iframe
[src]="iframeUrl | safe"
[src]="iframeUrl"
width="1200"
height="800"
frameborder="0"

View File

@ -0,0 +1,394 @@
.delete-results-container {
max-width: 100%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px 16px 24px;
border-bottom: 1px solid #e0e0e0;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
}
.modal-header h2 {
margin: 0;
display: flex;
align-items: center;
gap: 12px;
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
}
.header-icon {
font-size: 28px;
width: 28px;
height: 28px;
}
.success-icon {
color: #28a745;
}
.warning-icon {
color: #ffc107;
}
.error-icon {
color: #dc3545;
}
.close-button {
color: #6c757d;
transition: all 0.3s ease;
}
.close-button:hover {
color: #dc3545;
background-color: rgba(220, 53, 69, 0.1);
transform: scale(1.1);
}
.modal-content {
padding: 24px;
max-height: 60vh;
overflow-y: auto;
}
/* Resumen general */
.summary-section {
margin-bottom: 24px;
}
.summary-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
border-radius: 12px;
border: 2px solid;
transition: all 0.3s ease;
}
.success-card {
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
border-color: #28a745;
}
.warning-card {
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
border-color: #ffc107;
}
.error-card {
background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
border-color: #dc3545;
}
.summary-icon {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.summary-icon mat-icon {
font-size: 32px;
width: 32px;
height: 32px;
}
.success-card .summary-icon mat-icon {
color: #28a745;
}
.warning-card .summary-icon mat-icon {
color: #ffc107;
}
.error-card .summary-icon mat-icon {
color: #dc3545;
}
.summary-content h3 {
margin: 0 0 8px 0;
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
}
.summary-content p {
margin: 0;
color: #6c757d;
font-size: 1rem;
line-height: 1.5;
}
/* Estadísticas */
.stats-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid;
transition: all 0.3s ease;
}
.stat-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.success-stat {
border-left-color: #28a745;
}
.error-stat {
border-left-color: #dc3545;
}
.total-stat {
border-left-color: #6c757d;
}
.stat-item mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
.success-stat mat-icon {
color: #28a745;
}
.error-stat mat-icon {
color: #dc3545;
}
.total-stat mat-icon {
color: #6c757d;
}
.stat-label {
flex: 1;
font-weight: 500;
color: #495057;
}
.stat-value {
font-size: 1.25rem;
font-weight: 700;
color: #2c3e50;
}
/* Listas de clientes */
.success-list, .error-list {
margin-bottom: 20px;
}
.success-list h4, .error-list h4 {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 16px 0;
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
}
.success-list h4 mat-icon {
color: #28a745;
}
.error-list h4 mat-icon {
color: #dc3545;
}
.client-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #e9ecef;
border-radius: 8px;
background: #ffffff;
}
.client-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid #f1f3f4;
transition: background-color 0.2s ease;
}
.client-item:last-child {
border-bottom: none;
}
.client-item:hover {
background-color: #f8f9fa;
}
.client-item mat-icon {
color: #6c757d;
font-size: 18px;
width: 18px;
height: 18px;
}
.success-item mat-icon {
color: #28a745;
}
.client-name {
flex: 1;
font-weight: 500;
color: #2c3e50;
}
.client-ip {
color: #6c757d;
font-size: 0.9rem;
font-family: 'Courier New', monospace;
}
/* Errores */
.error-details {
max-height: 200px;
overflow-y: auto;
border: 1px solid #e9ecef;
border-radius: 8px;
background: #ffffff;
}
.error-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid #f1f3f4;
}
.error-item:last-child {
border-bottom: none;
}
.error-item mat-icon {
color: #dc3545;
font-size: 18px;
width: 18px;
height: 18px;
margin-top: 2px;
}
.error-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.error-message {
color: #dc3545;
font-weight: 500;
}
.error-client {
color: #6c757d;
font-size: 0.9rem;
font-style: italic;
}
/* Acciones */
.modal-actions {
display: flex;
justify-content: flex-end;
padding: 16px 24px;
border-top: 1px solid #e0e0e0;
background: #f8f9fa;
}
.close-btn {
display: flex;
align-items: center;
gap: 8px;
color: #6c757d;
font-weight: 500;
transition: all 0.3s ease;
}
.close-btn:hover {
color: #dc3545;
background-color: rgba(220, 53, 69, 0.1);
}
/* Responsive */
@media (max-width: 768px) {
.modal-header {
padding: 16px 20px 12px 20px;
}
.modal-header h2 {
font-size: 1.25rem;
}
.modal-content {
padding: 20px;
}
.stats-section {
grid-template-columns: 1fr;
gap: 12px;
}
.summary-card {
flex-direction: column;
text-align: center;
gap: 12px;
}
.summary-icon {
width: 50px;
height: 50px;
}
.summary-icon mat-icon {
font-size: 28px;
width: 28px;
height: 28px;
}
}
@media (max-width: 480px) {
.modal-header {
padding: 12px 16px 8px 16px;
}
.modal-content {
padding: 16px;
}
.client-item, .error-item {
padding: 10px 12px;
}
.modal-actions {
padding: 12px 16px;
}
}

View File

@ -0,0 +1,74 @@
<div class="delete-results-container">
<div class="modal-header">
<h2 mat-dialog-title>
<mat-icon class="header-icon" [ngClass]="{
'success-icon': isCompleteSuccess,
'warning-icon': isPartialSuccess,
'error-icon': isCompleteFailure
}">
{{ isCompleteSuccess ? 'check_circle' : isPartialSuccess ? 'warning' : 'error' }}
</mat-icon>
Resultados de Eliminación
</h2>
<button mat-icon-button (click)="onClose()" class="close-button">
<mat-icon>close</mat-icon>
</button>
</div>
<div mat-dialog-content class="modal-content">
<!-- Resumen general -->
<div class="summary-section">
<div class="summary-card" [ngClass]="{
'success-card': isCompleteSuccess,
'warning-card': isPartialSuccess,
'error-card': isCompleteFailure
}">
<div class="summary-icon">
<mat-icon>{{ isCompleteSuccess ? 'check_circle' : isPartialSuccess ? 'warning' : 'error' }}</mat-icon>
</div>
<div class="summary-content">
<h3>{{ getSummaryTitle() }}</h3>
<p>{{ getSummaryMessage() }}</p>
</div>
</div>
</div>
<!-- Lista de clientes eliminados exitosamente -->
<div class="success-list" *ngIf="successCount > 0">
<h4>
Clientes Eliminados ({{ successCount }})
</h4>
<div class="client-list">
<div class="client-item success-item" *ngFor="let client of getSuccessfullyDeletedClients()">
<mat-icon>computer</mat-icon>
<span class="client-name">{{ client.name }}</span>
<span class="client-ip">{{ client.ip }}</span>
</div>
</div>
</div>
<!-- Lista de errores si los hay -->
<div class="error-list" *ngIf="hasErrors">
<h4>
<mat-icon>error</mat-icon>
Errores Encontrados ({{ errorCount }})
</h4>
<div class="error-details">
<div class="error-item" *ngFor="let error of data.response.errors">
<mat-icon>warning</mat-icon>
<div class="error-content">
<span class="error-message">{{ error.message || 'Error desconocido' }}</span>
<span class="error-client" *ngIf="error.client">Cliente: {{ error.client }}</span>
</div>
</div>
</div>
</div>
</div>
<div mat-dialog-actions class="modal-actions">
<button mat-button (click)="onClose()" class="close-btn">
<mat-icon>close</mat-icon>
Cerrar
</button>
</div>
</div>

View File

@ -0,0 +1,85 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
export interface DeleteResultsData {
response: {
deleted: number;
total: number;
success: boolean;
errors?: any[];
};
selectedClients: any[];
totalSelected: number;
}
@Component({
selector: 'app-delete-results-modal',
templateUrl: './delete-results-modal.component.html',
styleUrls: ['./delete-results-modal.component.css']
})
export class DeleteResultsModalComponent {
constructor(
public dialogRef: MatDialogRef<DeleteResultsModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: DeleteResultsData
) {}
onClose(): void {
this.dialogRef.close();
}
get successCount(): number {
return this.data.response.deleted;
}
get errorCount(): number {
return this.data.totalSelected - this.data.response.deleted;
}
get hasErrors(): boolean {
return !!(this.data.response.errors && this.data.response.errors.length > 0);
}
get isPartialSuccess(): boolean {
return this.successCount > 0 && this.errorCount > 0;
}
get isCompleteSuccess(): boolean {
return this.successCount === this.data.totalSelected && this.errorCount === 0;
}
get isCompleteFailure(): boolean {
return this.successCount === 0 && this.errorCount === this.data.totalSelected;
}
getSummaryTitle(): string {
if (this.isCompleteSuccess) {
return 'Eliminación Completada';
} else if (this.isPartialSuccess) {
return 'Eliminación Parcial';
} else {
return 'Error en Eliminación';
}
}
getSummaryMessage(): string {
if (this.isCompleteSuccess) {
return `Se eliminaron exitosamente todos los ${this.data.totalSelected} cliente${this.data.totalSelected > 1 ? 's' : ''} seleccionado${this.data.totalSelected > 1 ? 's' : ''}.`;
} else if (this.isPartialSuccess) {
return `Se eliminaron ${this.successCount} de ${this.data.totalSelected} cliente${this.data.totalSelected > 1 ? 's' : ''} seleccionado${this.data.totalSelected > 1 ? 's' : ''}. ${this.errorCount} cliente${this.errorCount > 1 ? 's' : ''} no se pudieron eliminar.`;
} else {
return `No se pudo eliminar ninguno de los ${this.data.totalSelected} cliente${this.data.totalSelected > 1 ? 's' : ''} seleccionado${this.data.totalSelected > 1 ? 's' : ''}.`;
}
}
getSuccessfullyDeletedClients(): any[] {
// En una implementación real, esto debería comparar con la respuesta del servidor
// Por ahora, asumimos que todos los clientes seleccionados fueron eliminados exitosamente
// si no hay errores específicos
if (this.data.response.errors && this.data.response.errors.length > 0) {
// Si hay errores, necesitaríamos lógica más compleja para determinar cuáles se eliminaron
// Por simplicidad, mostramos todos los seleccionados
return this.data.selectedClients.slice(0, this.successCount);
}
return this.data.selectedClients;
}
}

View File

@ -173,6 +173,8 @@
"newClientButton": "New Client",
"newSingleClientButton": "Add single client",
"newMultipleClientButton": "Add numerous clients",
"newClientButtonTooltip": "Create a new client",
"noOrganizationalUnitsTooltip": "You must create at least one organizational unit before adding clients",
"keyStepText": "The legend will show you the types of organizational units and their corresponding icons",
"legendButton": "Legend",
"unitStepText": "This is the section where 'Center' type organizational units will be displayed",
@ -519,6 +521,8 @@
},
"changeOU": "Change Organizational Unit",
"moveClientsTooltip": "Move clients to another organizational unit",
"deleteMultipleClients": "Delete selected",
"deleteMultipleClientsTooltip": "Delete all selected clients",
"remoteAccess": "Remote access available",
"noRemoteAccess": "Remote access not available",
"capacityWarning": "The capacity cannot be negative",

View File

@ -173,6 +173,8 @@
"newClientButton": "Nuevo Cliente",
"newSingleClientButton": "Añadir cliente unitario",
"newMultipleClientButton": "Añadir clientes masivamente",
"newClientButtonTooltip": "Crear un nuevo cliente",
"noOrganizationalUnitsTooltip": "Debe crear al menos una unidad organizativa antes de añadir clientes",
"keyStepText": "La leyenda te mostrará los tipos de unidades organizativas y sus iconos correspondientes",
"legendButton": "Leyenda",
"unitStepText": "Esta es la sección donde se mostrarán las unidades organizativas de tipo 'Centro'",
@ -518,8 +520,10 @@
"hardwareInventory": "Inventario Hardware",
"runScript": "Ejecutar script"
},
"changeOU": "Mover clientes",
"changeOU": "Mover",
"moveClientsTooltip": "Mover clientes a otra unidad organizativa",
"deleteMultipleClients": "Eliminar seleccionados",
"deleteMultipleClientsTooltip": "Eliminar todos los clientes seleccionados",
"remoteAccess": "Disponible acceso remoto",
"noRemoteAccess": "No disponible acceso remoto",
"capacityWarning": "El aforo no puede ser",