refs #1354. Added multiple commands logic. Updated endpoints
testing/ogGui-multibranch/pipeline/head Something is wrong with the build of this commit Details

pull/12/head
Manuel Aranda Rosales 2025-01-27 15:57:32 +01:00
parent 3a7bff9e74
commit b516103008
18 changed files with 420 additions and 186 deletions

View File

@ -63,10 +63,10 @@ const routes: Routes = [
{ path: 'commands-task', component: CommandsTaskComponent },
{ path: 'commands-logs', component: TaskLogsComponent },
{ path: 'calendars', component: CalendarComponent },
{ path: 'clients/deploy-image', component: DeployImageComponent },
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent },
{ path: 'clients/:id', component: ClientMainViewComponent },
{ path: 'clients/:id/partition-assistant', component: PartitionAssistantComponent },
{ path: 'clients/:id/create-image', component: CreateImageComponent },
{ path: 'clients/:id/deploy-image', component: DeployImageComponent },
{ path: 'images', component: ImagesComponent },
{ path: 'repositories', component: RepositoriesComponent },
{ path: 'repository/:id', component: MainRepositoryViewComponent },

View File

@ -90,3 +90,10 @@ table {
color: white;
}
.header-container-title {
flex-grow: 1;
text-align: left;
padding-left: 1em;
}

View File

@ -2,7 +2,11 @@
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' | translate }}</h2>
<div class="header-container-title">
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' | translate }}</h2>
</div>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="resetFiltersStep" text="{{ 'resetFiltersStepText' | translate }}">
{{ 'resetFilters' | translate }}

View File

@ -1,10 +1,20 @@
<button mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu">
<mat-icon>terminal</mat-icon>
</button>
<ng-container [ngSwitch]="buttonType">
<button *ngSwitchCase="'icon'" mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu">
<mat-icon>{{ icon }}</mat-icon>
</button>
<mat-menu #commandMenu="matMenu">
<button mat-menu-item [disabled]="command.disabled" *ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)">
<button mat-flat-button [disabled]="clientData.length === 0" *ngSwitchCase="'text'" mat-button color="primary" [matMenuTriggerFor]="commandMenu">
{{ buttonText }}
</button>
</ng-container>
<mat-menu #commandMenu="matMenu" >
<button
mat-menu-item
[disabled]="command.disabled || (command.slug === 'create-image' && clientData.length > 1)"
*ngFor="let command of arrayCommands"
(click)="onCommandSelect(command.slug)"
>
{{ command.name }}
</button>
</mat-menu>

View File

