refs #2182. Changed Output messages

pull/26/head
Manuel Aranda Rosales 2025-06-09 12:05:35 +02:00
parent 06d0a83aab
commit b4ba0b1244
41 changed files with 618 additions and 87 deletions

View File

@ -153,6 +153,7 @@ import { ClientTaskLogsComponent } from './components/task-logs/client-task-logs
import { BootSoPartitionComponent } from './components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component';
import { RemoveCacheImageComponent } from './components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component';
import { ChangeParentComponent } from './components/groups/shared/change-parent/change-parent.component';
import { SoftwareProfilePartitionComponent } from './components/commands/main-commands/execute-command/software-profile-partition/software-profile-partition.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
@ -263,7 +264,8 @@ registerLocaleData(localeEs, 'es-ES');
ClientTaskLogsComponent,
BootSoPartitionComponent,
RemoveCacheImageComponent,
ChangeParentComponent
ChangeParentComponent,
SoftwareProfilePartitionComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,

View File

@ -7,6 +7,7 @@ import { BootSoPartitionComponent } from "./boot-so-partition/boot-so-partition.
import { MatDialog } from "@angular/material/dialog";
import { RemoveCacheImageComponent } from "./remove-cache-image/remove-cache-image.component";
import { AuthService } from '@services/auth.service';
import {SoftwareProfilePartitionComponent} from "./software-profile-partition/software-profile-partition.component";
@Component({
selector: 'app-execute-command',
@ -33,7 +34,7 @@ export class ExecuteCommandComponent implements OnInit {
{ translationKey: 'executeCommands.deployImage', slug: 'deploy-image', disabled: false },
{ translationKey: 'executeCommands.deleteImageCache', slug: 'remove-cache-image', disabled: false },
{ translationKey: 'executeCommands.partition', slug: 'partition', disabled: false },
{ translationKey: 'executeCommands.softwareInventory', slug: 'software-inventory', disabled: true },
{ translationKey: 'executeCommands.softwareInventory', slug: 'software-inventory', disabled: false },
{ translationKey: 'executeCommands.hardwareInventory', slug: 'hardware-inventory', disabled: true },
{ translationKey: 'executeCommands.runScript', slug: 'run-script', disabled: false },
];
@ -111,10 +112,10 @@ export class ExecuteCommandComponent implements OnInit {
if (states[0] === 'off' || states[0] === 'disconnected') {
command.disabled = command.slug !== 'power-on';
} else {
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'remove-cache-image', 'partition', 'run-script'].includes(command.slug);
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'remove-cache-image', 'partition', 'run-script', 'software-inventory'].includes(command.slug);
}
} else {
if (command.slug === 'create-image') {
if (command.slug === 'create-image'|| command.slug === 'software-inventory') {
command.disabled = multipleClients;
} else if (
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'remove-cache-image', 'run-script'].includes(command.slug)
@ -164,6 +165,14 @@ export class ExecuteCommandComponent implements OnInit {
if (action === 'remove-cache-image') {
this.removeImageCache();
}
if (action === 'hardware-inventory') {
this.hardwareInventory();
}
if (action === 'software-inventory') {
this.softwareInventory();
}
}
rebootClient(): void {
@ -174,7 +183,7 @@ export class ExecuteCommandComponent implements OnInit {
this.toastService.success('Cliente actualizado correctamente');
},
error => {
this.toastService.error('Error de conexión con el cliente');
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
}
);
}
@ -227,6 +236,55 @@ export class ExecuteCommandComponent implements OnInit {
});
}
hardwareInventory(): void {
if (this.clientData.length === 0) {
this.toastService.error('No hay clientes seleccionados');
return;
}
const clientId = this.clientData[0].uuid;
this.http.post(`${this.baseUrl}/clients/server/${clientId}/hardware-inventory`, {
clients: this.clientData.map((client: any) => client['@id'])
}).subscribe(
response => {
this.toastService.success('Inventario de hardware actualizado correctamente');
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
}
);
}
softwareInventory(): void {
if (this.clientData.length === 0) {
this.toastService.error('No hay clientes seleccionados');
return;
}
const clientDataToSend = {
clientId: this.clientData[0].uuid,
name: this.clientData[0].name,
mac: this.clientData[0].mac,
status: this.clientData[0].status,
partitions: this.clientData[0].partitions,
firmwareType: this.clientData[0].firmwareType,
ip: this.clientData[0].ip
}
const clientId = this.clientData[0].uuid;
const dialogRef = this.dialog.open(SoftwareProfilePartitionComponent, {
width: '70vw',
height: 'auto',
data: { client: clientDataToSend }
});
dialogRef.afterClosed().subscribe(result => {
});
}
powerOnClient(): void {
this.http.post(`${this.baseUrl}/image-repositories/wol`, {
clients: this.clientData.map((client: any) => client['@id'])
@ -235,7 +293,7 @@ export class ExecuteCommandComponent implements OnInit {
this.toastService.success('Petición de encendido enviada correctamente');
},
error => {
this.toastService.error('Error de conexión con el cliente');
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
}
);
}
@ -248,7 +306,7 @@ export class ExecuteCommandComponent implements OnInit {
this.toastService.success('Petición de apagado enviada correctamente');
},
error => {
this.toastService.error('Error de conexión con el cliente');
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
}
);
}

