refs #691. Important improvements ogBoot

develop-jenkins
Manuel Aranda Rosales 2024-10-22 07:26:35 +02:00
parent 4871d443a5
commit 54bdcd926a
25 changed files with 732 additions and 277 deletions

View File

@ -10,9 +10,6 @@
</mat-select>
</mat-form-field>
</div>
<span>
Clientes: {{ getClientesNames() }}
</span>
<div class="mat-dialog-actions">
<button mat-button (click)="onCancel()">Cancelar</button>
<button mat-button color="primary" (click)="onSave()">{{ isEditMode ? 'Guardar' : 'Añadir' }}</button>

View File

@ -12,7 +12,6 @@ export class CreatePxeBootFileComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
pxeTemplates: any[] = [];
selectedPxeTemplate: string | undefined;
clientes: string[] = [];
selectedElements: any;
isEditMode: boolean = false;
@ -24,8 +23,7 @@ export class CreatePxeBootFileComponent implements OnInit {
) {}
ngOnInit(): void {
this.selectedElements = this.data.clients;
this.clientes = this.selectedElements.map((client: { uuid: any }) => `/clients/${client.uuid}`);
this.selectedElements = this.data
this.loadPxeTemplates();
if (this.data.bootFile) {
this.isEditMode = true;
@ -33,10 +31,6 @@ export class CreatePxeBootFileComponent implements OnInit {
}
}
getClientesNames(): string {
return this.selectedElements.map((client: { name: any }) => client.name).join(', ');
}
loadPxeTemplates(): void {
this.http.get(`${this.baseUrl}/pxe-templates?page=1&itemsPerPage=30`)
.subscribe((response: any) => {
@ -54,7 +48,7 @@ export class CreatePxeBootFileComponent implements OnInit {
if (this.selectedPxeTemplate) {
const payload = {
template: `/pxe-templates/${this.selectedPxeTemplate}`,
clients: this.clientes
clients: this.selectedElements
};
if (this.isEditMode && this.data.bootFile) {

View File

@ -1,9 +1,6 @@
<h2 mat-dialog-title>Imagen ogLive</h2>
<mat-dialog-content>
<h3>Nombre</h3>
<p>{{name}}</p>
<h3>URL</h3>
<p>{{downloadUrl}}</p>
<pre>{{ data | json }}</pre>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onNoClick()">Cancel</button>

View File

@ -9,17 +9,14 @@ import { ToastrService } from 'ngx-toastr';
styleUrls: ['./info-image.component.css']
})
export class InfoImageComponent {
name: string;
downloadUrl: string;
constructor(
private toastService: ToastrService,
private http: HttpClient,
public dialogRef: MatDialogRef<InfoImageComponent>,
@Inject(MAT_DIALOG_DATA) public data: { name: string, downloadUrl: string, uuid: string }
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.name = data.name;
this.downloadUrl = data.downloadUrl;
}
onNoClick(): void {

View File

@ -12,35 +12,6 @@
margin: 20px 0;
}
.lists-container {
padding: 16px;
}
.imagesLists-container {
flex: 1;
}
.card.unidad-card {
height: 100%;
box-sizing: border-box;
}
.image-container {
display: flex;
align-items: center;
margin-bottom: 16px;
border-bottom: 1px solid rgba(122, 122, 122, 0.555);
}
.image-container h4 {
margin: 0;
flex: 1;
}
.image-name{
cursor: pointer;
}
table {
width: 100%;
margin-top: 50px;

View File

@ -47,8 +47,7 @@
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image" [ngClass]="{'clickable': column.columnDef === 'name'}"
(click)="column.columnDef === 'name' && showInfo(image)">
<td mat-cell *matCellDef="let image" >
<ng-container *ngIf="column.columnDef === 'isDefault' || column.columnDef === 'installed'">
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
@ -57,12 +56,24 @@
</ng-container>
<ng-container *ngIf="column.columnDef === 'downloadUrl'">
<span matTooltip="{{ image.downloadUrl }}">
{{ image.downloadUrl ? image.downloadUrl.substring(0, 20) + '...' : '' }}
</span>
<span matTooltip="{{ image.downloadUrl }}">
{{ image.downloadUrl ? image.downloadUrl.substring(0, 20) + '...' : '' }}
</span>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'isDefault' && column.columnDef !== 'installed' && column.columnDef !== 'downloadUrl'">
<ng-container *ngIf="column.columnDef === 'name'">
<span matTooltip="{{ image.name }}">
{{ image.name ? image.name.substring(0, 20) + '...' : '' }}
</span>
</ng-container>
<ng-container *ngIf="column.columnDef === 'status'">
<mat-chip>
{{ column.cell(image) }}
</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'isDefault' && column.columnDef !== 'installed' && column.columnDef !== 'downloadUrl' && column.columnDef !== 'status' && column.columnDef !== 'name' ">
{{ column.cell(image) }}
</ng-container>
@ -72,14 +83,16 @@
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions">Acciones</th>
<td mat-cell *matCellDef="let image">
<button mat-icon-button color="info" (click)="showOgLive($event, image)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" (click)="editImage(image)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="deleteImage(image)" i18n="@@buttonDelete"><mat-icon>delete</mat-icon></button>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="toggleAction(image, 'install')">Instalar</button>
<button mat-menu-item (click)="toggleAction(image, 'uninstall')">Desinstalar</button>
<button mat-menu-item (click)="toggleAction(image, 'set-default')">Cambiar a imagen por defecto</button>
<button mat-menu-item [disabled]="!image.installed" (click)="toggleAction(image, 'uninstall')">Desinstalar</button>
<button mat-menu-item [disabled]="!image.installed" (click)="toggleAction(image, 'set-default')">Cambiar a imagen por defecto</button>
</mat-menu>
</td>
</ng-container>

View File

@ -10,6 +10,8 @@ import { DatePipe } from "@angular/common";
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
import {DataService} from "./data.service";
import {ServerInfoDialogComponent} from "../../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component";
import {ShowTemplateContentComponent} from "../pxe/show-template-content/show-template-content.component";
import {Observable} from "rxjs";
@Component({
selector: 'app-pxe-images',
@ -56,6 +58,11 @@ export class PXEimagesComponent implements OnInit {
header: 'Imagen instalada en ogBoot',
cell: (user: any) => `${user.installed}`
},
{
columnDef: 'status',
header: 'Estado',
cell: (image: any) => `${image.status}`
},
{
columnDef: 'createdAt',
header: 'Fecha de creación',
@ -115,40 +122,36 @@ export class PXEimagesComponent implements OnInit {
case 'set-default':
this.http.post(`${this.apiUrl}/server/${image.uuid}/set-default`, {}).subscribe({
next: () => {
console.log('Imagen cambiada');
this.toastService.success('Petición de cambio de imagen enviada');
this.search();
},
error: (error) => {
console.error('Error al cambiar la imagen:', error);
this.toastService.error('Error al instalar la imagen por defecto');
console.error(error.error['hydra:description']);
this.toastService.error('Error:' + error.error['hydra:description']);
}
});
break;
case 'install':
this.http.post(`${this.apiUrl}/server/${image.uuid}/install`, {}).subscribe({
next: () => {
console.log('Imagen cambiada');
this.toastService.success('Petición de instalación enviada');
this.search();
},
error: (error) => {
console.error('Error al instalar la imagen:', error);
this.toastService.error('Error al instalar la imagen');
console.error(error.error['hydra:description']);
this.toastService.error('Error:' + error.error['hydra:description']);
}
});
break;
case 'uninstall':
this.http.post(`${this.apiUrl}/server/${image.uuid}/uninstall`, {}).subscribe({
next: () => {
console.log('Imagen cambiada');
this.toastService.success('Petición de desinstalación enviada');
/* this.deleteImage(image); */
this.search();
},
error: (error) => {
console.error('Error al desinstalar la imagen:', error);
this.toastService.error('Error al desinstalar la imagen');
console.error(error.error['hydra:description']);
this.toastService.error('Error:' + error.error['hydra:description']);
}
});
break;
@ -171,6 +174,35 @@ export class PXEimagesComponent implements OnInit {
});
}
deleteImage(image: any): void {
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '400px',
data: { name: image.name }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
const apiUrl = `${this.baseUrl}${image['@id']}`;
this.http.delete(apiUrl).subscribe({
next: () => {
this.search();
this.toastService.success('oG Live deleted successfully');
},
error: (error) => {
this.toastService.error(error.error['hydra:description']);
}
});
} else {
console.log('ogLive deletion cancelled');
}
});
}
showOgLive(event: MouseEvent, data: any): void {
event.stopPropagation();
const dialogRef = this.dialog.open(InfoImageComponent, { data: { data }, width: '700px'});
}
applyFilter() {
this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({
@ -190,25 +222,26 @@ export class PXEimagesComponent implements OnInit {
this.applyFilter();
}
loadAlert() {
this.http.get(`${this.apiUrl}/server/get-collection`)
.subscribe(response => {
// @ts-ignore
this.alertMessage = response.message
}, error => {
console.error('Error al cargar la información del alert', error);
});
loadAlert(): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/server/get-collection`);
}
openSubnetInfoDialog() {
this.loadAlert()
this.dialog.open(ServerInfoDialogComponent, {
width: '600px',
data: {
alertMessage: this.alertMessage,
length: this.length
this.loadAlert().subscribe(
response => {
this.alertMessage = response.message;
this.dialog.open(ServerInfoDialogComponent, {
width: '600px',
data: {
message: this.alertMessage
}
});
},
error => {
console.error('Error al cargar la información del alert', error);
}
});
);
}
syncOgBoot(): void {

View File

@ -0,0 +1,17 @@
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100px;
}
mat-form-field {
width: 100%;
}
mat-dialog-actions {
display: flex;
justify-content: flex-end;
}

View File

@ -0,0 +1,29 @@
<h2 mat-dialog-title>Añade clientes a {{data.subnetName}}</h2>
<mat-dialog-content>
<mat-form-field appearance="fill" class="search-select">
<input type="text" matInput [formControl]="clientControl" [matAutocomplete]="clientAuto" placeholder="Seleccione un cliente">
<mat-autocomplete #clientAuto="matAutocomplete" [displayWith]="displayFnClient" (optionSelected)="onOptionClientSelected($event.option.value)">
<mat-option *ngFor="let client of filteredClients | async" [value]="client">
{{ client.name }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<div *ngIf="selectedClients.length > 0">
<h3>Clientes seleccionados:</h3>
<ul>
<li *ngFor="let client of selectedClients">
{{ client.name }}
<button mat-icon-button color="warn" (click)="removeClient(client)">
<mat-icon>delete</mat-icon>
</button>
</li>
</ul>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="close()">Cancelar</button>
<button mat-button (click)="save()">Añadir</button>
</mat-dialog-actions>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AddClientsToPxeComponent } from './add-clients-to-pxe.component';
describe('AddClientsToPxeComponent', () => {
let component: AddClientsToPxeComponent;
let fixture: ComponentFixture<AddClientsToPxeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddClientsToPxeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AddClientsToPxeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,99 @@
import {Component, Inject} from '@angular/core';
import {Observable, startWith} from "rxjs";
import {FormControl} from "@angular/forms";
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ToastrService} from "ngx-toastr";
import {map} from "rxjs/operators";
@Component({
selector: 'app-add-clients-to-pxe',
templateUrl: './add-clients-to-pxe.component.html',
styleUrl: './add-clients-to-pxe.component.css'
})
export class AddClientsToPxeComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
clients: any[] = [];
selectedClients: any[] = [];
loading: boolean = true;
filters: { [key: string]: string } = {};
filteredClients!: Observable<any[]>;
clientControl = new FormControl();
constructor(
private http: HttpClient,
public dialogRef: MatDialogRef<AddClientsToPxeComponent>,
private toastService: ToastrService,
@Inject(MAT_DIALOG_DATA) public data: { subnetUuid: string, subnetName: string }
) {}
ngOnInit(): void {
console.log('Selected subnet UUID:', this.data);
this.loading = true;
this.loadClients();
this.filteredClients = this.clientControl.valueChanges.pipe(
startWith(''),
map(value => (typeof value === 'string' ? value : value?.name)),
map(name => (name ? this._filterClients(name) : this.clients.slice()))
);
}
loadClients() {
this.http.get<any>( `${this.baseUrl}/clients?&page=1&itemsPerPage=10000&exists[template]=false`).subscribe(
response => {
this.clients = response['hydra:member'];
this.loading = false;
},
error => {
console.error('Error fetching parent units:', error);
this.loading = false;
}
);
}
save() {
const postData = {
clients: this.selectedClients.map(client => client['@id'])
};
this.http.post(`${this.baseUrl}/pxe-templates/${this.data.subnetUuid}/add-clients`, postData).subscribe(
response => {
this.toastService.success('Clientes asignados correctamente');
},
error => {
this.toastService.error(error.error['hydra:description']);
}
);
this.dialogRef.close(this.selectedClients);
}
close() {
this.dialogRef.close();
}
removeClient(client: any) {
const index = this.selectedClients.indexOf(client);
if (index >= 0) {
this.selectedClients.splice(index, 1);
}
}
private _filterClients(name: string): any[] {
const filterValue = name.toLowerCase();
return this.clients.filter(client => client.name.toLowerCase().includes(filterValue));
}
displayFnClient(client: any): string {
return client && client.name ? client.name : '';
}
onOptionClientSelected(client: any) {
if (!this.selectedClients.includes(client)) {
this.selectedClients.push(client);
}
this.clientControl.setValue('');
}
}

View File

@ -0,0 +1,47 @@
mat-dialog-actions {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
button {
margin-left: 10px;
}
.green-icon {
color: green;
}
.red-icon {
color: red;
}
.spacing-container {
margin-top: 20px;
margin-bottom: 16px;
}
.list-item-content {
display: flex;
align-items: flex-start; /* Alinea el contenido al inicio */
justify-content: space-between; /* Espacio entre los textos y los íconos */
width: 100%; /* Asegúrate de que el contenido ocupe todo el ancho */
}
.text-content {
flex-grow: 1;
margin-right: 16px;
margin-left: 10px;
}
.icon-container {
display: flex;
align-items: center;
}
.right-icon {
margin-left: 8px;
cursor: pointer;
}

View File

@ -0,0 +1,25 @@
<h2 mat-dialog-title>Gestionar clientes </h2>
<mat-dialog-content>
<mat-list>
<ng-container *ngFor="let client of clients">
<mat-list-item >
<div class="list-item-content">
<mat-icon matListItemIcon [ngClass]="{'red-icon': client.pxeSync === false || !client.pxeSync, 'green-icon': client.pxeSync === true}">computer</mat-icon>
<div class="text-content">
<div matListItemTitle>{{ client.name }}</div>
<div matListItemLine>{{ client.mac }}</div>
</div>
<div class="icon-container">
<button mat-icon-button color="primary" (click)="addClientToTemplate(client)" i18n="@@editImage"> <mat-icon>sync</mat-icon></button>
<button mat-icon-button color="warn" class="right-icon" (click)="deleteClient(client)">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</mat-list-item>
</ng-container>
</mat-list>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button type="button" (click)="onCancel()">Cancelar</button>
</mat-dialog-actions>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ClientsComponent } from './clients.component';
describe('ClientsComponent', () => {
let component: ClientsComponent;
let fixture: ComponentFixture<ClientsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ClientsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ClientsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,81 @@
import {Component, Inject} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
@Component({
selector: 'app-clients',
templateUrl: './clients.component.html',
styleUrl: './clients.component.css'
})
export class ClientsComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
templateForm!: FormGroup;
clients: any[] = [];
constructor(
public dialogRef: MatDialogRef<ClientsComponent>,
public dialog: MatDialog,
private http: HttpClient,
private toastService: ToastrService,
@Inject(MAT_DIALOG_DATA) public data: any
) {}
ngOnInit() {
this.getPxeClients()
}
getPxeClients(): void {
this.http.get<any>(`${this.baseUrl}/clients?template.id=${this.data.data.id}`).subscribe({
next: data => {
console.log(data['hydra:member'])
this.clients = data['hydra:member']
},
error: error => {
console.error('Error al obtener los clientes PXE:', error);
}
});
}
addClientToTemplate(client: any): void {
const postData = {
client: client['@id']
};
this.http.post(`${this.baseUrl}/pxe-templates/${this.data.data.uuid}/sync-client`, postData).subscribe(
response => {
this.toastService.success('Clientes asignados correctamente');
this.getPxeClients()
},
error => {
this.toastService.error(error.error['hydra:description']);
}
);
}
deleteClient(client: any): void {
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: client.name }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.http.post(`${this.baseUrl}/pxe-templates/${this.data.uuid}/delete-client`, { client: client['@id'] }).subscribe({
next: () => {
this.toastService.success('Cliente eliminado exitosamente');
this.dialogRef.close();
},
error: (error) => {
this.toastService.error(error.error['hydra:description']);
}
});
}})
}
onCancel(): void {
this.dialogRef.close(false);
}
}

View File

@ -1,7 +1,3 @@
form {
max-width: 600px;
padding: 20px;
}
mat-form-field {
width: 100%;
@ -49,3 +45,31 @@ h3 {
font-size: 1.2rem;
color: #000000;
}
.spacing-container {
margin-top: 20px;
margin-bottom: 16px;
}
.list-item-content {
display: flex;
align-items: flex-start; /* Alinea el contenido al inicio */
justify-content: space-between; /* Espacio entre los textos y los íconos */
width: 100%; /* Asegúrate de que el contenido ocupe todo el ancho */
}
.text-content {
flex-grow: 1;
margin-right: 16px;
margin-left: 10px;
}
.icon-container {
display: flex;
align-items: center;
}
.right-icon {
margin-left: 8px;
cursor: pointer;
}

View File

@ -1,27 +1,31 @@
<form [formGroup]="templateForm" (ngSubmit)="onSave()">
<h2 *ngIf="!isEditMode">Crear Plantilla PXE</h2>
<h2 *ngIf="isEditMode">Editar Plantilla PXE</h2>
<h2 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Añadir' }} plantilla </h2>
<mat-form-field appearance="fill">
<mat-label>Nombre de la Plantilla</mat-label>
<input matInput formControlName="name" placeholder="Introduce el nombre de la plantilla">
<mat-error *ngIf="templateForm.get('name')?.hasError('required')">
El nombre de la plantilla es requerido.
</mat-error>
</mat-form-field>
<mat-dialog-content>
<div class="spacing-container">
<form [formGroup]="templateForm" (ngSubmit)="onSave()">
<mat-form-field appearance="fill">
<mat-label>Contenido de la Plantilla</mat-label>
<textarea matInput formControlName="templateContent" rows="20" placeholder="Introduce el contenido de la plantilla"></textarea>
<mat-error *ngIf="templateForm.get('templateContent')?.hasError('required')">
El contenido de la plantilla es requerido.
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Nombre de la Plantilla</mat-label>
<input matInput formControlName="name" placeholder="Introduce el nombre de la plantilla">
<mat-error *ngIf="templateForm.get('name')?.hasError('required')">
El nombre de la plantilla es requerido.
</mat-error>
</mat-form-field>
<mat-dialog-actions align="end">
<button mat-button type="button" (click)="onCancel()">Cancelar</button>
<button mat-raised-button color="primary" type="submit" [disabled]="!templateForm.valid">
{{ isEditMode ? 'Actualizar' : 'Crear' }}
</button>
</mat-dialog-actions>
</form>
<mat-form-field appearance="fill">
<mat-label>Contenido de la Plantilla</mat-label>
<textarea matInput formControlName="templateContent" rows="20" placeholder="Introduce el contenido de la plantilla"></textarea>
<mat-error *ngIf="templateForm.get('templateContent')?.hasError('required')">
El contenido de la plantilla es requerido.
</mat-error>
</mat-form-field>
</form>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button type="button" (click)="onCancel()">Cancelar</button>
<button mat-raised-button color="primary" type="submit" (click)="onSave()" [disabled]="!templateForm.valid">
{{ isEditMode ? 'Actualizar' : 'Crear' }}
</button>
</mat-dialog-actions>

View File

@ -1,8 +1,9 @@
import { HttpClient } from '@angular/common/http';
import { Component, Inject, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import {MatDialogRef, MAT_DIALOG_DATA, MatDialog} from '@angular/material/dialog';
import { ToastrService } from 'ngx-toastr';
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
@Component({
selector: 'app-create-pxe-template',
@ -14,9 +15,11 @@ export class CreatePxeTemplateComponent implements OnInit {
templateForm!: FormGroup;
previewContent: string = '';
isEditMode: boolean = false;
clients: any[] = [];
constructor(
public dialogRef: MatDialogRef<CreatePxeTemplateComponent>,
public dialog: MatDialog,
private http: HttpClient,
private fb: FormBuilder,
private toastService: ToastrService,
@ -24,8 +27,12 @@ export class CreatePxeTemplateComponent implements OnInit {
) {}
ngOnInit() {
this.isEditMode = !!this.data; // Verifica si hay datos inyectados (modo edición)
this.isEditMode = !!this.data;
if (this.isEditMode){
this.getPxeClients()
}
this.templateForm = this.fb.group({
name: [this.data?.name || '', Validators.required],
templateContent: [this.data?.templateContent || '', Validators.required]
@ -40,6 +47,17 @@ export class CreatePxeTemplateComponent implements OnInit {
}
}
getPxeClients(): void {
this.http.get<any>(`${this.baseUrl}/clients?template.id=${this.data.id}`).subscribe({
next: data => {
this.clients = data['hydra:member']
},
error: error => {
console.error('Error al obtener los clientes PXE:', error);
}
});
}
createPxeTemplate(): void {
const formValues = this.templateForm.value;
const payload = {
@ -70,18 +88,51 @@ export class CreatePxeTemplateComponent implements OnInit {
this.http.patch<any>(`${this.baseUrl}/pxe-templates/${this.data.uuid}`, payload).subscribe({
next: data => {
console.log('Plantilla PXE actualizada:', data);
this.toastService.success('Plantilla PXE actualizada exitosamente');
this.dialogRef.close(true);
},
error: error => {
console.error('Error al actualizar la plantilla PXE:', error);
this.toastService.error('Error al actualizar la plantilla PXE');
this.dialogRef.close(false);
}
});
}
addClientToTemplate(client: any): void {
const postData = {
client: client['@id']
};
this.http.post(`${this.baseUrl}/pxe-templates/${this.data.uuid}/sync-client`, postData).subscribe(
response => {
this.toastService.success('Clientes asignados correctamente');
},
error => {
this.toastService.error(error.error['hydra:description']);
}
);
}
deleteClient(client: any): void {
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: client.name }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.http.post(`${this.baseUrl}/pxe-templates/${this.data.uuid}/delete-client`, { client: client['@id'] }).subscribe({
next: () => {
this.toastService.success('Cliente eliminado exitosamente');
this.dialogRef.close();
},
error: (error) => {
this.toastService.error(error.error['hydra:description']);
}
});
}})
}
onCancel(): void {
this.dialogRef.close(false);
}

View File

@ -1,52 +1,9 @@
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 100px;
padding: 10px;
margin-top: 16px;
}
.title {
font-size: 24px;
font-size: 24px;
}
.divider {
margin: 20px 0;
}
.lists-container {
padding: 16px;
}
.templatesLists-container {
flex: 1;
}
.card.unidad-card {
height: 100%;
box-sizing: border-box;
}
.template-container {
display: flex;
align-items: center;
margin-bottom: 16px;
border-bottom: 1px solid rgba(122, 122, 122, 0.555);
}
.template-container h4 {
margin: 0;
flex: 1;
}
.mat-icon-button {
margin-left: 16px;
align-self: center;
}
.template-name{
cursor: pointer;
margin: 20px 0;
}
table {
@ -74,10 +31,10 @@ table {
}
.header-container {
margin-top: 16px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
.mat-elevation-z8 {
@ -90,66 +47,21 @@ table {
margin-bottom: 30px;
}
.info-container {
background-color: #e8eaf6;
padding: 16px;
border-radius: 4px;
font-size: 14px;
overflow-x: auto;
margin-top: 16px;
border: 1px solid #c5cae9;
.example-headers-align .mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}
.info-container h3 {
margin-top: 0;
font-size: 16px;
color: #000000;
.example-headers-align .mat-mdc-form-field + .mat-mdc-form-field {
margin-left: 8px;
}
.info-container p {
margin: 8px 0;
line-height: 1.5;
.example-button-row {
display: table-cell;
max-width: 600px;
}
.info-container strong {
color: #0d47a1;
.example-button-row .mat-mdc-button-base {
margin: 8px 8px 8px 0;
}
.info-container {
background-color: #e8eaf6;
padding: 16px;
border-radius: 4px;
font-size: 14px;
overflow-x: auto;
margin-top: 16px;
border: 1px solid #c5cae9;
}
.info-container h3 {
margin-top: 0;
font-size: 16px;
color: #1e88e5;
}
.info-container p {
margin: 8px 0;
line-height: 1.5;
}
.info-container strong {
color: #0d47a1;
}
.info-container pre {
background-color: #f5f5f5;
padding: 16px;
border-radius: 4px;
font-size: 12px;
overflow-x: auto;
white-space: pre;
border: 1px solid #dcdcdc;
}
td {
cursor: pointer;
}

View File

@ -1,19 +1,18 @@
<mat-accordion>
<mat-accordion class="example-headers-align">
<mat-expansion-panel hideToggle>
<mat-expansion-panel-header>
<mat-panel-title>Sincronización ogBoot </mat-panel-title>
<mat-panel-description>
<mat-icon [style.color]="getIcon().color">{{ getIcon().name }}</mat-icon>
</mat-panel-description>
<mat-panel-title> Información en servidor ogBoot </mat-panel-title>
</mat-expansion-panel-header>
<p *ngIf="alertMessage">Plantillas creadsa en servidor ogBoot: {{ alertMessage }}</p>
<p *ngIf="alertMessage">Plantillas creadsa en servidor ogCore (base de datos): {{ length }}</p>
<div class="example-button-row">
<button mat-flat-button color="primary" (click)="syncOgCore()"> Sincronizar OgCore</button>
<button mat-flat-button color="primary" (click)="syncTemplates()"> Sincronizar base de datos</button>
</div>
<div class="example-button-row">
<button mat-flat-button color="accent" (click)="openSubnetInfoDialog()">Ver Información</button>
</div>
</mat-expansion-panel>
</mat-accordion>
<div class="header-container">
<h2 class="title" i18n="@@adminPXETitle">Administrar plantillas PXE</h2>
<div class="pxe-button-row">
@ -40,32 +39,33 @@
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image" [ngClass]="{'clickable': column.columnDef === 'name'}"
(click)="column.columnDef === 'name' && showPxeInfo(image)">
<!-- Condición para mostrar íconos para isDefault e installed -->
<td mat-cell *matCellDef="let image" >
<ng-container *ngIf="column.columnDef === 'synchronized'">
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
</mat-icon>
</ng-container>
<!-- Mostrar otros campos normalmente -->
<ng-container *ngIf="column.columnDef !== 'synchronized'">
{{ column.cell(image) }}
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions">Acciones</th>
<td mat-cell *matCellDef="let template">
<ng-container matColumnDef="actions" >
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let template" style="text-align: center;">
<button mat-icon-button color="info" (click)="showTemplate($event, template)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="info" [disabled]="template.clientsLength === 0" (click)="editClients($event, template)"><mat-icon i18n="@@deleteElementTooltip">computer</mat-icon></button>
<button mat-icon-button color="primary" (click)="editPxeTemplate(template)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="toggleAction(template, 'create')">Crear</button>
<button mat-menu-item (click)="toggleAction(template, 'create')">Crear en servidor ogBoot</button>
<button mat-menu-item (click)="addClientsToPxe(template)">Añadir cliente</button>
<button mat-menu-item (click)="toggleAction(template, 'sync')">Sincronizar base de datos</button>
<button mat-menu-item (click)="toggleAction(template, 'delete')">Eliminar</button>
</mat-menu>
</td>
@ -82,7 +82,3 @@
(page)="onPageChange($event)">
</mat-paginator>
</div>
<div class="info-container" *ngIf="selectedItem">
<h3>Detalles de {{ selectedItem.name }}</h3>
<pre>{{ previewContent }}</pre>
</div>

View File

@ -8,6 +8,18 @@ import { ToastrService } from 'ngx-toastr';
import { DatePipe } from '@angular/common';
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
import { DataService } from './data.service';
import {
ShowOrganizationalUnitComponent
} from "../../groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component";
import {ShowTemplateContentComponent} from "./show-template-content/show-template-content.component";
import {ServerInfoDialogComponent} from "../../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component";
import {
AddClientsToSubnetComponent
} from "../../ogdhcp/og-dhcp-subnets/add-clients-to-subnet/add-clients-to-subnet.component";
import {Subnet} from "../../ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component";
import {AddClientsToPxeComponent} from "./add-clients-to-pxe/add-clients-to-pxe.component";
import {Observable} from "rxjs";
import {ClientsComponent} from "./clients/clients.component";
@Component({
selector: 'app-pxe',
@ -43,7 +55,7 @@ export class PxeComponent {
},
{
columnDef: 'synchronized',
header: 'Creado en ogBoot',
header: 'Sincronizado',
cell: (user: any) => `${user.synchronized}`
},
{
@ -65,7 +77,6 @@ export class PxeComponent {
ngOnInit(): void {
this.search();
this.loadAlert();
}
search(): void {
@ -84,7 +95,7 @@ export class PxeComponent {
addPxeTemplate() {
const dialogRef = this.dialog.open(CreatePxeTemplateComponent, {
width: '600px'
width: '800px'
});
dialogRef.afterClosed().subscribe(() => {
@ -95,7 +106,7 @@ export class PxeComponent {
editPxeTemplate(template: any) {
const dialogRef = this.dialog.open(CreatePxeTemplateComponent, {
data: template, // Pasa los datos del template para edición
width: '600px'
width: '800px'
});
dialogRef.afterClosed().subscribe(() => {
@ -103,11 +114,6 @@ export class PxeComponent {
});
}
showPxeInfo(template: any) {
this.selectedItem = template;
this.previewContent = template.templateContent;
}
toggleAction(image: any, action: string): void {
switch (action) {
case 'create':
@ -115,7 +121,19 @@ export class PxeComponent {
next: (response) => {
this.search();
// @ts-ignore
this.toastService.success(response.message);
this.toastService.success(response.success);
},
error: (error) => {
this.toastService.error(error.error.error);
}
});
break;
case 'sync':
this.http.get(`${this.apiUrl}/server/${image.uuid}/get`, {}).subscribe({
next: (response) => {
this.search();
// @ts-ignore
this.toastService.success(response.success);
},
error: (error) => {
this.toastService.error(error.error.error);
@ -125,11 +143,11 @@ export class PxeComponent {
case 'delete':
this.http.post(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({
next: () => {
console.log('Plantilla cambiada');
this.toastService.success('Plantilla eliminada correctamente');
this.search();
},
error: (error) => {
console.error('Error al cambiar la imagen:', error);
this.toastService.error(error.error.error);
}
});
break;
@ -139,6 +157,38 @@ export class PxeComponent {
}
}
showTemplate(event: MouseEvent, data: any): void {
event.stopPropagation();
const dialogRef = this.dialog.open(ShowTemplateContentComponent, { data: { data }, width: '700px'});
}
editClients(event: MouseEvent, data: any): void {
event.stopPropagation();
const dialogRef = this.dialog.open(ClientsComponent, { data: { data }, width: '700px'});
}
syncTemplates() {
this.http.post(`${this.apiUrl}/sync`, {})
.subscribe(response => {
this.toastService.success('Sincronización completada');
this.search()
}, error => {
console.error('Error al sincronizar', error);
this.toastService.error('Error al sincronizar');
});
}
addClientsToPxe(template: Subnet) {
const dialogRef = this.dialog.open(AddClientsToPxeComponent, {
width: '600px',
data: { subnetUuid: template.uuid, subnetName: template.name }
});
dialogRef.afterClosed().subscribe(result => {
this.search();
});
}
applyFilter() {
this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({
next: (response) => {
@ -151,38 +201,31 @@ export class PxeComponent {
});
}
loadAlert(): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/server/get-collection`);
}
openSubnetInfoDialog() {
this.loadAlert().subscribe(
response => {
this.alertMessage = response.message;
this.dialog.open(ServerInfoDialogComponent, {
width: '600px',
data: {
message: this.alertMessage
}
});
},
error => {
console.error('Error al cargar la información del alert', error);
}
);
}
onPageChange(event: PageEvent) {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.applyFilter();
}
loadAlert() {
this.http.get(`${this.apiUrl}/server/get-collection`)
.subscribe(response => {
// @ts-ignore
this.alertMessage = response.templates.length
}, error => {
console.error('Error al cargar la información del alert', error);
});
}
getIcon(): { name: string, color: string } {
if (Number(this.alertMessage) === this.length) {
return { name: 'check_circle', color: 'green' }; // Icono de check verde
} else {
return { name: 'cancel', color: 'red' }; // Icono de cruz roja
}
}
syncOgCore(): void {
this.http.post(`${this.apiUrl}/sync`, {})
.subscribe(response => {
this.toastService.success('Sincronización completada');
this.search();
}, error => {
console.error('Error al sincronizar', error);
this.toastService.error('Error al sincronizar');
});
}
}

View File

@ -0,0 +1,33 @@
.info-container {
background-color: #e8eaf6;
padding: 16px;
border-radius: 4px;
font-size: 14px;
overflow-x: auto;
border: 1px solid #c5cae9;
}
.info-container h3 {
margin-top: 0;
font-size: 16px;
color: #000000;
}
.info-container p {
margin: 8px 0;
line-height: 1.5;
}
.info-container strong {
color: #0d47a1;
}
.info-container pre {
background-color: #f5f5f5;
padding: 16px;
border-radius: 4px;
font-size: 12px;
overflow-x: auto;
white-space: pre;
border: 1px solid #dcdcdc;
}

View File

@ -0,0 +1,4 @@
<div class="info-container">
<h3>Detalles de {{ data.data.name }}</h3>
<pre class="code-block">{{ data.data.templateContent }}</pre>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ShowTemplateContentComponent } from './show-template-content.component';
describe('ShowTemplateContentComponent', () => {
let component: ShowTemplateContentComponent;
let fixture: ComponentFixture<ShowTemplateContentComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ShowTemplateContentComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ShowTemplateContentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,19 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http";
@Component({
selector: 'app-show-template-content',
templateUrl: './show-template-content.component.html',
styleUrl: './show-template-content.component.css'
})
export class ShowTemplateContentComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
displayedColumns: string[] = ['property', 'value'];
generalData: any[] = [];
constructor(
@Inject(MAT_DIALOG_DATA) public data: any,
private http: HttpClient
) {
}
}