@ -1,4 +1,4 @@
import {Component, Inject, Input, OnInit} from '@angular/core';
import {Component, Inject, Input, OnInit, SimpleChanges} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup } from '@angular/forms';
@ -11,7 +11,10 @@ import {ToastrService} from "ngx-toastr";
styleUrls: ['./execute-command.component.css']
})
export class ExecuteCommandComponent implements OnInit {
@Input() clientData: any = {};
@Input() clientData: any[] = [];
@Input() buttonType: 'icon' | 'text' = 'icon';
@Input() buttonText: string = 'Ejecutar Comandos';
@Input() icon: string = 'terminal';
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
loading: boolean = true;
@ -29,6 +32,8 @@ export class ExecuteCommandComponent implements OnInit {
{name: 'Ejecutar script', slug: 'run-script', disabled: true},
];
client: any = {};
constructor(
private dialog: MatDialog,
private http: HttpClient,
@ -39,20 +44,14 @@ export class ExecuteCommandComponent implements OnInit {
}
ngOnInit(): void {
this.clientData = this.clientData || {};
this.loadClient(this.clientData)
this.clientData = this.clientData || [];
}
loadClient = (uuid: string) => {
this.http.get<any>(`${this.baseUrl}${uuid}`).subscribe({
next: data => {
this.clientData = data;
this.loading = false;
},
error: error => {
console.error('Error al obtener el cliente:', error);
}
});
ngOnChanges(changes: SimpleChanges): void {
if (changes['clientData']) {
console.log(this.clientData.length)
console.log('clientData ha cambiado:', changes['clientData'].currentValue);
}
}
onCommandSelect(action: any): void {
@ -82,7 +81,9 @@ export class ExecuteCommandComponent implements OnInit {
}
rebootClient(): void {
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/reboot`, {}).subscribe(
this.http.post(`${this.baseUrl}/clients/server/reboot`, {
clients: this.clientData.map((client: any) => client['@id'])
}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
},
@ -94,10 +95,10 @@ export class ExecuteCommandComponent implements OnInit {
powerOnClient(): void {
const payload = {
client: this.clientData['@id']
client: ''
}
this.http.post(`${this.baseUrl}${this.clientData.repository['@id']}/wol`, payload).subscribe(
this.http.post('', payload).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
},
@ -108,7 +109,9 @@ export class ExecuteCommandComponent implements OnInit {
}
powerOffClient(): void {
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/power-off`, {}).subscribe(
this.http.post(`${this.baseUrl}/clients/server/power-off`, {
clients: this.clientData.map((client: any) => client['@id'])
}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
},
@ -119,21 +122,24 @@ export class ExecuteCommandComponent implements OnInit {
}
openPartitionAssistant(): void {
this.router.navigate([`/clients/${this.clientData.uuid}/partition-assistant`]).then(r => {
console.log('navigated', r);
this.router.navigate(['/clients/partition-assistant'], {
state: { clientData: this.clientData },
}).then(r => {
console.log('Navigated to partition assistant with data:', this.clientData);
});
}
openCreateImageAssistant(): void {
this.router.navigate([`/clients/${this.clientData.uuid}/create-image`]).then(r => {
this.router.navigate([`/clients/${this.clientData[0].uuid}/create-image`]).then(r => {
console.log('navigated', r);
});
}
openDeployImageAssistant(): void {
this.router.navigate([`/clients/${this.clientData.uuid}/deploy-image`]).then(r => {
console.log('navigated', r);
this.router.navigate(['/clients/deploy-image'], {
state: { clientData: this.clientData },
}).then(r => {
console.log('Navigated to deploy image with data:', this.clientData);
});
}
}

View File

@ -302,7 +302,7 @@ export class ClientMainViewComponent implements OnInit {
}
openDeployImageAssistant(): void {
this.router.navigate([`/clients/${this.clientData.uuid}/deploy-image`]).then(r => {
this.router.navigate([`/clients/deploy-image`]).then(r => {
console.log('navigated', r);
});
}

View File

@ -1,8 +1,11 @@
<div class="header-container">
<button mat-flat-button color="primary" (click)="back()">Volver</button>
<h2 class="title" i18n="@@subnetsTitle">Crear Imagen desde {{ clientName }}</h2>
<div class="header-container-title">
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
Crear imagen desde {{ clientName }}
</h2>
</div>
<div class="subnets-button-row">
<button mat-flat-button color="primary" (click)="save()">Guardar y ejecutar</button>
<button mat-flat-button color="primary" (click)="save()">Ejecutar</button>
</div>
</div>
<mat-divider></mat-divider>
@ -12,14 +15,6 @@
<mat-label>Nombre canónico</mat-label>
<input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione imagen creada previamente</mat-label>
<mat-select [(ngModel)]="selectedImage">
<mat-option>--</mat-option>
<mat-option *ngFor="let image of images" [value]="image['@id']">{{ image.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">

View File

@ -22,38 +22,42 @@ import {MatFormField, MatLabel} from "@angular/material/form-field";
import {MatOption} from "@angular/material/autocomplete";
import {MatSelect} from "@angular/material/select";
import {MatInput} from "@angular/material/input";
import {JoyrideModule} from "ngx-joyride";
import {TranslatePipe} from "@ngx-translate/core";
@Component({
selector: 'app-create-image',
templateUrl: './create-image.component.html',
standalone: true,
imports: [
MatButton,
MatDivider,
NgForOf,
NgIf,
ReactiveFormsModule,
MatTable,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
MatCell,
MatCellDef,
MatChip,
MatHeaderRow,
MatRow,
MatHeaderRowDef,
MatRowDef,
MatCheckbox,
MatRadioGroup,
MatRadioButton,
MatFormField,
MatLabel,
MatOption,
MatSelect,
MatInput,
FormsModule
],
imports: [
MatButton,
MatDivider,
NgForOf,
NgIf,
ReactiveFormsModule,
MatTable,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
MatCell,
MatCellDef,
MatChip,
MatHeaderRow,
MatRow,
MatHeaderRowDef,
MatRowDef,
MatCheckbox,
MatRadioGroup,
MatRadioButton,
MatFormField,
MatLabel,
MatOption,
MatSelect,
MatInput,
FormsModule,
JoyrideModule,
TranslatePipe
],
styleUrl: './create-image.component.css'
})
export class CreateImageComponent {
@ -65,7 +69,6 @@ export class CreateImageComponent {
partitions: any[] = [];
images: any[] = [];
clientName: string = '';
selectedImage: string | null = null;
selectedPartition: any = null;
name: string = '';
client: any = null;
@ -147,15 +150,10 @@ export class CreateImageComponent {
);
}
back() {
this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
}
save(): void {
const payload = {
client: `/clients/${this.clientId}`,
name: this.name,
image: this.selectedImage,
partition: this.selectedPartition['@id'],
source: 'assistant'
};
@ -168,7 +166,6 @@ export class CreateImageComponent {
this.router.navigate(['/images']);
},
error: (error) => {
console.error('Error:', error);
this.toastService.error(error.error['hydra:description']);
}
}

View File

@ -37,6 +37,7 @@ table {
width: 100%;
padding: 0 5px;
box-sizing: border-box;
padding-left: 1em;
}
.input-group {
@ -70,7 +71,6 @@ table {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
.mat-elevation-z8 {
@ -82,3 +82,47 @@ table {
justify-content: end;
margin-bottom: 30px;
}
.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;
}

View File

@ -1,12 +1,43 @@
<div class="header-container">
<button mat-flat-button color="primary" (click)="back()">Volver</button>
<h2 class="title" i18n="@@subnetsTitle">Desplegar imagen en {{ clientName }}</h2>
<div class="header-container-title">
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
Despliegue de imagen
</h2>
</div>
<div class="subnets-button-row">
<button mat-flat-button color="primary" (click)="save()">Guardar</button>
<button mat-flat-button color="primary" (click)="save()">Ejecutar</button>
</div>
</div>
<mat-divider></mat-divider>
<div class="select-container">
<mat-expansion-panel hideToggle>
<mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title>
<mat-panel-description> Listado de clientes donde se desplegará la imagen </mat-panel-description>
</mat-expansion-panel-header>
<div class="clients-grid" >
<div *ngFor="let client of clientData" class="client-item">
<div class="client-card">
<img
[src]="'assets/images/ordenador_' + client.status + '.png'"
alt="Client Icon"
class="client-image" />
<div class="client-details">
<span class="client-name">{{ client.name }}</span>
<span class="client-ip">{{ client.ip }}</span>
<span class="client-ip">{{ client.mac }}</span>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
</div>
<mat-divider style="margin-top: 20px;"></mat-divider>
<div class="select-container">
<div class="option-container">
<mat-radio-group [(ngModel)]="selectedOption" name="selectedOption" aria-label="Selecciona una opcion">
@ -84,12 +115,12 @@
<mat-form-field appearance="fill" class="input-field">
<mat-label>Máximo Clientes</mat-label>
<input matInput [(ngModel)]="mcastMaxClients" name="mcastMaxClients">
<input matInput [(ngModel)]="mcastMaxClients" name="mcastMaxClients" type="number">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Tiempo Máximo de Espera</mat-label>
<input matInput [(ngModel)]="mcastMaxTime" name="mcastMaxTime">
<input matInput [(ngModel)]="mcastMaxTime" name="mcastMaxTime" type="number">
</mat-form-field>
</div>

View File

@ -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,6 +33,7 @@ export class DeployImageComponent {
p2pTime: Number = 0;
name: string = '';
client: any = null;
clientData: any = [];
protected p2pModeOptions = [
{ name: 'Leecher', value: 'p2p-mode-leecher' },
@ -47,7 +48,6 @@ export class DeployImageComponent {
allMethods = [
'uftp',
'udpcast',
'multicast-direct',
'unicast',
'unicast-direct',
'p2p'
@ -56,7 +56,6 @@ export class DeployImageComponent {
updateCacheMethods = [
'uftp',
'udpcast',
'multicast',
'unicast',
'p2p'
];
@ -98,13 +97,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 +114,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,10 +149,6 @@ export class DeployImageComponent {
);
}
back() {
this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
}
save(): void {
if (!this.selectedImage) {
this.toastService.error('Debe seleccionar una imagen');
@ -171,26 +165,42 @@ 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.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
});
}
}
);

View File

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

View File

@ -1,12 +1,43 @@
<div class="header-container">
<button mat-flat-button color="primary" (click)="back()">Volver</button>
<h2 class="title" i18n="@@subnetsTitle">Asistente de particionado</h2>
<div class="header-container-title">
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
Asistente de particionado
</h2>
</div>
<div class="subnets-button-row">
<button mat-flat-button color="primary" [disabled]="data.status === 'busy'" (click)="save()">Ejecutar</button>
</div>
</div>
<mat-divider></mat-divider>
<div class="select-container">
<mat-expansion-panel hideToggle>
<mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title>
<mat-panel-description> Listado de clientes donde se realizará el particionado </mat-panel-description>
</mat-expansion-panel-header>
<div class="clients-grid" >
<div *ngFor="let client of clientData" class="client-item">
<div class="client-card">
<img
[src]="'assets/images/ordenador_' + client.status + '.png'"
alt="Client Icon"
class="client-image" />
<div class="client-details">
<span class="client-name">{{ client.name }}</span>
<span class="client-ip">{{ client.ip }}</span>
<span class="client-ip">{{ client.mac }}</span>
</div>
</div>
</div>
</div>
</mat-expansion-panel>
</div>
<mat-divider style="margin-top: 20px;"></mat-divider>
<mat-dialog-content>
<div class="disk-select">
<mat-form-field appearance="fill">

View File

@ -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<any>();
partitionTypes = PARTITION_TYPES;
@ -39,6 +39,7 @@ export class PartitionAssistantComponent implements OnInit {
updateRequests: any[] = [];
data: any = {};
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = [];
clientData: any = [];
private apiUrl: string = this.baseUrl + '/partitions';
@ -51,11 +52,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 +65,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,10 +252,6 @@ 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.';
@ -283,14 +281,16 @@ 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) => {

View File

@ -1,39 +0,0 @@
<h2>{{ 'diskImageAssistantTitle' | translate }}</h2>
<div *ngFor="let disk of disks" class="partition-assistant">
<div class="header">
<label>{{ 'diskLabel' | translate }} {{ disk.diskNumber }}</label>
</div>
<table class="partition-table">
<thead>
<tr>
<th>{{ 'partitionColumn' | translate }}</th>
<th>{{ 'isoImageColumn' | translate }}</th>
<th>{{ 'ogliveColumn' | translate }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let partition of disk.partitions">
<td>{{ partition.partitionNumber }}</td>
<td>
<select [(ngModel)]="partition.associatedImageId" (change)="onImageSelected(partition, $event)" name="associatedImage-{{partition.partitionNumber}}">
<option value="">{{ 'selectImageOption' | translate }}</option>
<option *ngFor="let image of availableImages" [value]="image['@id']">
{{ image.name }}
</option>
</select>
</td>
<td>
<select (change)="onOgLiveSelected(partition, $event)">
<option value="">{{ 'selectOgLiveOption' | translate }}</option>
<option *ngFor="let ogLive of availableOgLives" [value]="ogLive">{{ ogLive }}</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<div class="actions">
<button mat-flat-button color="primary" (click)="saveAssociations()">{{ 'saveAssociationsButton' | translate }}</button>
</div>

View File

@ -147,10 +147,15 @@
<!-- Clients view -->
<div class="clients-container">
<div class="clients-view-header">
<span class="clients-title-name">{{ 'clients' | translate }}
<span class="clients-title-name">{{ 'clients' | translate }}
<strong>{{ selectedNode?.name }}</strong>
</span>
</span>
<div class="view-type-container">
<app-execute-command
[clientData]="arrayClients"
[buttonType]="'text'"
[buttonText]="'Ejecutar comandos'"
></app-execute-command>
<button mat-button color="primary" (click)="toggleView('card')" [disabled]="currentView === 'card'">
<mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
</button>
@ -192,7 +197,11 @@
<button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)">
<mat-icon>visibility</mat-icon>
</button>
<app-execute-command [clientData]="client['@id']"></app-execute-command>
<app-execute-command
[clientData]="[client]"
[buttonType]="'icon'"
[icon]="'terminal'"
></app-execute-command>
</div>
</div>
</div>
@ -201,6 +210,23 @@
<!-- List view -->
<div class="clients-table" *ngIf="currentView === 'list'">
<table mat-table matSort [dataSource]="selectedClients" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()"
>
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()"
(change)="toggleRow(row)"
[checked]="selection.isSelected(row)"
[disabled]="row.status === 'busy'"
>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
<td mat-cell *matCellDef="let client">
@ -267,7 +293,11 @@
<button mat-icon-button [matMenuTriggerFor]="clientMenu">
<mat-icon>more_vert</mat-icon>
</button>
<app-execute-command [clientData]="client['@id']"></app-execute-command>
<app-execute-command
[clientData]="[client]"
[buttonType]="'icon'"
[icon]="'terminal'"
></app-execute-command>
<mat-menu #clientMenu="matMenu">
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
<mat-icon>edit</mat-icon>

View File

@ -22,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',
@ -51,16 +52,17 @@ export class GroupsComponent implements OnInit, OnDestroy {
commands: Command[] = [];
commandsLoading = false;
selectedClients = new MatTableDataSource<Client>([]);
selection = new SelectionModel<any>(true, []);
cols = 4;
selectedClientsOriginal: Client[] = [];
currentView: 'card' | 'list' = 'list';
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;
@ -139,7 +141,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
ip: node.ip,
'@id': node['@id'],
});
toggleView(view: 'card' | 'list'): void {
this.currentView = view;
}
@ -149,6 +151,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.cols = width <= 600 ? 1 : width <= 960 ? 2 : width <= 1280 ? 3 : 4;
};
<<<<<<< Updated upstream
clearSelection(): void {
this.selectedUnidad = null;
this.selectedDetail = null;
@ -169,6 +172,20 @@ export class GroupsComponent implements OnInit, OnDestroy {
// )
// );
// }
=======
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);
}
)
);
}
>>>>>>> Stashed changes
// Función para cargar un filtro seleccionado actu
// loadSelectedFilter(savedFilter: [string, string]): void {
@ -218,17 +235,22 @@ export class GroupsComponent implements OnInit, OnDestroy {
private refreshData(selectedNodeIdOrUuid?: string): void {
this.loading = true;
this.isLoadingClients = !!selectedNodeIdOrUuid;
this.dataService.getOrganizationalUnits().subscribe({
next: (data) => {
<<<<<<< Updated upstream
this.originalTreeData = data.map((unidad) => this.convertToTreeData(unidad));
this.treeDataSource.data = [...this.originalTreeData];
=======
this.treeDataSource.data = data.map((unidad) => this.convertToTreeData(unidad));
>>>>>>> Stashed changes
if (selectedNodeIdOrUuid) {
this.selectedNode = this.findNodeByIdOrUuid(this.treeDataSource.data, selectedNodeIdOrUuid);
if (this.selectedNode) {
this.treeControl.collapseAll();
this.treeControl.collapseAll();
this.expandPathToNode(this.selectedNode);
this.fetchClientsForNode(this.selectedNode);
}
@ -242,7 +264,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.selectedClients.data = [];
}
}
this.loading = false;
this.isLoadingClients = false;
},
@ -254,13 +276,18 @@ export class GroupsComponent implements OnInit, OnDestroy {
},
});
}
<<<<<<< Updated upstream
public expandPathToNode(node: TreeNode): void {
=======
private expandPathToNode(node: TreeNode): void {
>>>>>>> Stashed changes
const path: TreeNode[] = [];
let currentNode: TreeNode | null = node;
while (currentNode) {
path.unshift(currentNode);
path.unshift(currentNode);
currentNode = currentNode.id ? this.findParentNode(this.treeDataSource.data, currentNode.id) : null;
}
@ -271,13 +298,13 @@ 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)) {
return node;
}
if (node.children && node.children.length > 0) {
const parent = this.findParentNode(node.children, childId);
if (parent) {
@ -287,7 +314,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) {
@ -358,15 +385,15 @@ export class GroupsComponent implements OnInit, OnDestroy {
data: { organizationalUnit: targetNode },
width: '900px',
});
dialogRef.afterClosed().subscribe((result) => {
if (result?.client && result?.organizationalUnit) {
const organizationalUnitUrl = result.organizationalUnit;
const uuid = organizationalUnitUrl.split('/')[2];
const uuid = organizationalUnitUrl.split('/')[2];
const parentNode = this.findNodeByIdOrUuid(this.treeDataSource.data, uuid);
if (parentNode) {
this.refreshData(parentNode.uuid);
this.refreshData(parentNode.uuid);
}
}
});
@ -381,14 +408,14 @@ export class GroupsComponent implements OnInit, OnDestroy {
width: '900px',
});
dialogRef.afterClosed().subscribe((result) => {
if (result?.success) {
const organizationalUnitUrl = result.organizationalUnit;
const uuid = organizationalUnitUrl.split('/')[2];
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);
this.refreshData(parentNode.uuid);
} else {
console.error('No se encontró el nodo padre después de la creación masiva.');
}
@ -432,18 +459,18 @@ export class GroupsComponent implements OnInit, OnDestroy {
private deleteEntityorClient(uuid: string, type: string): void {
if (!this.selectedNode) return;
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) {
@ -622,18 +649,44 @@ 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];
}
private extractUuid(idPath: string | undefined): string | null {
return idPath ? idPath.split('/').pop() || null : null;
}

View File

@ -157,7 +157,7 @@ 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) => {
this.toastService.success('Cliente creado exitosamente', 'Éxito');