View File

@ -0,0 +1,117 @@
.dialog-content {
display: flex;
flex-direction: column;
padding: 40px;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
.select-container {
margin-top: 20px;
align-items: center;
padding: 20px;
box-sizing: border-box;
}
.clients-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.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;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
&:hover {
background-color: #f0f0f0;
transform: scale(1.02);
}
}
.button-row {
display: flex;
padding-right: 1em;
}
.action-button {
margin-top: 10px;
margin-bottom: 10px;
}
.client-item {
position: relative;
}
.mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}
.selected-client {
background-color: #a0c2e5 !important;
color: white !important;
}
.loading-spinner {
display: block;
margin: 0 auto;
align-items: center;
justify-content: center;
}
.client-details {
margin-top: 4px;
}
.client-name {
font-size: 0.9em;
font-weight: 600;
color: #333;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
display: inline-block;
}
.client-ip {
display: block;
font-size: 0.9em;
color: #666;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
@media (max-width: 600px) {
.form-field {
width: 100%;
}
.dialog-actions {
flex-direction: column;
align-items: stretch;
}
button {
width: 100%;
margin-left: 0;
margin-bottom: 8px;
}
}

View File

@ -0,0 +1,55 @@
<h2 mat-dialog-title> Seleccionar partición para inventariar</h2>
<mat-dialog-content class="dialog-content">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
<div *ngIf="!loading" class="partition-table-container">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar imagen</th>
<td mat-cell *matCellDef="let row">
<mat-radio-group
[(ngModel)]="selectedPartition"
>
<mat-radio-button [value]="row">
</mat-radio-button>
</mat-radio-group>
</td>
</ng-container>
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef !== 'size' && column.columnDef !== 'operativeSystem'">
{{ column.cell(image) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'size'">
<div style="display: flex; flex-direction: column;">
<span> {{ image.size }} MB</span>
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef === 'operativeSystem'">
<div style="display: flex; flex-direction: column;">
<span> {{ image.operativeSystem?.name }} </span>
<span style="font-size: 0.75rem; color: gray;">{{ image.image?.name}} </span>
</div>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-dialog-content>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">Cancelar</button>
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
</div>

View File

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

View File

@ -0,0 +1,107 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ConfigService} from "@services/config.service";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-software-profile-partition',
templateUrl: './software-profile-partition.component.html',
styleUrl: './software-profile-partition.component.css'
})
export class SoftwareProfilePartitionComponent implements OnInit{
baseUrl: string;
selectedPartition: any = null;
dataSource = new MatTableDataSource<any>();
clientId: string | null = null;
selectedClients: any[] = [];
selectedModelClient: any = null;
filteredPartitions: any[] = [];
allSelected: boolean = false;
clientData: any[] = [];
loading: boolean = false;
columns = [
{
columnDef: 'diskNumber',
header: 'Disco',
cell: (partition: any) => partition.diskNumber
},
{
columnDef: 'partitionNumber',
header: 'Particion',
cell: (partition: any) => partition.partitionNumber
},
{
columnDef: 'size',
header: 'Tamaño',
cell: (partition: any) => `${partition.size} MB`
},
{
columnDef: 'partitionCode',
header: 'Tipo de partición',
cell: (partition: any) => partition.partitionCode
},
{
columnDef: 'filesystem',
header: 'Sistema de ficheros',
cell: (partition: any) => partition.filesystem
},
{
columnDef: 'operativeSystem',
header: 'SO',
cell: (partition: any) => partition.operativeSystem?.name
}
];
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
constructor(
@Inject(MAT_DIALOG_DATA) public data: { client: any },
private dialogRef: MatDialogRef<SoftwareProfilePartitionComponent>,
private configService: ConfigService,
private http: HttpClient,
private toastService: ToastrService,
) {
this.baseUrl = this.configService.apiUrl;
this.clientId = this.data.client.clientId
}
ngOnInit() {
this.loadPartitions();
}
loadPartitions() {
const url = `${this.baseUrl}/clients/${this.data.client.clientId}`;
this.http.get(url).subscribe(
(response: any) => {
if (response.partitions) {
this.dataSource.data = response.partitions;
}
},
(error) => {
console.error('Error al cargar los datos del cliente:', error);
}
);
}
close() {
this.dialogRef.close();
}
execute(): void {
this.loading = true;
this.http.post(`${this.baseUrl}/clients/server/${this.data.client.clientId}/software-inventory`, {
partition: this.selectedPartition['@id'],
}).subscribe(
response => {
this.toastService.success('Inventario de software actualizado correctamente');
this.dialogRef.close(response);
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error al actualizar el inventario de software');
}
);
}
}

