refs #1354. Added multiple commands logic. Updated endpoints
testing/ogGui-multibranch/pipeline/head Something is wrong with the build of this commit
Details
testing/ogGui-multibranch/pipeline/head Something is wrong with the build of this commit
Details
parent
3a7bff9e74
commit
b516103008
|
@ -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 },
|
||||
|
|
|
@ -90,3 +90,10 @@ table {
|
|||
color: white;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in New Issue