Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop

develop-jenkins
Manuel Aranda Rosales 2024-10-22 07:29:58 +02:00
commit 8eef8cd8ec
5 changed files with 247 additions and 303 deletions

1
.gitignore vendored 100644
View File

@ -0,0 +1 @@
ogWebconsole/.env

View File

@ -1 +1 @@
NG_APP_BASE_API_URL=http://127.0.0.1:8001 NG_APP_BASE_API_URL=https://localhost:8443

View File

@ -1,90 +1,70 @@
.header-container { .global-selectors {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
height: 100px; margin-bottom: 15px;
padding: 10px; gap: 10px;
margin-top: 16px;
} }
.title { .global-selectors mat-form-field {
font-size: 24px; flex: 1;
margin-right: 10px;
} }
.templates-button-row { .global-selectors button {
height: 40px;
padding: 0 20px;
}
.filters-container {
display: flex; display: flex;
justify-content: flex-start; justify-content: space-between;
margin-top: 16px; align-items: flex-end;
gap: 10px;
margin-bottom: 15px;
}
.filters-container mat-form-field {
flex: 1;
}
.mat-elevation-z8 {
margin-top: 15px;
}
.mat-table {
width: 100%;
}
mat-header-cell, mat-cell {
text-align: center;
font-size: 14px;
padding: 8px;
}
mat-form-field {
width: 100%;
}
.mat-paginator {
margin-top: 20px;
display: flex;
justify-content: center;
}
mat-select, mat-input {
font-size: 14px;
}
.mat-form-field-appearance-fill .mat-form-field-flex {
padding: 8px 0;
}
button.mat-flat-button {
padding: 8px 16px;
font-size: 14px;
} }
.divider { .divider {
margin: 20px 0; 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;
}
table {
width: 100%;
margin-top: 50px;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 5px;
box-sizing: border-box;
}
.search-string {
flex: 2;
padding: 5px;
}
.search-entity {
flex: 1;
padding: 5px;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
}

View File

@ -2,12 +2,7 @@
<mat-expansion-panel hideToggle> <mat-expansion-panel hideToggle>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> Sincronización ogBoot </mat-panel-title> <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-expansion-panel-header> </mat-expansion-panel-header>
<p *ngIf="alertMessage">Oglives creados en servidor ogBoot: {{ alertMessage }}</p>
<p *ngIf="alertMessage">Oglives creados en servidor ogCore (base de datos): {{ length }}</p>
<div class="example-button-row"> <div class="example-button-row">
<button mat-flat-button color="primary" (click)="syncOgCore()"> Sincronizar OgCore</button> <button mat-flat-button color="primary" (click)="syncOgCore()"> Sincronizar OgCore</button>
@ -16,67 +11,78 @@
</mat-accordion> </mat-accordion>
<div class="header-container"> <div class="header-container">
<h2 class="title" i18n="@@adminPXETitle">Administrar ficheros de arranque PXE</h2> <h2 class="title">Administrar ficheros de arranque PXE</h2>
</div> </div>
<mat-divider class="divider"></mat-divider> <div [formGroup]="taskForm" class="filters-container">
<div class="search-container"> <mat-form-field appearance="fill" class="full-width">
<mat-form-field appearance="fill" class="search-entity"> <mat-label>Selecciona Unidad Organizacional</mat-label>
<mat-label i18n="@@searchLabel">Plantilla</mat-label> <mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
<mat-select [(ngModel)]="filters['template']" (selectionChange)="search()" placeholder="Seleccionar entidad"> <mat-option *ngIf="loadingUnits" disabled>Cargando unidades...</mat-option>
<mat-option [value]="''">Todos</mat-option> <mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
<mat-option *ngFor="let template of templateOptions" [value]="template.id">{{ template.name }}</mat-option> {{ unit.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="taskForm.get('organizationalUnit')?.invalid">Este campo es obligatorio</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Selecciona aula</mat-label>
<mat-select formControlName="selectedChild" (selectionChange)="onChildChange()">
<mat-option *ngIf="selectedUnitChildren.length === 0" disabled>No hay aulas disponibles</mat-option>
<mat-option *ngFor="let child of selectedUnitChildren" [value]="child['@id']">
{{ child.name }}
</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<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 element">
<ng-container *ngIf="column.columnDef === 'clients'">
<button mat-icon-button [matMenuTriggerFor]="clientsMenu">
<mat-icon>visibility</mat-icon>
</button>
<mat-menu #clientsMenu="matMenu">
<ng-container *ngFor="let client of column.cell(element)">
<button mat-menu-item>
<mat-list>
<mat-list-item lines="3">
<span matListItemTitle>{{ client.name }}</span>
<span matListItemLine>{{ client.ip }}</span>
<span matListItemLine>{{client.mac}}</span>
</mat-list-item>
</mat-list>
</button>
</ng-container>
</mat-menu>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'clients'">
{{column.cell(element)}}
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions"> <mat-divider class="divider"></mat-divider>
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions">Acciones</th>
<td mat-cell *matCellDef="let image">
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="toggleAction(image, 'delete')">Eliminar</button>
</mat-menu>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <div class="global-selectors">
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <mat-form-field appearance="fill">
</table> <mat-label>Global ogLive</mat-label>
<div class="paginator-container"> <mat-select [(value)]="globalOgLive">
<mat-paginator [length]="length" <mat-option *ngFor="let option of ogLiveOptions" [value]="option['@id']">{{ option.name }}</mat-option>
[pageSize]="itemsPerPage" </mat-select>
[pageIndex]="page" </mat-form-field>
[pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)"> <button mat-flat-button color="primary" (click)="applyToAll()">Aplicar a todos</button>
</mat-paginator>
</div> </div>
<mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef> Id </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.id}} </mat-cell>
</ng-container>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef> Nombre </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
</ng-container>
<ng-container matColumnDef="ogLive">
<mat-header-cell *matHeaderCellDef> Plantilla </mat-header-cell>
<mat-cell *matCellDef="let element">
<mat-form-field>
<mat-select [(value)]="element.ogLive">
<mat-option *ngFor="let option of ogLiveOptions" [value]="option['@id']">{{ option.name }}</mat-option>
</mat-select>
</mat-form-field>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
<div class="save-button"
>
<button mat-flat-button color="primary" (click)="saveOgLiveTemplates()">Guardar</button>
</div>
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>

View File

@ -1,198 +1,155 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import {MatTableDataSource} from "@angular/material/table"; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {MatDialog} from "@angular/material/dialog"; import { HttpClient } from '@angular/common/http';
import {HttpClient} from "@angular/common/http"; import { ToastrService } from 'ngx-toastr';
import { CreatePxeTemplateComponent } from '../pxe/create-pxeTemplate/create-pxe-template.component'; import { PageEvent } from '@angular/material/paginator';
import {PageEvent} from "@angular/material/paginator";
import {ToastrService} from "ngx-toastr";
import {DatePipe} from "@angular/common";
import {DataService} from "./data.service";
@Component({ @Component({
selector: 'app-pxe-boot-files', selector: 'app-pxe-boot-files',
templateUrl: './pxe-boot-files.component.html', templateUrl: './pxe-boot-files.component.html',
styleUrl: './pxe-boot-files.component.css' styleUrls: ['./pxe-boot-files.component.css']
}) })
export class PxeBootFilesComponent { export class PxeBootFilesComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
pxeTemplates: any[] = [];
currentPage: number = 1;
dataSource = new MatTableDataSource<any>();
length: number = 0;
alertMessage: string | null = null;
itemsPerPage: number = 10;
loading:boolean = false;
filters: { [key: string]: string } = {};
page: number = 1;
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
datePipe: DatePipe = new DatePipe('es-ES');
selectedElements: string[] = [];
templateOptions: any[] = [];
columns = [
{
columnDef: 'id',
header: 'ID',
cell: (user: any) => `${user.id}`
},
{
columnDef: 'templateName',
header: 'Nombre de la plantilla',
cell: (user: any) => `${user.template.name}`
},
{
columnDef: 'clients',
header: 'Clientes',
cell: (user: any) => user.clients
},
{
columnDef: 'createdAt',
header: 'Fecha de creación',
cell: (user: any) => `${this.datePipe.transform(user.createdAt, 'dd/MM/yyyy hh:mm:ss')}`
}
];
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/pxe-boot-files`; availableOrganizationalUnits: any[] = [];
selectedUnitChildren: any[] = [];
dataSource: any[] = [];
taskForm: FormGroup;
loadingUnits: boolean = false;
ogLiveOptions: any[] = [];
globalOgLive: string | null = null;
length: number = 0;
itemsPerPage: number = 10;
page: number = 0;
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
filters: any = {};
displayedColumns: string[] = ['id', 'name', 'ogLive'];
constructor( constructor(
public dialog: MatDialog, private fb: FormBuilder,
private http: HttpClient, private http: HttpClient,
private toastService: ToastrService, private toastService: ToastrService
private dataService: DataService ) {
) this.taskForm = this.fb.group({
{ } organizationalUnit: ['', Validators.required],
selectedChild: ['', Validators.required]
});
}
ngOnInit(): void { ngOnInit(): void {
this.search(); this.fetchOrganizationalUnits();
this.loadAlert(); this.fetchPxeTemplates();
this.dataService.getPxeTemplateOptions().subscribe(
data => {
this.templateOptions = data;
},
error => {
console.error('Error fetching template options', error);
}
);
} }
search(): void { fetchOrganizationalUnits(): void {
this.loading = true; this.loadingUnits = true;
this.dataService.getPxeBootFiles(this.filters).subscribe( this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`)
data => { .subscribe(
this.dataSource.data = data; response => {
this.loading = false; this.availableOrganizationalUnits = response['hydra:member'].filter((item: any) => item.type === 'organizational-unit');
}, this.loadingUnits = false;
error => { },
console.error('Error fetching pxe boot files', error); error => {
this.loading = false; this.toastService.error('Error al cargar las unidades organizativas');
} this.loadingUnits = false;
); }
);
} }
toggleAction(image: any, action:string): void { fetchPxeTemplates(): void {
switch (action) { this.http.get<any>(`${this.baseUrl}/pxe-templates?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters })
case 'create': .subscribe(
this.http.post(`${this.apiUrl}/server/${image.uuid}/post`, {}).subscribe({ response => {
next: () => { this.ogLiveOptions = response['hydra:member'];
console.log('Plantilla cambiada'); this.length = response['hydra:totalItems'];
this.search(); },
}, error => {
error: (error) => { this.toastService.error('Error al cargar las plantillas');
console.error('Error al cambiar la imagen:', error); }
} );
}); }
break;
case 'delete': onOrganizationalUnitChange(): void {
this.http.post(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({ const selectedUnitId = this.taskForm.get('organizationalUnit')?.value;
next: () => { const selectedUnit = this.availableOrganizationalUnits.find(unit => unit['@id'] === selectedUnitId);
console.log('Plantilla cambiada'); if (selectedUnit && selectedUnit.children) {
this.search(); this.selectedUnitChildren = selectedUnit.children.filter((child: any) => child.type === 'classroom');
}, } else {
error: (error) => { this.selectedUnitChildren = [];
console.error('Error al cambiar la imagen:', error);
}
});
break;
default:
console.error('Acción no soportada:', action);
break;
} }
} }
deletePxeTemplate(uuid: string) { onChildChange(): void {
// Lógica para eliminar una plantilla const selectedChildId = this.taskForm.get('selectedChild')?.value;
const selectedChild = this.selectedUnitChildren.find(child => child['@id'] === selectedChildId);
if (selectedChild && selectedChild.clients) {
this.dataSource = selectedChild.clients;
} else {
this.dataSource = [];
}
} }
editPxeTemplate(template: any) { applyToAll(): void {
const dialogRef = this.dialog.open(CreatePxeTemplateComponent, { this.dataSource = this.dataSource.map(client => ({
data: template ...client,
}); ogLive: this.globalOgLive || client.ogLive
}));
dialogRef.afterClosed().subscribe(() => {
this.search();
});
} }
/* editPxeBootFile(bootFile: any) { saveOgLiveTemplates(): void {
const dialogRef = this.dialog.open(CreatePxeBootFileComponent, { const groupedByTemplate: { [key: string]: string[] } = {};
data: {
clients: bootFile.clients, this.dataSource.forEach(client => {
bootFile: bootFile if (client.ogLive) {
if (!groupedByTemplate[client.ogLive]) {
groupedByTemplate[client.ogLive] = [];
}
groupedByTemplate[client.ogLive].push(client['@id']);
} }
}); });
dialogRef.afterClosed().subscribe(result => { Object.keys(groupedByTemplate).forEach(templateId => {
if (result) { const clients = groupedByTemplate[templateId];
this.loadPxeBootFiles(); // Refrescar la lista después de la edición const url = `${this.baseUrl}${templateId}/add-clients`;
}
});
} */
applyFilter() { const payload = {
this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({ clients: clients
next: (response) => { };
this.dataSource.data = response['hydra:member'];
this.length = response['hydra:totalItems']; console.log('Payload:', url);
}, this.http.post(url, payload).subscribe({
error: (error) => { next: () => {
console.error('Error al cargar las imágenes:', error); this.toastService.success(`Clientes guardados correctamente para la plantilla ${templateId}`);
} },
error: () => {
this.toastService.error(`Error al guardar clientes para la plantilla ${templateId}`);
}
});
}); });
} }
onPageChange(event: PageEvent) { onPageChange(event: PageEvent): void {
this.page = event.pageIndex; this.page = event.pageIndex;
this.itemsPerPage = event.pageSize; this.itemsPerPage = event.pageSize;
this.applyFilter(); this.fetchPxeTemplates();
}
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 { syncOgCore(): void {
this.http.post(`${this.apiUrl}/sync`, {}) this.http.post(`${this.baseUrl}/sync`, {}).subscribe(
.subscribe(response => { () => {
this.toastService.success('Sincronización completada'); this.toastService.success('Sincronización completada');
this.search() },
}, error => { error => {
console.error('Error al sincronizar', error);
this.toastService.error('Error al sincronizar'); this.toastService.error('Error al sincronizar');
}); }
);
} }
} }