View File

@ -69,7 +69,7 @@ export class GlobalStatusComponent implements OnInit {
},
error: (error) => {
clearTimeout(timeoutId);
this.toastService.error('Error al sincronizar las subredes DHCP');
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las subredes');
}
});
}
@ -85,7 +85,7 @@ export class GlobalStatusComponent implements OnInit {
this.toastService.success('Sincronización de las plantillas Pxe completada');
}, error => {
clearTimeout(timeoutId);
this.toastService.error('Error al sincronizar las plantillas Pxe');
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las plantillas Pxe');
});
}
@ -100,7 +100,7 @@ export class GlobalStatusComponent implements OnInit {
this.toastService.success('Sincronización con los ogLives completada');
}, error => {
clearTimeout(timeoutId);
this.toastService.error('Error al sincronizar imágenes ogLive');
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las imagenes ogLive');
});
}
@ -144,7 +144,7 @@ export class GlobalStatusComponent implements OnInit {
clearTimeout(timeoutId);
},
error: error => {
console.log(error);
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado de ogBoot');
this.loading = false;
this[errorState] = true;
clearTimeout(timeoutId);
@ -217,7 +217,7 @@ export class GlobalStatusComponent implements OnInit {
callback(false);
},
error => {
console.error(`Error fetching status for repository ${repositoryUuid}`, error);
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del repositorio');
clearTimeout(timeoutId);
callback(true);
}

View File

@ -196,3 +196,24 @@ mat-option .unit-name {
justify-content: space-between;
align-items: center;
}
.instructions-box {
margin-top: 15px;
background-color: #f5f5f5;
border: 1px solid #ccc;
padding: 15px;
border-radius: 6px;
}
.instructions-textarea textarea {
font-family: monospace;
white-space: pre;
}
.instructions-card {
background-color: #f5f5f5;
box-shadow: none !important;
margin-top: 15px;
}

View File

@ -15,6 +15,12 @@
(click)="save()">Ejecutar</button>
</div>
<div class="button-row">
<button class="action-button" (click)="generateOgInstructions()">
Generar instrucciones
</button>
</div>
<div>
<button mat-stroked-button color="accent"
[disabled]="!isFormValid()"
@ -118,6 +124,20 @@
</div>
<div class="partition-table-container">
<div *ngIf="showInstructions" class="instructions-box">
<mat-card class="instructions-card">
<mat-card-title>
Instrucciones generadas
<button mat-icon-button (click)="showInstructions = false" style="float: right;">
<mat-icon>close</mat-icon>
</button>
</mat-card-title>
<mat-card-content>
<pre>{{ ogInstructions }}</pre>
</mat-card-content>
</mat-card>
</div>
<table mat-table [dataSource]="filteredPartitions" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef style="text-align: start">Seleccionar partición</th>

View File

@ -37,6 +37,9 @@ export class DeployImageComponent implements OnInit{
loading: boolean = false;
allSelected = true;
runScriptContext: any = null;
ogInstructions: string = '';
deployImage: boolean = true;
showInstructions: boolean = false;
protected p2pModeOptions = [
{ name: 'Leecher', value: 'leecher' },
@ -395,4 +398,39 @@ export class DeployImageComponent implements OnInit{
}
});
}
generateOgInstructions() {
let script = '';
const disk = this.selectedPartition?.disk;
const partition = this.selectedPartition?.partition;
let ip = this.selectedImage?.repository?.ip || 'REPO';
let imgName = this.selectedImage?.canonicalName || '';
let target = ` ${disk} ${partition}`;
let log = `ogEcho log session "[0] $MSG_SCRIPTS_TASK_START `;
if (this.deployImage) {
script = 'deployImage ';
} else {
script = 'updateCache ';
imgName += '.img';
target = '';
}
script += `${ip} /${imgName}${target} ${this.selectedMethod}`;
log += `${script}"\n`;
script = log + script;
let params = '';
if (['udpcast', 'uftp', 'udpcast-direct'].includes(<string>this.selectedMethod)) {
params = `${this.mcastPort}:${this.mcastMode}:${this.mcastIp}:${this.mcastSpeed}M:${this.mcastMaxClients}:${this.mcastMaxTime}`;
} else if (this.selectedMethod === 'p2p') {
params = `${this.p2pMode}:${this.p2pTime}`;
}
script += ` ${params}`;
this.ogInstructions = script;
this.showInstructions = true
}
}

View File

@ -280,5 +280,12 @@ button.remove-btn:hover {
white-space: pre;
}
.instructions-card {
background-color: #f5f5f5;
box-shadow: none !important;
margin-top: 15px;
}

View File

@ -92,11 +92,18 @@
</div>
<div class="partition-assistant" *ngIf="selectedDisk">
<div *ngIf="generatedInstructions" class="instructions-box">
<mat-form-field class="instructions-textarea" appearance="fill" style="width: 100%;">
<textarea matInput rows="10" readonly [value]="generatedInstructions"></textarea>
</mat-form-field>
<div *ngIf="showInstructions" class="instructions-box">
<mat-card class="instructions-card">
<mat-card-title>
Instrucciones generadas
<button mat-icon-button (click)="showInstructions = false" style="float: right;">
<mat-icon>close</mat-icon>
</button>
</mat-card-title>
<mat-card-content>
<pre>{{ generatedInstructions }}</pre>
</mat-card-content>
</mat-card>
</div>
<div class="row-button">
@ -107,7 +114,6 @@
<mat-chip color="info" *ngIf="partitionCode">
Tabla de particiones: {{ partitionCode }}
</mat-chip>
</div>
<mat-divider style="padding: 10px;"></mat-divider>

View File

@ -44,6 +44,7 @@ export class PartitionAssistantComponent implements OnInit{
clientData: any = [];
loading: boolean = false;
runScriptContext: any = null;
showInstructions = false;
view: [number, number] = [400, 300];
showLegend = true;
@ -509,5 +510,6 @@ export class PartitionAssistantComponent implements OnInit{
instructions += `ogExecAndLog command session ogListPartitions ${diskNumber}\n`;
this.generatedInstructions = instructions;
this.showInstructions = true
}
}

View File

@ -265,8 +265,7 @@
<div *ngFor="let client of arrayClients" class="client-item">
<div class="client-card">
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(client)"
[checked]="selection.isSelected(client)"
[disabled]="client.status === 'busy' || client.status === 'off' || client.status === 'disconnected'">
[checked]="selection.isSelected(client)">
</mat-checkbox>
<img style="margin-top: 0.5em;" [src]="'assets/images/computer_' + client.status + '.svg'"
alt="Client Icon" class="client-image" />
@ -341,8 +340,7 @@
</th>
<td mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(row)"
[checked]="selection.isSelected(row)"
[disabled]="row.status === 'busy' || row.status === 'off' || row.status === 'disconnected'">
[checked]="selection.isSelected(row)">
</mat-checkbox>
</td>
</ng-container>

View File

@ -587,11 +587,11 @@ export class GroupsComponent implements OnInit, OnDestroy {
onRoomMap(room: TreeNode | null): void {
if (!room || !room['@id']) return;
this.subscriptions.add(
this.http.get<{ clients: Client[] }>(`${this.baseUrl}${room['@id']}`).subscribe(
(response) => {
this.http.get<{ clients: Client[] }>(`${this.baseUrl}/clients?organizationalUnit.id=${room.id}`).subscribe(
(response: any) => {
this.dialog.open(ClassroomViewDialogComponent, {
width: '90vw',
data: { clients: response.clients },
data: { clients: response['hydra:member'] },
});
},
(error) => {
@ -750,8 +750,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.syncingClientId = null;
this.refreshData(parentNodeId)
},
() => {
this.toastr.error('Error de conexión con el cliente');
(error) => {
this.toastr.error(error.error['hydra:description'] || 'Error al actualizar el cliente');
this.syncStatus = false;
this.syncingClientId = null;
this.refreshData(parentNodeId)

View File

@ -72,7 +72,6 @@ export class ClassroomViewComponent implements OnInit, OnChanges {
}
handleClientClick(client: any): void {
console.log('Client clicked:', client);
this.dialog.open(ClientViewComponent, { data: { client }, width: '800px', height: '700px' });
}
@ -108,4 +107,4 @@ export class ClassroomViewComponent implements OnInit, OnChanges {
} else
this.toastService.success('Cliente actualizado!', 'Éxito');
}
}
}

View File

@ -60,7 +60,8 @@ export class OgbootStatusComponent implements OnInit {
this.loading = false;
}, error => {
this.toastService.error('Error al sincronizar con el el servicio de og-boot');
console.log(error)
this.toastService.error(error.error['hydra:description']);
this.loading = false;
});
}

View File

@ -43,10 +43,12 @@ export class CreatePXEImageComponent implements OnInit {
next: (response: any) => {
this.loading = false;
this.downloads = response.message;
this.loading = false;
},
error: (error) => {
console.error('Error fetching downloads:', error);
this.toastService.error('Error fetching iso files');
this.loading = false;
this.toastService.error(error.error['hydra:description'] || 'Error fetching downloads');
}
});
}

View File

@ -8,7 +8,7 @@
</h2>
</div>
<div class="images-button-row">
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openSubnetInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
<button class="action-button" (click)="addImage()" joyrideStep="addImageStep"
[text]="'addOgLiveButtonDescription' | translate">
{{ 'addOgLiveButton' | translate }}

View File

@ -76,10 +76,8 @@ export class PXEimagesComponent implements OnInit {
}
ngOnInit(): void {
this.loading = true;
this.search();
this.loadAlert();
this.loading = false;
}
addImage(): void {
@ -204,7 +202,8 @@ export class PXEimagesComponent implements OnInit {
return this.http.get<any>(`${this.apiUrl}/server/get-collection`);
}
openSubnetInfoDialog() {
openInfoDialog() {
this.loading = true;
this.loadAlert().subscribe(
response => {
this.alertMessage = response.message;
@ -215,10 +214,12 @@ export class PXEimagesComponent implements OnInit {
message: this.alertMessage
}
});
this.loading = false
},
error => {
this.toastService.error(error.error['hydra:description']);
console.error('Error al cargar la información del alert', error);
this.loading = false;
}
);
}

View File

@ -77,10 +77,8 @@ export class PxeComponent implements OnInit{
}
ngOnInit(): void {
this.loading = true;
this.search();
this.loadAlert()
this.loading = false;
}
search(): void {

View File

@ -1,5 +1,3 @@
<app-loading [isLoading]="loading"></app-loading>
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>

View File

@ -183,6 +183,7 @@ export class OgDhcpSubnetsComponent implements OnInit {
}
openSubnetInfoDialog() {
this.loading = true;
this.loadAlert().subscribe(
response => {
this.alertMessage = response.message;
@ -193,10 +194,12 @@ export class OgDhcpSubnetsComponent implements OnInit {
message: this.alertMessage
}
});
this.loading = false;
},
error => {
this.toastService.error(error.error['hydra:description']);
console.error('Error al cargar la información del alert', error);
this.loading = false;
}
);
}

View File

@ -37,3 +37,8 @@ mat-dialog-actions {
.selected-item button {
margin-left: 10px;
}
mat-spinner {
margin: 0 auto;
align-self: center;
}

View File

@ -1,7 +1,8 @@
<h2 mat-dialog-title>Convertir imagen en virtual </h2>
<mat-dialog-content>
<mat-form-field appearance="fill" class="full-width">
<mat-spinner *ngIf="loading" class="loading-spinner"></mat-spinner>
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">
<mat-label>Extension</mat-label>
<input matInput [(ngModel)]="extension" placeholder="Introduzca la extensión de la imagen a convertir."
/>

View File

@ -1,4 +1,4 @@
import {Component, Inject} from '@angular/core';
import {Component, Inject, OnInit} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ToastrService} from "ngx-toastr";
@ -10,9 +10,9 @@ import {ConfigService} from "@services/config.service";
templateUrl: './convert-image-to-virtual.component.html',
styleUrl: './convert-image-to-virtual.component.css'
})
export class ConvertImageToVirtualComponent {
export class ConvertImageToVirtualComponent implements OnInit {
baseUrl: string;
loading: boolean = true;
loading: boolean = false;
extension: string = '';
constructor(
@ -27,25 +27,27 @@ export class ConvertImageToVirtualComponent {
}
ngOnInit(): void {
this.loading = true;
}
save() {
this.loading = true;
this.http.post<any>(`${this.baseUrl}${this.data.imageImageRepository['@id']}/convert-image-to-virtual`, {
extension: this.extension
}).subscribe({
next: (response) => {
this.toastService.success('Peticion de conversion de imagen enviada correctamente');
this.dialogRef.close();
this.dialogRef.close(true);
this.loading = false;
this.router.navigate(['/commands-logs']);
},
error: error => {
this.loading = false;
this.toastService.error(error.error['hydra:description']);
}
});
}
close() {
this.dialogRef.close();
this.dialogRef.close(true);
}
}

View File

@ -37,3 +37,9 @@ mat-dialog-actions {
.selected-item button {
margin-left: 10px;
}
mat-spinner {
margin: 0 auto;
align-self: center;
}

View File

@ -1,15 +1,17 @@
<h2 mat-dialog-title>Convertir imagen virtual </h2>
<mat-dialog-content>
<p >Repositorio destino: {{ data.name }}</p>
<mat-spinner *ngIf="loading" class="loading-spinner"></mat-spinner>
<mat-form-field appearance="fill" class="full-width">
<p *ngIf="!loading" >Repositorio destino: {{ data.name }}</p>
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">
<mat-label>Imagen</mat-label>
<input matInput [(ngModel)]="imageName" placeholder="Introduzca el nombre de la imagen a importar."
/>
<mat-hint>El nombre de la imagen tiene que ir con la extensión. </mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">
<mat-label>Sistema de archivos</mat-label>
<input matInput [(ngModel)]="filesystem" placeholder="Introduzca el sistema de archivos."/>
</mat-form-field>

View File

@ -1,4 +1,4 @@
import { Component, Inject } from '@angular/core';
import {Component, Inject, OnInit} from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { ToastrService } from "ngx-toastr";
@ -10,9 +10,9 @@ import { ConfigService } from "@services/config.service";
templateUrl: './convert-image.component.html',
styleUrl: './convert-image.component.css'
})
export class ConvertImageComponent {
export class ConvertImageComponent implements OnInit{
baseUrl: string;
loading: boolean = true;
loading: boolean = false;
imageName: string = '';
filesystem: string = '';
@ -28,27 +28,28 @@ export class ConvertImageComponent {
}
ngOnInit(): void {
this.loading = true;
}
save() {
console.log(this.data?.repositoryUuid)
this.loading = true;
this.http.post<any>(`${this.baseUrl}/image-repositories/${this.data?.repositoryUuid}/convert-image`, {
name: this.imageName,
filesystem: this.filesystem
}).subscribe({
next: (response) => {
this.toastService.success('Peticion de conversion de imagen enviada correctamente');
this.dialogRef.close();
this.dialogRef.close(true);
this.loading = false;
this.router.navigate(['/commands-logs']);
},
error: error => {
this.loading = false;
this.toastService.error(error.error['hydra:description']);
}
});
}
close() {
this.dialogRef.close();
this.dialogRef.close(true);
}
}

View File

@ -28,8 +28,8 @@ mat-dialog-actions {
.selected-item {
display: flex;
justify-content: space-between; /* Alinea texto a la izquierda y botón a la derecha */
align-items: center; /* Centra verticalmente */
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #ccc;
}
@ -37,3 +37,8 @@ mat-dialog-actions {
.selected-item button {
margin-left: 10px;
}
mat-spinner {
margin: 0 auto;
align-self: center;
}

View File

@ -1,7 +1,8 @@
<h2 mat-dialog-title>Importar imagenes a {{ data.name }}</h2>
<mat-dialog-content>
<mat-form-field appearance="fill" class="full-width">
<mat-spinner *ngIf="loading" class="loading-spinner"></mat-spinner>
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">
<mat-label>Imagen</mat-label>
<input matInput [(ngModel)]="imageName" placeholder="Introduzca el nombre de la imagen a importar."
/>

View File

@ -12,7 +12,7 @@ import { ConfigService } from '@services/config.service';
})
export class ImportImageComponent implements OnInit {
baseUrl: string;
loading: boolean = true;
loading: boolean = false;
imageName: string = '';
constructor(
@ -27,26 +27,28 @@ export class ImportImageComponent implements OnInit {
}
ngOnInit(): void {
this.loading = true;
}
save() {
console.log(this.data?.repositoryUuid)
this.loading = true;
this.http.post<any>(`${this.baseUrl}/image-repositories/${this.data?.repositoryUuid}/import-image`, {
name: this.imageName
}).subscribe({
next: (response) => {
this.toastService.success('Peticion de importacion de imagen enviada correctamente');
this.dialogRef.close();
this.dialogRef.close(true);
this.loading = false;
this.router.navigate(['/commands-logs']);
},
error: error => {
this.loading = false;
this.toastService.error(error.error['hydra:description']);
}
});
}
close() {
this.dialogRef.close();
this.dialogRef.close(true);
}
}

View File

@ -98,3 +98,8 @@ table {
gap: 1em;
padding: 1.5em;
}
.loading-spinner {
margin: 0 auto;
align-self: center;
}

View File

@ -1,5 +1,3 @@
<app-loading [isLoading]="loading"></app-loading>
<mat-dialog-content>
<div class="header-container">
<div class="header-container-title">
@ -43,7 +41,9 @@
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="imagesTable"
<mat-spinner *ngIf="loading" class="loading-spinner"></mat-spinner>
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="imagesTable"
text="Esta tabla muestra las imágenes disponibles.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
@ -62,7 +62,7 @@
</ng-container>
<ng-container *ngIf="column.columnDef === 'isGlobal'">
<mat-chip>
{{ image.isGlobal ? 'Sí' : 'No' }}
{{ image.image?.isGlobal ? 'Sí' : 'No' }}
</mat-chip>
</ng-container>

View File

@ -89,7 +89,6 @@ export class ShowMonoliticImagesComponent implements OnInit {
}
ngOnInit(): void {
console.error()
if (this.data) {
this.loadData();
}
@ -105,6 +104,8 @@ export class ShowMonoliticImagesComponent implements OnInit {
},
error => {
console.error('Error fetching image repositories', error);
this.loading = false;
this.toastService.error(error.error['hydra:description'] || 'Error al cargar las imágenes del repositorio');
}
)
}
@ -192,6 +193,7 @@ export class ShowMonoliticImagesComponent implements OnInit {
}
toggleAction(image: any, action:string): void {
this.loading = true;
switch (action) {
case 'get-aux':
this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/create-aux-files`, {}).subscribe({
@ -353,6 +355,7 @@ export class ShowMonoliticImagesComponent implements OnInit {
});
break;
case 'rename':
this.loading = true;
this.dialog.open(RenameImageComponent, {
width: '600px',
data: {
@ -403,6 +406,7 @@ export class ShowMonoliticImagesComponent implements OnInit {
}
openImageInfoDialog() {
this.loading = true;
this.loadAlert().subscribe(
response => {
this.alertMessage = response.output;
@ -411,9 +415,12 @@ export class ShowMonoliticImagesComponent implements OnInit {
width: '800px',
data: this.alertMessage
});
this.loading = false;
},
error => {
console.error('Error al cargar la información del alert', error);
this.toastService.error(error.error['hydra:description']);
this.loading = false;
}
);
}

View File

@ -48,7 +48,7 @@
<div class="software-list">
<mat-list>
<mat-list-item *ngFor="let software of selectedSoftwares">
<mat-list-item *ngFor="let software of softwareCollection">
{{ software.name }}
<button mat-icon-button (click)="removeSoftware(software)">
<mat-icon>delete</mat-icon>
@ -64,4 +64,4 @@
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onCancel($event)">Cancelar</button>
<button class="submit-button" (click)="onSubmit()" cdkFocusInitial>Guardar</button>
</mat-dialog-actions>
</mat-dialog-actions>

View File

@ -74,7 +74,7 @@ export class CreateSoftwareProfileComponent implements OnInit {
}
loadSoftware() {
this.http.get<any>(`${this.baseUrl}/software?page=1&itemsPerPage=10`).subscribe(
this.http.get<any>(`${this.baseUrl}/software?softwareProfileId=${this.data.id}&page=1&itemsPerPage=10`).subscribe(
response => {
this.softwareCollection = response['hydra:member'];
},

View File

@ -39,12 +39,12 @@ export class SoftwareProfileComponent {
{
columnDef: 'description',
header: 'Descripción',
cell: (software: any) => `${software.description}`
cell: (software: any) => software.description
},
{
columnDef: 'operativeSystem',
header: 'Sistema Operativo',
cell: (software: any) => `${software.operativeSystem?.name}`
cell: (software: any) => software.operativeSystem?.name
},
{
columnDef: 'createdAt',
@ -61,7 +61,7 @@ export class SoftwareProfileComponent {
private toastService: ToastrService,
private joyrideService: JoyrideService,
private configService: ConfigService
) {
) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/software-profiles`;
}

View File

@ -39,17 +39,12 @@ export class SoftwareComponent {
{
columnDef: 'name',
header: 'Nombre',
cell: (software: any) => `${software.name}`
},
{
columnDef: 'description',
header: 'Descripción',
cell: (software: any) => `${software.description}`
cell: (software: any) => software.name
},
{
columnDef: 'type',
header: 'Tipo',
cell: (software: any) => `${software.type}`
cell: (software: any) => software.type
},
{
columnDef: 'createdAt',
@ -108,15 +103,15 @@ export class SoftwareComponent {
});
}
deleteSoftware(calendar: any): void {
deleteSoftware(software: any): void {
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '400px',
data: { name: calendar.name }
data: { name: software.name }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
const apiUrl = `${this.baseUrl}${calendar['@id']}`;
const apiUrl = `${this.baseUrl}${software['@id']}`;
this.http.delete(apiUrl).subscribe({
next: () => {

View File

@ -68,6 +68,23 @@
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
<mat-form-field appearance="fill" class="search-date">
<mat-label>Desde</mat-label>
<input matInput [matDatepicker]="fromPicker" [(ngModel)]="filters['startDate']"
(dateChange)="onDateFilterChange()" [max]="today">
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="fill" class="search-date">
<mat-label>Hasta</mat-label>
<input matInput [matDatepicker]="toPicker" [(ngModel)]="filters['endDate']" (dateChange)="onDateFilterChange()"
[max]="today">
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker>
</mat-form-field>
</div>
<app-loading [isLoading]="loading"></app-loading>
@ -180,4 +197,4 @@
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>
</div>

View File

@ -36,6 +36,7 @@ export class TaskLogsComponent implements OnInit {
mode: ProgressBarMode = 'buffer';
progress = 0;
bufferValue = 0;
today = new Date();
filteredCommands2 = Object.keys(COMMAND_TYPES).map(key => ({
name: key,
@ -215,10 +216,20 @@ export class TaskLogsComponent implements OnInit {
loadTraces(): void {
this.loading = true;
const url = `${this.baseUrl}/traces?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`;
const params = { ...this.filters };
const params: any = { ...this.filters };
if (params['status'] === undefined) {
delete params['status'];
}
if (params['startDate']) {
params['executedAt[after]'] = this.datePipe.transform(params['startDate'], 'yyyy-MM-dd');
delete params['startDate'];
}
if (params['endDate']) {
params['executedAt[before]'] = this.datePipe.transform(params['endDate'], 'yyyy-MM-dd');
delete params['endDate'];
}
this.http.get<any>(url, { params }).subscribe(
(data) => {
this.traces = data['hydra:member'];
@ -288,6 +299,21 @@ export class TaskLogsComponent implements OnInit {
}));
}
onDateFilterChange(): void {
const start = this.filters['startDate'];
const end = this.filters['endDate'];
if (!start || !end) {
return;
}
if (start && end && start > end) {
this.toastService.warning('La fecha de inicio no puede ser mayor que la fecha de fin');
return;
}
this.loadTraces();
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;