refs #1138 Fix stash pop from add translate and es-en files

oggui/translations
Alvaro Puente Mella 2024-11-15 13:57:20 +01:00
commit bb41d9c956
91 changed files with 1997 additions and 791 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
ogWebconsole/.env
ogWebconsole/test-results/ogGui-junit-report.xml

View File

@ -22,6 +22,7 @@
"@ngx-translate/http-loader": "^16.0.0",
"@swimlane/ngx-charts": "^20.5.0",
"jwt-decode": "^4.0.0",
"ngx-joyride": "^2.5.0",
"ngx-toastr": "^19.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
@ -11266,6 +11267,18 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
"node_modules/ngx-joyride": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/ngx-joyride/-/ngx-joyride-2.5.0.tgz",
"integrity": "sha512-C/J8C4uWZjKl9aMmRBt9egVjuIpwWFplJgBZDl1EfqNVTJkdEC51nt9DpAOuDwOgkbArhJ9sZIk3bZT4vkud/w==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/common": ">=8.2.14",
"@angular/core": ">=8.2.14"
}
},
"node_modules/ngx-toastr": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.0.0.tgz",

View File

@ -24,6 +24,7 @@
"@ngx-translate/http-loader": "^16.0.0",
"@swimlane/ngx-charts": "^20.5.0",
"jwt-decode": "^4.0.0",
"ngx-joyride": "^2.5.0",
"ngx-toastr": "^19.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",

View File

@ -27,6 +27,10 @@ import { RestoreImageComponent } from './components/groups/components/client-mai
import {SoftwareComponent} from "./components/software/software.component";
import {SoftwareProfileComponent} from "./components/software-profile/software-profile.component";
import {OperativeSystemComponent} from "./components/operative-system/operative-system.component";
import {
PartitionAssistantComponent
} from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component";
import {RepositoriesComponent} from "./components/repositories/repositories.component";
const routes: Routes = [
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
{
@ -51,7 +55,9 @@ const routes: Routes = [
{ path: 'commands-logs', component: TaskLogsComponent },
{ path: 'calendars', component: CalendarComponent },
{ path: 'client/:id', component: ClientMainViewComponent },
{ path: 'client/:id/partition-assistant', component: PartitionAssistantComponent },
{ path: 'images', component: ImagesComponent },
{ path: 'repositories', component: RepositoriesComponent },
{ path: 'restore-image', component: RestoreImageComponent},
{ path: 'software', component: SoftwareComponent },
{ path: 'software-profiles', component: SoftwareProfileComponent },

View File

@ -16,6 +16,7 @@
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<<<<<<< Updated upstream
<mat-form-field appearance="fill" class="search-string">

View File

@ -8,6 +8,7 @@ import { ToastrService } from "ngx-toastr";
import { PageEvent } from "@angular/material/paginator";
import { CreateCalendarComponent } from "./create-calendar/create-calendar.component";
import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-calendar',
@ -51,14 +52,14 @@ export class CalendarComponent implements OnInit {
}
];
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/remote-calendars`;
constructor(
public dialog: MatDialog,
private http: HttpClient,
private dataService: DataService,
private toastService: ToastrService
private toastService: ToastrService,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
@ -71,7 +72,6 @@ export class CalendarComponent implements OnInit {
});
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
this.search();
});
}
@ -84,24 +84,21 @@ export class CalendarComponent implements OnInit {
this.loading = false;
},
error => {
console.error('Error fetching og lives', error);
console.error('Error fetching calendars', error);
this.loading = false;
}
);
}
sync(calendar: any): void {
console.log('Syncing calendars');
this.syncUds = true;
this.http.post(`${this.apiUrl}/${calendar.uuid}/sync-uds`, {}).subscribe({
next: () => {
console.log('Calendars synced successfully');
this.toastService.success('Calendarios sincronizados correctamente');
this.search();
this.syncUds = false;
},
error: (error) => {
console.error('Error al sincronizar los calendarios:', error);
this.toastService.error(error.error['hydra:description']);
this.syncUds = false;
}
@ -133,16 +130,13 @@ export class CalendarComponent implements OnInit {
this.http.delete(apiUrl).subscribe({
next: () => {
console.log('Calendar deleted successfully');
this.search();
this.toastService.success('Calendar deleted successfully');
},
error: (error) => {
error: () => {
this.toastService.error('Error deleting calendar');
}
});
} else {
console.log('calendar deletion cancelled');
}
});
}
@ -155,8 +149,7 @@ export class CalendarComponent implements OnInit {
this.length = response['hydra:totalItems'];
this.loading = false;
},
error: (error) => {
console.error('Error al cargar las imágenes:', error);
error: () => {
this.loading = false;
}
});
@ -167,4 +160,12 @@ export class CalendarComponent implements OnInit {
this.itemsPerPage = event.pageSize;
this.applyFilter();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: ['titleStep', 'addButtonStep', 'searchStep', 'tableStep', 'actionsStep'],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -15,6 +15,7 @@
>>>>>>> Stashed changes
</div>
</div>
<mat-divider class="divider"></mat-divider>
<<<<<<< Updated upstream
<div class="search-container">
@ -66,10 +67,11 @@
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<<<<<<< Updated upstream
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let client" style="text-align: center;">
<td mat-cell *matCellDef="let client" style="text-align: center;" joyrideStep="actionsStep" text="Usa estas opciones para ver, editar o eliminar un grupo de comandos.">
<button mat-icon-button color="info" (click)="viewGroupDetails(client)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" (click)="editCommandGroup(client)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
=======

View File

@ -7,6 +7,7 @@ import { DetailCommandGroupComponent } from './detail-command-group/detail-comma
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
import { MatTableDataSource } from "@angular/material/table";
import { DatePipe } from "@angular/common";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-commands-groups',
@ -48,7 +49,8 @@ export class CommandsGroupsComponent implements OnInit {
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/command-groups`;
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService) {}
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
private joyrideService: JoyrideService) {}
ngOnInit(): void {
this.search();
@ -114,4 +116,21 @@ export class CommandsGroupsComponent implements OnInit {
this.length = event.length;
this.search();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'addCommandGroupStep',
'searchStep',
'tableStep',
'viewCommandsStep',
'actionsStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -1,8 +1,8 @@
.create-command-group-container {
padding: 20px;
max-width: 800px; /* Ancho máximo del contenedor */
margin: auto; /* Centra el contenedor en la pantalla */
background-color: #fff; /* Fondo blanco para el contenedor */
max-width: 800px;
margin: auto;
background-color: #fff;
}
.form-container {
@ -24,14 +24,14 @@
width: 48%;
display: flex;
flex-direction: column;
max-height: 200px; /* Limita la altura máxima para evitar desbordamiento */
max-height: 200px;
}
.table-wrapper {
flex: 1;
overflow-y: auto; /* Scroll para la tabla si hay demasiados comandos */
border: 1px solid #ccc; /* Borde para la tabla */
border-radius: 4px; /* Bordes redondeados */
overflow-y: auto;
border: 1px solid #ccc;
border-radius: 4px;
}
.selected-commands-list {
@ -40,7 +40,7 @@
padding: 10px;
background-color: #f9f9f9;
flex: 1;
overflow-y: auto; /* Scroll para los comandos seleccionados */
overflow-y: auto;
}
.commands-container {
@ -60,7 +60,7 @@
.remove-icon {
cursor: pointer;
color: #f44336; /* Rojo para eliminar */
color: #f44336;
}
.chevron-icon {
@ -74,29 +74,27 @@
justify-content: space-between;
}
.available-commands, .selected-commands {
width: 48%;
display: flex;
flex-direction: column;
max-height: 500px; /* Limita la altura máxima para evitar desbordamiento */
max-height: 500px;
}
.table-wrapper {
flex: 1;
overflow-y: auto; /* Scroll para la tabla si hay demasiados comandos */
border: 1px solid #ccc; /* Borde para la tabla */
border-radius: 4px; /* Bordes redondeados */
max-height: 400px; /* Establece la altura máxima */
overflow-y: auto;
border: 1px solid #ccc;
border-radius: 4px;
max-height: 400px;
}
/* Para asegurar que el componente sea responsivo en pantallas pequeñas */
@media (max-width: 600px) {
.command-selection {
flex-direction: column; /* Cambia a columna en pantallas pequeñas */
flex-direction: column;
}
.available-commands, .selected-commands {
width: 100%; /* Ocupa el ancho completo */
margin-bottom: 20px; /* Espacio entre elementos */
width: 100%;
margin-bottom: 20px;
}
}

View File

@ -46,15 +46,13 @@
<div class="selected-commands-list" cdkDropList (cdkDropListDropped)="drop($event)">
>>>>>>> Stashed changes
<div class="commands-container">
<ng-container *ngFor="let command of selectedCommands; let last = last">
<div *ngFor="let command of selectedCommands" cdkDrag>
<div class="command-item">
<mat-icon class="drag-handle" cdkDragHandle>drag_indicator</mat-icon>
{{ command.name }}
<mat-icon class="remove-icon" (click)="removeCommand(command)">close</mat-icon>
</div>
<ng-container *ngIf="!last">
<mat-icon class="chevron-icon">chevron_right</mat-icon>
</ng-container>
</ng-container>
</div>
</div>
</div>
</div>

View File

@ -2,6 +2,7 @@ import { Component, OnInit, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ToastrService } from 'ngx-toastr';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
@Component({
selector: 'app-create-command-group',
@ -55,7 +56,9 @@ export class CreateCommandGroupComponent implements OnInit {
}
addCommand(command: any): void {
this.selectedCommands.push(command);
if (!this.selectedCommands.includes(command)) {
this.selectedCommands.push(command);
}
}
removeCommand(command: any): void {
@ -65,6 +68,10 @@ export class CreateCommandGroupComponent implements OnInit {
}
}
drop(event: CdkDragDrop<any[]>): void {
moveItemInArray(this.selectedCommands, event.previousIndex, event.currentIndex);
}
onSubmit(): void {
const payload = {
name: this.groupName,
@ -81,6 +88,7 @@ export class CreateCommandGroupComponent implements OnInit {
},
error: (error) => {
console.error('Error actualizando el grupo de comandos', error);
this.toastService.error('Error al actualizar el grupo de comandos');
}
});
} else {
@ -91,6 +99,7 @@ export class CreateCommandGroupComponent implements OnInit {
},
error: (error) => {
console.error('Error creando el grupo de comandos', error);
this.toastService.error('Error al crear el grupo de comandos');
}
});
}

View File

@ -31,10 +31,6 @@
</mat-form-field>
</div>
<div *ngIf="loading" class="loading-container">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="!loading">
<<<<<<< Updated upstream
<table mat-table [dataSource]="tasks" class="mat-elevation-z8">
@ -69,7 +65,7 @@
<ng-container matColumnDef="actions">
<<<<<<< Updated upstream
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let task" style="text-align: center;">
<td mat-cell *matCellDef="let task" style="text-align: center;" joyrideStep="actionsStep" text="Usa estas opciones para ver, editar o eliminar una tarea.">
<button mat-icon-button color="info" (click)="viewTaskDetails(task)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" (click)="editTask(task)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
=======

View File

@ -5,6 +5,7 @@ import { ToastrService } from 'ngx-toastr';
import { CreateTaskComponent } from './create-task/create-task.component';
import { DetailTaskComponent } from './detail-task/detail-task.component';
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-commands-task',
@ -23,7 +24,8 @@ export class CommandsTaskComponent implements OnInit {
loading: boolean = false;
private apiUrl = `${this.baseUrl}/command-tasks`;
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService) {}
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
private joyrideService: JoyrideService) {}
ngOnInit(): void {
this.loadTasks();
@ -93,4 +95,20 @@ export class CommandsTaskComponent implements OnInit {
this.itemsPerPage = event.pageSize;
this.loadTasks();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'addTaskStep',
'searchStep',
'tableStep',
'actionsStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -12,10 +12,16 @@
.button-container {
display: flex;
justify-content: space-between;
justify-content: flex-end;
margin-top: 20px;
}
mat-form-field {
margin-bottom: 16px; /* Espaciado entre campos */
margin-bottom: 16px;
}
.section-title {
margin-top: 24px;
margin-bottom: 8px;
font-weight: 500;
}

View File

@ -2,13 +2,13 @@
<form [formGroup]="taskForm" class="task-form">
<mat-dialog-content>
<mat-horizontal-stepper linear>
<!-- Paso 1: Información y Selecciona Comandos -->
<mat-step label="Información y Selecciona Comandos">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Información</mat-label>
<textarea matInput formControlName="notes" placeholder="Ingresa tus notas aquí"></textarea>
</mat-form-field>
<h3 class="section-title">Información</h3>
<mat-divider></mat-divider>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Información</mat-label>
<textarea matInput formControlName="notes" placeholder="Ingresa tus notas aquí"></textarea>
</mat-form-field>
<<<<<<< Updated upstream
<mat-form-field appearance="fill" class="full-width">
@ -21,61 +21,42 @@
<mat-error *ngIf="taskForm.get('commandGroup')?.invalid">Este campo es obligatorio</mat-error>
</mat-form-field>
<div class="button-container">
<button mat-raised-button color="primary" matStepperNext [disabled]="taskForm.get('commandGroup')?.invalid">Continuar</button>
</div>
</mat-step>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Selecciona Comandos Individuales (Opcional)</mat-label>
<mat-select formControlName="extraCommands" multiple>
<mat-option *ngFor="let command of availableIndividualCommands" [value]="command.uuid">
{{ command.name }}
</mat-option>
</mat-select>
</mat-form-field>
<!-- Paso 2: Selecciona Comandos Individuales -->
<mat-step label="Selecciona Comandos Individuales">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Selecciona Comandos Individuales (Opcional)</mat-label>
<mat-select formControlName="extraCommands" multiple>
<mat-option *ngFor="let command of availableIndividualCommands" [value]="command.uuid">
{{ command.name }}
</mat-option>
</mat-select>
</mat-form-field>
<h3 class="section-title">Fecha y hora de ejecución</h3>
<mat-divider></mat-divider>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Fecha de Ejecución</mat-label>
<input matInput [matDatepicker]="picker" formControlName="date" placeholder="Selecciona una fecha">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
<mat-error *ngIf="taskForm.get('date')?.invalid">Este campo es obligatorio</mat-error>
</mat-form-field>
<div class="button-container">
<button mat-button matStepperPrevious>Atrás</button>
<button mat-raised-button color="primary" matStepperNext>Continuar</button>
</div>
</mat-step>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Hora de Ejecución</mat-label>
<input matInput type="time" formControlName="time" placeholder="Selecciona una hora">
<mat-error *ngIf="taskForm.get('time')?.invalid">Este campo es obligatorio</mat-error>
</mat-form-field>
<!-- Paso 3: Fecha de Ejecución y Hora -->
<mat-step label="Fecha de Ejecución y Hora">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Fecha de Ejecución</mat-label>
<input matInput [matDatepicker]="picker" formControlName="date" placeholder="Selecciona una fecha">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
<mat-error *ngIf="taskForm.get('date')?.invalid">Este campo es obligatorio</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Hora de Ejecución</mat-label>
<input matInput type="time" formControlName="time" placeholder="Selecciona una hora">
<mat-error *ngIf="taskForm.get('time')?.invalid">Este campo es obligatorio</mat-error>
</mat-form-field>
<div class="button-container">
<button mat-button matStepperPrevious>Atrás</button>
<button mat-raised-button color="primary" matStepperNext>Continuar</button>
</div>
</mat-step>
<!-- Paso 4: Selecciona Unidad Organizacional, Aula y Clientes -->
<mat-step label="Selecciona Unidad Organizacional, Aula y Clientes">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Selecciona Unidad Organizacional</mat-label>
<mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
{{ unit.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="taskForm.get('organizationalUnit')?.invalid">Este campo es obligatorio</mat-error>
</mat-form-field>
<h3 class="section-title">Selecciona destino</h3>
<mat-divider></mat-divider>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Selecciona Unidad Organizacional</mat-label>
<mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
{{ 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>
@ -168,24 +149,21 @@
</div>
>>>>>>> Stashed changes
<mat-form-field appearance="fill" class="full-width">
<mat-label>Selecciona Clientes</mat-label>
<mat-select formControlName="selectedClients" multiple>
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
Seleccionar todos
</mat-option>
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
{{ client.name }} ({{ client.ip }})
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Selecciona Clientes</mat-label>
<mat-select formControlName="selectedClients" multiple>
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
Seleccionar todos
</mat-option>
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
{{ client.name }} ({{ client.ip }})
</mat-option>
</mat-select>
</mat-form-field>
<div class="button-container">
<button mat-button matStepperPrevious>Atrás</button>
<button mat-raised-button color="primary" (click)="saveTask()">Guardar</button>
</div>
</mat-step>
<div class="button-container">
<button mat-raised-button color="primary" (click)="saveTask()">Guardar</button>
</div>
</mat-horizontal-stepper>
</mat-dialog-content>
</form>

View File

@ -49,8 +49,6 @@ export class CreateTaskComponent implements OnInit {
this.editing = true;
this.loadTaskData(this.data.task);
}
console.log(this.data);
}
loadCommandGroups(): void {
@ -101,6 +99,76 @@ export class CreateTaskComponent implements OnInit {
}
}
private collectClassrooms(unit: any): any[] {
let classrooms = [];
if (unit.type === 'classroom') {
classrooms.push(unit);
}
if (unit.children && unit.children.length > 0) {
for (let child of unit.children) {
classrooms = classrooms.concat(this.collectClassrooms(child));
}
}
return classrooms;
}
onOrganizationalUnitChange(): void {
const selectedUnitId = this.taskForm.get('organizationalUnit')?.value;
const selectedUnit = this.availableOrganizationalUnits.find(unit => unit['@id'] === selectedUnitId);
if (selectedUnit) {
this.selectedUnitChildren = this.collectClassrooms(selectedUnit);
} else {
this.selectedUnitChildren = [];
}
this.taskForm.patchValue({ selectedChild: '', selectedClients: [] });
this.selectedClients = [];
this.selectedClientIds.clear();
}
onChildChange(): void {
const selectedChildId = this.taskForm.get('selectedChild')?.value;
if (!selectedChildId) {
this.selectedClients = [];
return;
}
const url = `${this.baseUrl}${selectedChildId}`.replace(/([^:]\/)\/+/g, '$1');
this.http.get<any>(url).subscribe(
(data) => {
if (Array.isArray(data.clients) && data.clients.length > 0) {
this.selectedClients = data.clients;
} else {
this.selectedClients = [];
this.toastr.warning('El aula seleccionada no tiene clientes.');
}
this.taskForm.patchValue({ selectedClients: [] });
this.selectedClientIds.clear();
},
(error) => {
this.toastr.error('Error al cargar los detalles del aula seleccionada');
}
);
}
toggleSelectAll() {
const allSelected = this.areAllSelected();
if (allSelected) {
this.selectedClientIds.clear();
} else {
this.selectedClients.forEach(client => this.selectedClientIds.add(client.uuid));
}
this.taskForm.get('selectedClients')!.setValue(Array.from(this.selectedClientIds));
}
areAllSelected(): boolean {
return this.selectedClients.length > 0 && this.selectedClients.every(client => this.selectedClientIds.has(client.uuid));
}
onCommandGroupChange(): void {
const selectedGroupId = this.taskForm.get('commandGroup')?.value;
this.http.get<any>(`${this.baseUrl}/command-groups/${selectedGroupId}`).subscribe(
@ -113,40 +181,6 @@ export class CreateTaskComponent implements OnInit {
);
}
onOrganizationalUnitChange(): void {
const selectedUnitId = this.taskForm.get('organizationalUnit')?.value;
const selectedUnit = this.availableOrganizationalUnits.find(unit => unit['@id'] === selectedUnitId);
this.selectedUnitChildren = selectedUnit ? selectedUnit.children : [];
}
onChildChange(): void {
const selectedChildId = this.taskForm.get('selectedChild')?.value;
this.http.get<any>(`${this.baseUrl}${selectedChildId}`).subscribe(
(data) => {
this.selectedClients = data.clients;
this.taskForm.patchValue({ selectedClients: [] });
this.selectedClientIds.clear();
},
(error) => {
this.toastr.error('Error al cargar los detalles del aula seleccionada');
}
);
}
toggleSelectAll() {
const allSelected = this.areAllSelected();
if (allSelected) {
this.selectedClientIds.clear();
} else {
this.selectedClients.forEach(client => this.selectedClientIds.add(client.uuid));
}
this.taskForm.get('selectedClients')!.setValue(Array.from(this.selectedClientIds));
}
areAllSelected(): boolean {
return this.selectedClients.length > 0 && this.selectedClients.every(client => this.selectedClientIds.has(client.uuid));
}
saveTask(): void {
if (this.taskForm.invalid) {
this.toastr.error('Por favor, rellene todos los campos obligatorios');
@ -156,14 +190,14 @@ export class CreateTaskComponent implements OnInit {
const formData = this.taskForm.value;
const dateTime = this.combineDateAndTime(formData.date, formData.time);
const selectedCommands = formData.extraCommands && formData.extraCommands.length > 0
? formData.extraCommands.map((id: any) => `/commands/${id}`)
: null;
? formData.extraCommands.map((id: any) => `/commands/${id}`)
: null;
const payload: any = {
commandGroups: formData.commandGroup ? [`/command-groups/${formData.commandGroup}`] : null,
dateTime: dateTime,
notes: formData.notes || '',
clients: Array.from(this.selectedClientIds).map((uuid: string) => `/clients/${uuid}`),
clients: Array.from(this.selectedClientIds).map((uuid: string) => `/clients/${uuid}`),
};
if (selectedCommands) {

View File

@ -15,6 +15,7 @@
>>>>>>> Stashed changes
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
@ -47,12 +48,10 @@
</mat-form-field>
</div>
<!-- Indicador de carga -->
<div *ngIf="loading" class="loading-container">
<mat-spinner></mat-spinner>
</div>
<!-- Tabla de trazas -->
<div *ngIf="!loading">
<<<<<<< Updated upstream
<table mat-table [dataSource]="traces" class="mat-elevation-z8">

View File

@ -4,6 +4,7 @@ import { Observable } from 'rxjs';
import { FormControl } from '@angular/forms';
import { map, startWith } from 'rxjs/operators';
import { DatePipe } from '@angular/common';
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-task-logs',
@ -63,7 +64,8 @@ export class TaskLogsComponent implements OnInit {
filteredCommands!: Observable<any[]>;
commandControl = new FormControl();
constructor(private http: HttpClient) {}
constructor(private http: HttpClient,
private joyrideService: JoyrideService) {}
ngOnInit(): void {
this.loadTraces();
@ -183,4 +185,20 @@ export class TaskLogsComponent implements OnInit {
this.length = event.length;
this.loadTraces();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'resetFiltersStep',
'clientSelectStep',
'commandSelectStep',
'tableStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -13,6 +13,7 @@
>>>>>>> Stashed changes
</div>
</div>
<mat-divider class="divider"></mat-divider>
<<<<<<< Updated upstream

View File

@ -7,6 +7,8 @@ import { CreateCommandComponent } from './create-command/create-command.componen
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
import { MatTableDataSource } from '@angular/material/table';
import { DatePipe } from '@angular/common';
import { ExecuteCommandComponent } from './execute-command/execute-command.component';
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-commands',
@ -48,7 +50,8 @@ export class CommandsComponent implements OnInit {
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/commands`;
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService) {}
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
private joyrideService: JoyrideService) {}
ngOnInit(): void {
this.search();
@ -74,7 +77,7 @@ export class CommandsComponent implements OnInit {
this.dialog.open(CommandDetailComponent, {
width: '800px',
data: command,
}).afterClosed().subscribe(() => this.search());
});
}
openCreateCommandModal(): void {
@ -111,10 +114,38 @@ export class CommandsComponent implements OnInit {
});
}
executeCommand(event: MouseEvent, command: any): void {
this.dialog.open(ExecuteCommandComponent, {
width: '50%',
data: { commandData: command }
}).afterClosed().subscribe((result) => {
if (result) {
console.log('Comando ejecutado con éxito');
} else {
console.log('Ejecución de comando cancelada');
}
});
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.length = event.length;
this.search();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'addCommandStep',
'searchStep',
'tableStep',
'actionsStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -28,11 +28,4 @@
</mat-form-field>
</form>
</div>
<div class="button-row">
<button mat-button color="primary" class="primary-button" [disabled]="false" (click)="execute()" >
{{ showClientSelect ? 'Ejecutar' : 'Configurar ejecución' }}
</button>
<button mat-button (click)="cancel()">Cancelar</button>
</div>
</div>

View File

@ -38,54 +38,12 @@ export class CommandDetailComponent implements OnInit {
this.http.get<any>(`${this.baseUrl}/clients?page=1&itemsPerPage=30`).subscribe(response => {
this.clients = response['hydra:member'];
});
}
execute(): void {
if (!this.showClientSelect) {
this.showClientSelect = true;
} else {
if (this.form.get('selectedClients')?.value.length > 0) {
const payload = {
clients: this.form.value.selectedClients.map((uuid: any) => `/clients/${uuid}`)
};
const apiUrl = `${this.baseUrl}/commands/${this.data.uuid}/execute`;
this.http.post(apiUrl, payload).subscribe({
next: () => {
this.dialogRef.close();
this.toastService.success('Command executed successfully');
},
error: (error) => {
console.error('Error executing command:', error);
}
});
this.dialogRef.close();
} else {
this.form.get('selectedClients')?.markAsTouched();
}
}
}
onClientSelectionChange(event: any): void {
this.canExecute = this.form.get('selectedClients')?.value.length > 0;
}
onScheduleChange(event: any): void {
this.showDatePicker = event.checked;
if (event.checked) {
this.form.get('scheduleDate')?.setValidators(Validators.required);
this.form.get('scheduleTime')?.setValidators(Validators.required);
} else {
this.form.get('scheduleDate')?.clearValidators();
this.form.get('scheduleTime')?.clearValidators();
}
this.form.get('scheduleDate')?.updateValueAndValidity();
this.form.get('scheduleTime')?.updateValueAndValidity();
}
edit(): void {
const dialogRef = this.dialog.open(CreateCommandComponent, {
width: '600px',

View File

@ -0,0 +1,32 @@
.form-container {
padding: 20px 24px;
width: 100%; /* Asegura que el formulario ocupe el ancho completo */
}
.command-form {
display: flex;
flex-direction: column;
width: 100%;
}
.full-width {
width: 100%;
margin-bottom: 16px;
}
.checkbox-group {
display: flex;
flex-direction: column;
margin: 15px 0;
align-items: flex-start;
}
.mat-dialog-content {
padding: 24px;
width: 100%; /* Ocupar el ancho completo del modal */
}
.mat-dialog-actions {
padding: 16px 24px;
}

View File

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

View File

@ -0,0 +1,100 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'app-execute-command',
templateUrl: './execute-command.component.html',
styleUrls: ['./execute-command.component.css']
})
export class ExecuteCommandComponent implements OnInit {
form: FormGroup;
units: any[] = [];
childUnits: any[] = [];
clients: any[] = [];
selectedClients: any[] = [];
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
constructor(
private dialogRef: MatDialogRef<ExecuteCommandComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private http: HttpClient,
private fb: FormBuilder
) {
this.form = this.fb.group({
unit: [null],
childUnit: [null],
clientSelection: [[]]
});
}
ngOnInit(): void {
this.loadUnits();
this.form.get('unit')?.valueChanges.subscribe(value => this.onUnitChange(value));
this.form.get('childUnit')?.valueChanges.subscribe(value => this.onChildUnitChange(value));
}
loadUnits(): void {
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`).subscribe(
response => {
this.units = response['hydra:member'].filter((unit: { type: string; }) => unit.type === 'organizational-unit');
},
error => console.error('Error fetching organizational units:', error)
);
}
onUnitChange(unitId: string): void {
const unit = this.units.find(unit => unit.uuid === unitId);
this.childUnits = unit ? this.getAllChildren(unit) : [];
this.clients = [];
this.form.patchValue({ childUnit: null, clientSelection: [] });
}
getAllChildren(unit: any): any[] {
let allChildren = [];
if (unit.children && unit.children.length > 0) {
for (const child of unit.children) {
allChildren.push(child);
allChildren = allChildren.concat(this.getAllChildren(child));
}
}
return allChildren;
}
onChildUnitChange(childUnitId: string): void {
const childUnit = this.childUnits.find(unit => unit.uuid === childUnitId);
this.clients = childUnit && childUnit.clients ? childUnit.clients : [];
this.form.patchValue({ clientSelection: [] });
}
executeCommand(): void {
const payload = {
clients: ['/clients/'+this.form.get('clientSelection')?.value]
};
this.http.post(`${this.baseUrl}/commands/${this.data.commandData.uuid}/execute`, payload)
.subscribe({
next: () => {
console.log('Comando ejecutado con éxito');
this.dialogRef.close(true);
},
error: (error) => {
console.error('Error al ejecutar el comando:', error);
}
});
}
closeModal(): void {
this.dialogRef.close(false);
}
toggleClientSelection(clientId: string): void {
const selectedClients = this.form.get('clientSelection')?.value;
if (selectedClients.includes(clientId)) {
this.form.get('clientSelection')?.setValue(selectedClients.filter((id: string) => id !== clientId));
} else {
this.form.get('clientSelection')?.setValue([...selectedClients, clientId]);
}
}
}

View File

@ -21,7 +21,11 @@
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 100px;
padding: 10px;
}
.unidad-card, .elements-card {
@ -124,11 +128,6 @@ mat-spinner {
align-self: center;
}
.container {
display: flex;
justify-content: flex-end;
}
.classroomBtn-container {
display: flex;
justify-content: flex-end;
@ -174,62 +173,61 @@ mat-spinner {
width: 100%;
}
.results-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-bottom: 16px;
}
.result-card {
width: 100%;
max-width: 250px;
height: 250px;
height: auto;
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
padding: 15px;
margin: 10px 0;
margin: 10px 10px;
}
.result-card.small-card {
width: 100%;
max-width: 180px;
height: auto;
min-height: 130px;
padding: 10px;
margin: 5px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.result-title {
font-size: 1rem;
font-weight: 500;
color: #333;
margin-bottom: 5px;
}
.result-content {
padding-top: 5px;
color: #555;
font-size: 0.85rem;
}
.result-type, .result-ip, .result-mac, .result-status, .result-internal-units, .result-clients {
font-size: 0.8rem;
margin: 2px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.result-checkbox {
align-self: flex-start;
margin: 0 0 5px 0;
}
.result-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.result-checkbox {
float: right;
margin: 0;
}
.result-title {
font-size: 1.2rem;
font-weight: 600;
color: #333;
}
.result-content {
padding-top: 10px;
color: #555;
}
.result-type {
font-size: 1rem;
font-weight: 500;
margin: 0;
}
.result-ip, .result-mac, .result-status {
font-size: 0.9rem;
margin: 5px 0;
}
.result-internal-units,
.result-clients {
font-size: 0.9rem;
color: #007bff;
margin: 5px 0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
}
.paginator-container {
@ -251,13 +249,13 @@ mat-card {
}
.red-card {
background-color: #f35f53;
background-color: #f35f53;
color: white;
}
.green-card {
background-color: #4caf50;
color: white;
background-color: #4caf50;
color: white;
}
.view-mode-buttons button.active {
@ -265,16 +263,12 @@ mat-card {
color: #3f51b5;
}
.result-card {
width: 100%;
}
.result-card-list {
display: flex;
flex-direction: row;
align-items: center;
padding: 5px;
margin-bottom: 4px;
margin-bottom: 2px;
border: 1px solid #ddd;
}
@ -283,7 +277,7 @@ mat-card {
}
.result-card-list .result-title {
font-size: 14px;
font-size: 14px;
font-weight: bold;
margin-right: 8px;
}
@ -292,11 +286,11 @@ mat-card {
display: flex;
flex-direction: row;
gap: 8px;
font-size: 12px;
font-size: 12px;
}
.result-card-list p {
margin: 0;
margin: 0;
}
.result-list {

View File

@ -10,7 +10,7 @@
</div>
>>>>>>> Stashed changes
<div class="container">
<div class="header">
<div class="header" joyrideStep="filterSelectionStep" text="Selecciona entre los filtros guardados para ajustar los resultados de la búsqueda.">
<mat-form-field>
<mat-label>{{ 'selectFilterLabel' | translate }}</mat-label>
<mat-select (selectionChange)="loadSelectedFilter($event.value)">
@ -23,7 +23,7 @@
<mat-divider class="divider"></mat-divider>
<div class="view-mode-buttons">
<div class="view-mode-buttons" joyrideStep="viewModeStep" text="Elige cómo quieres ver los resultados: en cuadrícula o en lista.">
<button mat-button (click)="changeViewMode('grid')" [class.active]="viewMode === 'grid'">
<mat-icon>grid_view</mat-icon> {{ 'gridViewButton' | translate }}
</button>
@ -33,7 +33,7 @@
</div>
<div class="main-content">
<div class="filters">
<div class="filters" joyrideStep="filtersStep" text="Aplica filtros específicos para encontrar los resultados exactos que necesitas.">
<mat-form-field>
<mat-label>{{ 'selectOptionLabel' | translate }}</mat-label>
<mat-select [(value)]="selectedFilter1" (selectionChange)="applyFilter()">
@ -70,12 +70,12 @@
>>>>>>> Stashed changes
</div>
<div class="results">
<div class="results" joyrideStep="resultsStep" text="Aquí verás los resultados de tu búsqueda filtrada.">
<ng-container *ngIf="filteredResults && filteredResults.length > 0; else noResults">
<ng-container *ngIf="viewMode === 'grid'">
<mat-grid-list cols="4" rowHeight="1:1">
<mat-grid-list cols="8" rowHeight="1:1">
<mat-grid-tile *ngFor="let result of filteredResults">
<mat-card class="result-card">
<mat-card class="result-card small-card">
<mat-checkbox [checked]="isSelected(result.name)" (change)="onCheckboxChange($event, result.name, result['@id'])" class="result-checkbox"></mat-checkbox>
<mat-card-title class="result-title">{{ result.name }}</mat-card-title>
<mat-card-content class="result-content">
@ -106,7 +106,7 @@
</div>
</ng-container>
<div class="paginator-container">
<div class="paginator-container" joyrideStep="paginationStep" text="Usa el paginador para navegar entre los resultados.">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions" (page)="onPageChange($event)">
</mat-paginator>
</div>

View File

@ -23,6 +23,7 @@ import { Router } from '@angular/router';
import {
CreatePxeBootFileComponent
} from "../../../ogboot/pxe-boot-files/create-pxeBootFile/create-pxe-boot-file/create-pxe-boot-file.component";
import { JoyrideService } from 'ngx-joyride';
@Component({
@ -69,7 +70,8 @@ export class AdvancedSearchComponent {
private toastService: ToastrService,
private _bottomSheet: MatBottomSheet,
private http: HttpClient,
private router: Router
private router: Router,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
@ -436,4 +438,23 @@ export class AdvancedSearchComponent {
}
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'title2Step',
'filterSelectionStep',
'viewModeStep',
'filtersStep',
'selectAllStep',
'saveFiltersStep',
'sendActionStep',
'addPxeStep',
'resultsStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -205,3 +205,37 @@
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.circular-chart {
display: block;
margin: 0 auto;
max-width: 100%;
max-height: 150px;
}
.circle-bg {
fill: none;
stroke: #f0f0f0;
stroke-width: 3.8;
}
.circle {
fill: none;
stroke-width: 3.8;
stroke-linecap: round;
animation: progress 1s ease-out forwards;
}
/* Define colores distintos para cada partición */
.partition-0 { stroke: #00bfa5; } /* Ejemplo: verde */
.partition-1 { stroke: #ff6f61; } /* Ejemplo: rojo */
.partition-2 { stroke: #ffb400; } /* Ejemplo: amarillo */
.partition-3 { stroke: #3498db; } /* Ejemplo: azul */
/* Texto en el centro del gráfico */
.percentage {
fill: #333;
font-size: 0.7rem;
text-anchor: middle;
}

View File

@ -67,17 +67,20 @@
<div *ngFor="let disk of diskUsageData" class="disk-usage">
<h3>{{ 'diskTitle' | translate }} {{ disk.diskNumber }}</h3>
<div class="chart">
<svg viewBox="0 0 36 36" class="circular-chart green">
<path class="circle-bg"
d="M18 2.0845
a 15.9155 15.9155 0 0 1 0 31.831
a 15.9155 15.9155 0 0 1 0 -31.831" />
<path class="circle"
[attr.stroke-dasharray]="disk.percentage + ', 100'"
d="M18 2.0845
a 15.9155 15.9155 0 0 1 0 31.831
a 15.9155 15.9155 0 0 1 0 -31.831" />
<text x="18" y="20.35" class="percentage">{{ disk.percentage }}%</text>
<svg viewBox="0 0 36 36" class="circular-chart">
<path
class="circle-bg"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
<ng-container *ngFor="let partition of disk.partitions; let i = index">
<path
class="circle partition-{{ i }}"
[attr.stroke-dasharray]="(partition.size / 1024).toFixed(2) + ', 100'"
[attr.stroke-dashoffset]="getStrokeOffset(disk.partitions, i)"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
</ng-container>
<text x="18" y="20.35" class="percentage">{{ (disk.used / disk.total * 100).toFixed(0) }}%</text>
</svg>
</div>
<p>{{ 'diskUsedLabel' | translate }}: {{ disk.used }} GB ({{ disk.percentage }}%)</p>
@ -86,4 +89,5 @@
</div>
</ng-container>
</div>
</div>

View File

@ -4,6 +4,7 @@ import {DatePipe} from "@angular/common";
import {MatTableDataSource} from "@angular/material/table";
import {PartitionAssistantComponent} from "./partition-assistant/partition-assistant.component";
import {MatDialog} from "@angular/material/dialog";
import {Router} from "@angular/router";
interface ClientInfo {
property: string;
@ -62,7 +63,11 @@ export class ClientMainViewComponent implements OnInit {
isDiskUsageEmpty: boolean = true;
loading: boolean = true;
constructor(private http: HttpClient, private dialog: MatDialog) {
constructor(
private http: HttpClient,
private dialog: MatDialog,
private router: Router
) {
const url = window.location.href;
const segments = url.split('/');
this.clientUuid = segments[segments.length - 1];
@ -105,12 +110,13 @@ export class ClientMainViewComponent implements OnInit {
}
calculateDiskUsage() {
const diskUsageMap = new Map<number, { total: number, used: number }>();
const diskUsageMap = new Map<number, { total: number, used: number, partitions: any[] }>();
this.partitions.forEach((partition: any) => {
const diskNumber = partition.diskNumber;
if (!diskUsageMap.has(diskNumber)) {
diskUsageMap.set(diskNumber, { total: 0, used: 0 });
diskUsageMap.set(diskNumber, { total: 0, used: 0, partitions: [] });
}
const diskData = diskUsageMap.get(diskNumber);
@ -118,17 +124,28 @@ export class ClientMainViewComponent implements OnInit {
if (partition.partitionNumber === 0) {
diskData!.total = Number((partition.size / 1024).toFixed(2));
} else {
diskData!.used += Number(((partition.size * (partition.memoryUsage / 100))/ 1024).toFixed(2));
diskData!.used += Number(((partition.size * (partition.memoryUsage / 100)) / 1024).toFixed(2));
diskData!.partitions.push(partition);
}
});
this.diskUsageData = Array.from(diskUsageMap.entries()).map(([diskNumber, { total, used }]) => {
this.diskUsageData = Array.from(diskUsageMap.entries()).map(([diskNumber, { total, used, partitions }]) => {
const percentage = total > 0 ? Math.round((used / total) * 100) : 0;
return { diskNumber, total, used, percentage };
return { diskNumber, total, used, percentage, partitions };
});
this.isDiskUsageEmpty = this.diskUsageData.length === 0;
}
getStrokeOffset(partitions: any[], index: number): number {
const totalSize = partitions.reduce((acc, part) => acc + (part.size / 1024), 0);
const offset = partitions.slice(0, index).reduce((acc, part) => acc + (part.size / 1024), 0);
console.log(offset, totalSize)
return totalSize > 0 ? (offset / totalSize) : 0;
}
loadPartitions(): void {
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}`).subscribe({
next: data => {
@ -155,18 +172,14 @@ export class ClientMainViewComponent implements OnInit {
onCommandSelect(command: any): void {
if (command.name === 'Particionar y Formatear') {
this.openPartitionDialog();
this.openPartitionAssistant();
}
}
openPartitionDialog(): void {
const dialogRef = this.dialog.open(PartitionAssistantComponent, {
width: '1000px',
data: this.clientData['@id']
});
dialogRef.afterClosed().subscribe((result: any) => {
console.log('El diálogo se cerró', result);
openPartitionAssistant(): void {
console.log(this.clientData)
this.router.navigate([`/client/${this.clientData.uuid}/partition-assistant`]).then(r => {
console.log('navigated', r);
});
}
}

View File

@ -5,20 +5,12 @@
margin: 20px auto;
}
.header {
.header-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
align-items: center;
height: 100px;
padding: 10px;
background-color: #cecbcb;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header label {
font-weight: 500;
margin-right: 10px;
}
.disk-size {
@ -44,6 +36,11 @@
font-weight: 500;
font-size: 0.9rem;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
border-right: 2px solid white; /* Borde de separación */
}
.partition-segment:last-child {
border-right: none;
}
.partition-table {

View File

@ -24,17 +24,17 @@
<span class="disk-size">Tamaño: {{ (disk.totalDiskSize / 1024).toFixed(2) }} GB</span>
</div>
<div class="partition-bar">
<div
*ngFor="let partition of disk.partitions"
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
class="partition-segment"
>
{{ partition.type }} ({{ (partition.size / 1024).toFixed(2) }} GB)
</div>
<div class="partition-bar">
<div
*ngFor="let partition of disk.partitions"
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
class="partition-segment"
>
{{ partition.type }} ({{ (partition.size / 1024).toFixed(2) }} GB)
</div>
</div>
<button mat-flat-button color="primary" (click)="addPartition(disk.diskNumber)"> + </button>
<button mat-flat-button color="primary" (click)="addPartition(disk.diskNumber)"> + </button>
<table class="partition-table">
<thead>
@ -133,7 +133,3 @@
>>>>>>> Stashed changes
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>
<mat-dialog-actions align="end">
<button mat-button (click)="save()">Guardar</button>
</mat-dialog-actions>

View File

@ -2,6 +2,7 @@ import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/c
import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr';
import {MAT_DIALOG_DATA} from "@angular/material/dialog";
import {ActivatedRoute} from "@angular/router";
interface Partition {
uuid?: string;
@ -26,6 +27,8 @@ export class PartitionAssistantComponent implements OnInit {
errorMessage = '';
originalPartitions: any[] = [];
clientId: string | null = null;
data: any = {};
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[] }[] = [];
private apiUrl: string = this.baseUrl + '/partitions';
@ -33,15 +36,17 @@ export class PartitionAssistantComponent implements OnInit {
constructor(
private http: HttpClient,
private toastService: ToastrService,
@Inject(MAT_DIALOG_DATA) public data: any
private route: ActivatedRoute
) {}
ngOnInit() {
this.clientId = this.route.snapshot.paramMap.get('id');
this.loadPartitions();
}
loadPartitions() {
const url = `${this.baseUrl}${this.data}`;
const url = `${this.baseUrl}/clients/${this.clientId}`;
this.http.get(url).subscribe(
(response) => {
this.data = response;
@ -76,7 +81,7 @@ export class PartitionAssistantComponent implements OnInit {
type: partition.type || partition.filesystem || 'NTFS',
sizeBytes: partition.size,
format: false,
color: '#' + Math.floor(Math.random() * 16777215).toString(16),
color: '#1f1b91',
percentage: 0
});
}
@ -216,14 +221,13 @@ export class PartitionAssistantComponent implements OnInit {
memoryUsage: partition.memoryUsage,
size: partition.size,
filesystem: partition.type,
client: this.data['@id']
client: `/clients/${this.clientId}`
};
if (isNew) {
this.http.post(this.apiUrl, payload).subscribe(
(response) => {
this.toastService.success('Partición creada exitosamente');
this.loadPartitions();
},
(error) => {
console.error('Error al crear la partición:', error);
@ -235,7 +239,6 @@ export class PartitionAssistantComponent implements OnInit {
this.http.patch(patchUrl, payload).subscribe(
(response) => {
this.toastService.success('Partición actualizada exitosamente');
this.loadPartitions();
},
(error) => {
console.error('Error al actualizar la partición:', error);
@ -261,7 +264,6 @@ export class PartitionAssistantComponent implements OnInit {
this.http.delete(deleteUrl).subscribe(
(response) => {
this.toastService.success('Partición eliminada exitosamente');
this.loadPartitions();
},
(error) => {}
);

View File

@ -140,55 +140,54 @@
>>>>>>> Stashed changes
</div>
<div *ngIf="!loading" class="loading-container">
<mat-spinner></mat-spinner>
<div *ngIf="!loading" class="loading-container">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="loading">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="Lista de clientes filtrados por tus criterios de búsqueda.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let client" >
<ng-container *ngIf="column.columnDef === 'name'">
<div class="client-info">
<div class="client-name">{{ client.name }}</div>
<div class="client-ip">{{ client.ip }}</div>
<div class="client-mac">{{ client.mac }}</div>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef === 'status'">
<mat-chip>{{ client.status }}</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'status' && column.columnDef !== 'name'">
{{ column.cell(client) }}
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let client" style="text-align: center;" joyrideStep="actionsStep" text="Acciones disponibles para cada cliente, como ver, editar o eliminar.">
<button *ngIf="!syncStatus" mat-icon-button color="primary" (click)="getStatus(client)"><mat-icon>sync</mat-icon></button>
<button *ngIf="syncStatus" mat-icon-button color="primary"><mat-spinner diameter="24"></mat-spinner></button>
<button mat-icon-button color="info" (click)="handleClientClick($event, client)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" (click)="onEditClick($event, client.uuid)" i18n="@@editImage"><mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="onDeleteClick($event, client)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div class="paginator-container" joyrideStep="paginationStep" text="Navega entre las páginas de resultados utilizando el paginador.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"
[pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>
<div *ngIf="loading">
<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 client" >
<ng-container *ngIf="column.columnDef === 'name'">
<div class="client-info">
<div class="client-name">{{ client.name }}</div>
<div class="client-ip">{{ client.ip }}</div>
<div class="client-mac">{{ client.mac }}</div>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef === 'status'">
<mat-chip>
{{ client.status }}
</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'status' && column.columnDef !== 'name'" >
{{ column.cell(client) }}
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let client" style="text-align: center;">
<button *ngIf="!syncStatus" mat-icon-button color="primary" (click)="getStatus(client)"><mat-icon>sync</mat-icon></button>
<button *ngIf="syncStatus" mat-icon-button color="primary"><mat-spinner diameter="24"></mat-spinner></button>
<button mat-icon-button color="info" (click)="handleClientClick($event, client)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" (click)="onEditClick($event, client.uuid)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="onDeleteClick($event, client)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div class="paginator-container">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"
[pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>
</div>

View File

@ -13,6 +13,7 @@ import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import {ClientViewComponent} from "../../shared/client-view/client-view.component";
import { Router } from '@angular/router';
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-client-tab-view',
@ -78,7 +79,8 @@ export class ClientTabViewComponent {
public dialog: MatDialog,
private toastService: ToastrService,
private http: HttpClient,
private router: Router
private router: Router,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
@ -190,4 +192,20 @@ export class ClientTabViewComponent {
this.length = event.length;
this.getClients();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'title3Step',
'resetFiltersStep',
'addClientStep',
'searchContainerStep',
'tableStep',
'actionsStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -39,15 +39,17 @@
.mat-chip-success {
background-color: #4CAF50 !important;
color: white !important;
align-items: center;
vertical-align: middle;
}
.mat-chip-error {
background-color: #F44336 !important;
color: white !important;
}
.button-row{
display: flex;
gap: 10px;
}
.calendar-ico{
margin-top: 5px;
color: gray;
}

View File

@ -15,6 +15,7 @@
>>>>>>> Stashed changes
</div>
</div>
<mat-divider class="divider"></mat-divider>
<<<<<<< Updated upstream
=======
@ -27,10 +28,11 @@
<mat-icon matSuffix>search</mat-icon>
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-boolean">
<<<<<<< Updated upstream
<mat-label i18n="@@searchLabel">Tipo</mat-label>
<mat-select [(ngModel)]="filters['type']" (selectionChange)="search()" placeholder="Seleccionar opción" >
<mat-select [(ngModel)]="filters['type']" (selectionChange)="search()" placeholder="Seleccionar opción">
<mat-option [value]="''">Todos</mat-option>
<mat-option [value]="'organizational-unit'">Centro</mat-option>
<mat-option [value]="'classrooms-group'">Grupos de aulas</mat-option>
@ -39,16 +41,17 @@
</mat-select>
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="Aquí se muestran las unidades organizativas según los filtros aplicados.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let ou" >
<td mat-cell *matCellDef="let ou">
<ng-container *ngIf="column.columnDef !== 'available' && column.columnDef !== 'type'">
{{ column.cell(ou) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'available'" >
<mat-chip *ngIf="ou.available" class="mat-chip-success"><mat-icon style="color:white;">check</mat-icon></mat-chip>
<mat-chip *ngIf="!ou.available" class="mat-chip-error"> <mat-icon style="color:white;">close</mat-icon></mat-chip>
<ng-container *ngIf="column.columnDef === 'available'">
<mat-chip *ngIf="ou.available" class="mat-chip-success"><mat-icon class="calendar-ico">event_available</mat-icon></mat-chip>
<mat-chip *ngIf="!ou.available" class="mat-chip-error"><mat-icon class="calendar-ico">event_busy</mat-icon></mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef === 'type'" >
<mat-chip> {{ ou.type }} </mat-chip>
@ -89,9 +92,9 @@
<ng-container matColumnDef="actions">
<<<<<<< Updated upstream
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let ou" style="text-align: center;">
<td mat-cell *matCellDef="let ou" style="text-align: center;" joyrideStep="actionsStep" text="Usa estas opciones para ver, editar o eliminar una unidad organizativa.">
<button mat-icon-button color="info" (click)="onShowClick($event, ou)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" (click)="onEditClick($event, ou.uuid)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="primary" (click)="onEditClick($event, ou.uuid)" i18n="@@editImage"><mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="onDeleteClick($event, ou)"><mat-icon>delete</mat-icon></button>
=======
<th mat-header-cell *matHeaderCellDef>{{ 'columnActions' | translate }}</th>

View File

@ -21,6 +21,7 @@ import {
} from "../../shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component";
import {ClassroomViewDialogComponent} from "../../shared/classroom-view/classroom-view-modal";
import {TreeViewComponent} from "../../shared/tree-view/tree-view.component";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-organizational-unit-tab-view',
@ -79,7 +80,8 @@ export class OrganizationalUnitTabViewComponent {
private dataService: DataService,
public dialog: MatDialog,
private toastService: ToastrService,
private http: HttpClient
private http: HttpClient,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
@ -169,4 +171,21 @@ export class OrganizationalUnitTabViewComponent {
this.length = event.length;
this.getOrganizationalUnits();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleS4tep',
'resetFiltersStep',
'addOUStep',
'searchContainerStep',
'tableStep',
'actionsStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -18,6 +18,8 @@
.card {
flex-grow: 1;
margin: 10px;
border: 2px solid rgba(102, 102, 102, 0.103)
}
.header-container {
@ -28,8 +30,17 @@
padding: 10px;
}
.unidad-card, .elements-card {
flex: 1 1 45%;
.unidad-card {
flex: 1 1 20%;
background-color: #fafafa;
height: 600px;
overflow-y: auto;
box-shadow: none !important;
}
.elements-card {
flex: 1 1 75%;
background-color: #fafafa;
height: 600px;
overflow-y: auto;
@ -183,18 +194,13 @@ mat-spinner {
.result-card {
width: 100%;
max-width: 250px;
<<<<<<< Updated upstream
height: 250px; /* Fijo para mantener la forma cuadrada */
}
=======
height: 250px;
}
>>>>>>> Stashed changes
.paginator-container {
display: flex;
justify-content: center;
margin-bottom: 30px;
}
.divider {
@ -204,3 +210,7 @@ mat-spinner {
mat-card {
margin-bottom: 20px;
}
.mat-tooltip {
white-space: pre-line;
}

View File

@ -21,6 +21,7 @@
>>>>>>> Stashed changes
</div>
</div>
<div class="groupLists-container">
<<<<<<< Updated upstream
<mat-card class="card unidad-card">
@ -58,6 +59,7 @@
<span>{{ 'viewTreeMenu' | translate }}</span>
>>>>>>> Stashed changes
</button>
<button mat-menu-item (click)="onEditClick($event, unidad.type, unidad.uuid)">
<<<<<<< Updated upstream
<mat-icon
@ -72,6 +74,7 @@
<span>{{ 'editUnitMenu' | translate }}</span>
>>>>>>> Stashed changes
</button>
<button mat-menu-item (click)="onShowClick($event, unidad)">
<<<<<<< Updated upstream
<mat-icon
@ -86,6 +89,7 @@
<span>{{ 'viewUnitMenu' | translate }}</span>
>>>>>>> Stashed changes
</button>
<button mat-menu-item (click)="addOU($event, unidad)">
<<<<<<< Updated upstream
<mat-icon
@ -100,6 +104,7 @@
<span>{{ 'addInternalUnitMenu' | translate }}</span>
>>>>>>> Stashed changes
</button>
<button mat-menu-item (click)="addClient($event, unidad)">
<<<<<<< Updated upstream
<mat-icon
@ -145,7 +150,9 @@
<mat-icon>info</mat-icon>
<span>{{ 'noInternalElementsMessage' | translate }}</span>
</div>
<mat-list-item *ngFor="let child of children" [ngClass]="{'selected-item': child === selectedUnidad, 'clickable-item': true}" (click)="onSelectChild(child)">
<mat-list-item *ngFor="let child of children"
[ngClass]="{'selected-item': child === selectedUnidad, 'clickable-item': true}"
(click)="onSelectChild(child)">
<div class="item-content">
<mat-icon [ngSwitch]="child.type">
<ng-container *ngSwitchCase="'organizational-unit'">apartment</ng-container>
@ -168,6 +175,7 @@
<span>{{ 'editElementMenu' | translate }}</span>
>>>>>>> Stashed changes
</button>
<button *ngIf="child.type !== 'client'" mat-menu-item (click)="onShowClick($event, child)">
<<<<<<< Updated upstream
<mat-icon class="edit-icon" #tooltip="matTooltip" matTooltip="Visualizar unidad organizativa" matTooltipHideDelay="0" i18n="@@viewUnitTooltip">visibility</mat-icon>
@ -177,6 +185,7 @@
<span>{{ 'viewUnitMenu' | translate }}</span>
>>>>>>> Stashed changes
</button>
<button *ngIf="child.type !== 'client'" mat-menu-item (click)="addOU($event, child)">
<<<<<<< Updated upstream
<mat-icon class="edit-icon" #tooltip="matTooltip" matTooltip="Crear unidad organizativa interna" matTooltipHideDelay="0" i18n="@@addInternalUnitTooltip">add_home_work</mat-icon>
@ -186,6 +195,7 @@
<span>{{ 'addInternalUnitMenu' | translate }}</span>
>>>>>>> Stashed changes
</button>
<button *ngIf="child.type !== 'client'" mat-menu-item (click)="addClient($event, child)">
<<<<<<< Updated upstream
<mat-icon class="edit-icon" #tooltip="matTooltip" matTooltip="Crear cliente en esta unidad organizativa" matTooltipHideDelay="0" i18n="@@addClientTooltip">devices</mat-icon>
@ -195,6 +205,7 @@
<span>{{ 'addClientMenu' | translate }}</span>
>>>>>>> Stashed changes
</button>
<button mat-menu-item (click)="onDeleteClick($event, child.uuid, child.name, child.type)">
<<<<<<< Updated upstream
<mat-icon class="delete-icon" #tooltip="matTooltip" matTooltip="Borrar elemento" matTooltipHideDelay="0" i18n="@@deleteElementTooltip">delete</mat-icon>
@ -222,9 +233,11 @@
<mat-tab i18n-label label="Búsqueda avanzada">
<app-advanced-search></app-advanced-search>
</mat-tab>
<mat-tab i18n-label label="Clientes">
<app-client-tab-view #clientTab></app-client-tab-view>
</mat-tab>
<mat-tab i18n-label label="Unidades organizativas">
=======

View File

@ -25,6 +25,9 @@ import {ClientTabViewComponent} from "./components/client-tab-view/client-tab-vi
import {
OrganizationalUnitTabViewComponent
} from "./components/organizational-unit-tab-view/organizational-unit-tab-view.component";
import { ExecuteCommandComponent } from '../commands/main-commands/execute-command/execute-command.component';
import { ExecuteCommandOuComponent } from './shared/execute-command-ou/execute-command-ou.component';
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-groups',
@ -67,7 +70,8 @@ export class GroupsComponent implements OnInit {
public dialog: MatDialog,
private toastService: ToastrService,
private _bottomSheet: MatBottomSheet,
private http: HttpClient
private http: HttpClient,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
@ -298,6 +302,21 @@ export class GroupsComponent implements OnInit {
}
}
onExecuteCommand(event: MouseEvent, child: any, name: string, type:string): void {
console.log('Executing command on:', child);
this.dialog.open(ExecuteCommandOuComponent, {
width: '50%',
data: { childUnitUuid: child }
}).afterClosed().subscribe((result) => {
if (result) {
console.log('Comando ejecutado con éxito');
} else {
console.log('Ejecución de comando cancelada');
}
});
}
openSnackBar(isError: boolean, message: string) {
if (isError) {
this.toastService.error(' Error al eliminar la entidad: ' + message, 'Error');
@ -432,4 +451,11 @@ export class GroupsComponent implements OnInit {
const dialogRef = this.dialog.open(AcctionsModalComponent, { data: { selectedElements: this.selectedElements }, width: '700px'});
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: ['titleStep', 'addStep', 'keyStep', 'unitStep', 'elementsStep', 'tabsStep'],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -0,0 +1,57 @@
.form-container {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
}
.command-form {
width: 100%;
}
.full-width {
width: 100%;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 8px;
}
.checkbox-group label {
font-weight: bold;
margin-bottom: 8px;
}
.mat-checkbox {
margin-left: 8px;
}
.mat-dialog-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 12px;
}
.mat-dialog-content {
max-height: 60vh;
overflow-y: auto;
}
.mat-dialog-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px;
}
button[mat-button] {
font-weight: 500;
}
button[mat-button]:disabled {
color: rgba(0, 0, 0, 0.38);
}

View File

@ -0,0 +1,120 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
@Component({
selector: 'app-execute-command-ou',
templateUrl: './execute-command-ou.component.html',
styleUrls: ['./execute-command-ou.component.css']
})
export class ExecuteCommandOuComponent implements OnInit {
form: FormGroup;
clients: any[] = [];
commands: any[] = [];
commandGroups: any[] = [];
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
constructor(
private dialogRef: MatDialogRef<ExecuteCommandOuComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private http: HttpClient,
private fb: FormBuilder,
private toastService: ToastrService,
) {
this.form = this.fb.group({
selectedCommand: [null],
selectedCommandGroup: [null],
clientSelection: [[]]
});
}
ngOnInit(): void {
this.loadClients();
this.loadCommands();
this.loadCommandGroups();
}
loadClients(): void {
this.http.get<any>(`${this.baseUrl}/organizational-units/${this.data.childUnitUuid}`).subscribe(
response => {
this.clients = this.getAllClients(response);
const clientIds = this.clients.map(client => client.uuid);
this.form.get('clientSelection')?.setValue(clientIds);
},
error => console.error('Error al cargar los clientes:', error)
);
}
getAllClients(unit: any): any[] {
let allClients = unit.clients || [];
if (unit.children && unit.children.length > 0) {
unit.children.forEach((child: any) => {
allClients = allClients.concat(this.getAllClients(child));
});
}
return allClients;
}
loadCommands(): void {
this.http.get<any>(`${this.baseUrl}/commands?page=1&itemsPerPage=30`).subscribe(
response => {
this.commands = response['hydra:member'] || [];
},
error => this.toastService.error('Error al cargar comandos:', error)
);
}
loadCommandGroups(): void {
this.http.get<any>(`${this.baseUrl}/command-groups?page=1&itemsPerPage=30`).subscribe(
response => {
this.commandGroups = response['hydra:member'] || [];
},
error => this.toastService.error('Error al cargar grupos de comandos:', error)
);
}
toggleClientSelection(clientId: string): void {
const selectedClients = this.form.get('clientSelection')?.value || [];
if (selectedClients.includes(clientId)) {
this.form.get('clientSelection')?.setValue(selectedClients.filter((id: string) => id !== clientId));
} else {
this.form.get('clientSelection')?.setValue([...selectedClients, clientId]);
}
}
executeCommand(): void {
const selectedCommandUuid = this.form.get('selectedCommand')?.value || this.form.get('selectedCommandGroup')?.value;
const isCommandGroup = !!this.form.get('selectedCommandGroup')?.value;
if (!selectedCommandUuid) {
console.warn('No se ha seleccionado ningún comando o grupo de comandos');
return;
}
const payload = {
clients: (this.form.get('clientSelection')?.value || []).map((clientId: string) => `/clients/${clientId}`)
};
const url = isCommandGroup
? `${this.baseUrl}/command-groups/${selectedCommandUuid}/execute`
: `${this.baseUrl}/commands/${selectedCommandUuid}/execute`;
this.http.post(url, payload)
.subscribe({
next: () => {
this.toastService.success('Comando ejecutado con éxito');
this.dialogRef.close(true);
},
error: (error) => {
this.toastService.error('Error al ejecutar el comando:', error);
this.dialogRef.close(false);
}
});
}
closeModal(): void {
this.dialogRef.close(false);
}
}

View File

@ -14,6 +14,7 @@ export class CreateImageComponent implements OnInit {
imageForm: FormGroup<any>;
imageId: string | null = null;
softwareProfiles: any[] = [];
repositories: any[] = [];
constructor(
private fb: FormBuilder,
@ -29,6 +30,7 @@ export class CreateImageComponent implements OnInit {
comments: [''],
remotePc: [false],
softwareProfile: ['', Validators.required],
imageRepository: ['', Validators.required],
});
}
@ -37,6 +39,7 @@ export class CreateImageComponent implements OnInit {
this.load()
}
this.fetchSoftwareProfiles();
this.fetchRepositories();
}
load(): void {
@ -47,7 +50,8 @@ export class CreateImageComponent implements OnInit {
description: [response.description],
comments: [response.comments],
remotePc: [response.remotePc],
softwareProfile: [response.softwareProfile, Validators.required],
softwareProfile: [response.softwareProfile['@id'], Validators.required],
imageRepository: [response.repository['@id'], Validators.required],
});
this.imageId = response['@id'];
},
@ -70,13 +74,32 @@ export class CreateImageComponent implements OnInit {
});
}
fetchRepositories() {
const url = `${this.baseUrl}/image-repositories`;
this.http.get(url).subscribe({
next: (response: any) => {
this.repositories = response['hydra:member'];
},
error: (error) => {
console.error('Error al obtener los repositorios de imágenes:', error);
this.toastService.error('Error al obtener los repositorios de imágenes');
}
});
}
saveImage(): void {
if (this.imageForm.invalid) {
this.toastService.error('Por favor, rellena los campos obligatorios');
return;
}
const payload = {
name: this.imageForm.value.name,
description: this.imageForm.value.description,
comments: this.imageForm.value.comments,
remotePc: this.imageForm.value.remotePc,
softwareProfile: this.imageForm.value.softwareProfile
softwareProfile: this.imageForm.value.softwareProfile,
imageRepository: this.imageForm.value.imageRepository,
};
if (this.imageId) {

View File

@ -15,16 +15,18 @@
</button>
>>>>>>> Stashed changes
</div>
<mat-divider class="divider"></mat-divider>
</div>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-label>Buscar nombre de imagen</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchImageField" text="Busca una imagen por nombre. Pulsa 'enter' para iniciar la búsqueda.">
<mat-label>Buscar nombre de imagen</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
</div>
<<<<<<< Updated upstream
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
@ -42,18 +44,20 @@
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let client" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editImage($event, client)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="deleteImage($event, client)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;" joyrideStep="actionsHeader" text="Acciones disponibles para cada imagen.">Acciones</th>
<td mat-cell *matCellDef="let client" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editImage($event, client)" joyrideStep="editImageButton" text="Editar la imagen seleccionada.">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteImage($event, client)" joyrideStep="deleteImageButton" text="Eliminar la imagen seleccionada.">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div class="paginator-container">
<mat-paginator [length]="length"

View File

@ -7,6 +7,7 @@ import { DatePipe } from '@angular/common';
import { CreateImageComponent } from './create-image/create-image.component';
import {CreateCommandComponent} from "../commands/main-commands/create-command/create-command.component";
import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delete-modal.component";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-images',
@ -38,9 +39,14 @@ export class ImagesComponent implements OnInit {
header: 'Perfil de software',
cell: (image: any) => `${image.softwareProfile?.description}`
},
{
columnDef: 'imageRepository',
header: 'Repositorio',
cell: (image: any) => `${image.imageRepository?.name}`
},
{
columnDef: 'remotePc',
header: 'Acceso remoto',
header: 'Remote Pc',
cell: (image: any) => `${image.remotePc}`
},
{
@ -56,7 +62,8 @@ export class ImagesComponent implements OnInit {
constructor(
public dialog: MatDialog,
private http: HttpClient,
private toastService: ToastrService
private toastService: ToastrService,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
@ -122,4 +129,22 @@ export class ImagesComponent implements OnInit {
this.length = event.length;
this.search();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'imagesTitleStep',
'addImageButton',
'searchImageField',
'imagesTable',
'actionsHeader',
'editImageButton',
'deleteImageButton',
'imagesPagination'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -11,17 +11,15 @@
<mat-label>{{ 'loginlabelPassword' | translate }}</mat-label>
<input matInput (keydown.enter)="$event.preventDefault()" [type]="hide() ? 'password' : 'text'" required
[(ngModel)]="loginObj.password" name="password" />
<button mat-icon-button matSuffix (click)="clickEvent($event)" [attr.aria-label]="'Ocultar contraseña'">
<button mat-icon-button matSuffix type="button" (click)="clickEvent($event)" [attr.aria-label]="'Ocultar contraseña'">
<mat-icon>{{hide() ? 'visibility_off' : 'visibility'}}</mat-icon>
</button>
</mat-form-field>
<div class="button-row">
<button mat-flat-button color="primary" type="submit" [disabled]="!loginObj.username || !loginObj.password">
{{ 'buttonLogin' | translate }}
</button>
</div>
</form>
</div>
<<<<<<< Updated upstream

View File

@ -1,4 +1,11 @@
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 100px;
padding: 10px;
}
.disk-usage-info{
display: flex;
justify-content: start;

View File

@ -22,19 +22,19 @@
</div>
</div>
<div class="services-status">
<h3>Servicios</h3>
<ul>
<li *ngFor="let service of getServices()">
<span
class="status-led"
[ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }"
></span>
{{ service.name }}: {{ service.status }}
</li>
</ul>
</div>
<div class="services-status" joyrideStep="servicesStatusStep" text="Aquí puedes ver el estado de los servicios importantes del servidor.">
<h3>Servicios</h3>
<ul>
<li *ngFor="let service of getServices()">
<span
class="status-led"
[ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }"
></span>
{{ service.name }}: {{ service.status }}
</li>
</ul>
</div>
</div>
<div class="installed-oglives">
<h3>OGLives instalados</h3>

View File

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-ogboot-status',
@ -15,7 +16,6 @@ export class OgbootStatusComponent implements OnInit {
view: [number, number] = [1100, 500];
// Opciones de la gráfica
gradient: boolean = true;
showLegend: boolean = true;
showLabels: boolean = true;
@ -24,7 +24,7 @@ export class OgbootStatusComponent implements OnInit {
domain: ['#FF6384', '#3f51b5']
};
constructor(private http: HttpClient) {}
constructor(private http: HttpClient, private joyrideService: JoyrideService) {}
ngOnInit(): void {
this.loadStatus();
@ -57,4 +57,30 @@ export class OgbootStatusComponent implements OnInit {
status: this.servicesStatus[key]
}));
}
formatBytes(bytes: number): string {
if (bytes >= 1e9) {
return (bytes / 1e9).toFixed(2) + ' GB';
} else if (bytes >= 1e6) {
return (bytes / 1e6).toFixed(2) + ' MB';
} else if (bytes >= 1e3) {
return (bytes / 1e3).toFixed(2) + ' KB';
} else {
return bytes + ' B';
}
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'diskUsageStep',
'servicesStatusStep',
'oglivesStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -1,11 +1,10 @@
<div class="header-container">
<<<<<<< Updated upstream
<h2 class="title">Netboot avanzado</h2>
</div>
<div [formGroup]="taskForm" class="search-container">
<mat-form-field appearance="fill" class="search-boolean">
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="selectUnitStep" text="Selecciona la Unidad Organizacional para listar las aulas disponibles.">
<mat-label>Selecciona Unidad Organizacional</mat-label>
=======
<h2 class="title" joyrideStep="titleStep" text="{{ 'advancedNetbootDescription' | translate }}">
@ -55,12 +54,12 @@
<<<<<<< Updated upstream
<mat-form-field appearance="fill" class="selected-global">
<mat-label>Seleccione plantilla para aplicar a todos los clientes</mat-label>
<mat-select [(value)]="globalOgLive" (selectionChange)="applyToAll()" >
<mat-select [(value)]="globalOgLive" (selectionChange)="applyToAll()">
<mat-option *ngFor="let option of ogLiveOptions" [value]="option['@id']">{{ option.name }}</mat-option>
</mat-select>
</mat-form-field>
<button mat-flat-button color="primary" [disabled]="selectedUnitChildren.length === 0" (click)="saveOgLiveTemplates()">Guardar</button>
<button mat-flat-button color="primary" [disabled]="selectedUnitChildren.length === 0" (click)="saveOgLiveTemplates()" joyrideStep="saveButtonStep" text="Haz clic para guardar la configuración actual de plantillas.">Guardar</button>
</div>
<mat-table [dataSource]="dataSource" class="mat-elevation-z8">
@ -117,7 +116,6 @@
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>

View File

@ -5,6 +5,7 @@ import { ToastrService } from 'ngx-toastr';
import { PageEvent } from '@angular/material/paginator';
import {Observable} from "rxjs";
import {ServerInfoDialogComponent} from "../../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-pxe-boot-files',
@ -32,7 +33,8 @@ export class PxeBootFilesComponent implements OnInit {
constructor(
private fb: FormBuilder,
private http: HttpClient,
private toastService: ToastrService
private toastService: ToastrService,
private joyrideService: JoyrideService
) {
this.taskForm = this.fb.group({
organizationalUnit: ['', Validators.required],
@ -155,4 +157,21 @@ export class PxeBootFilesComponent implements OnInit {
this.itemsPerPage = event.pageSize;
this.fetchPxeTemplates();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'selectUnitStep',
'selectClassStep',
'applyToAllStep',
'saveButtonStep',
'tableStep',
'selectTemplateStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -8,7 +8,6 @@
<mat-panel-title>{{ 'serverInfoTitle' | translate }}</mat-panel-title>
>>>>>>> Stashed changes
</mat-expansion-panel-header>
<div class="button-row">
<<<<<<< Updated upstream
<button mat-flat-button color="primary" (click)="syncOgBoot()"> Sincronizar base de datos</button>
@ -44,7 +43,9 @@
>>>>>>> Stashed changes
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<<<<<<< Updated upstream
<mat-form-field appearance="fill" class="search-string">
@ -61,13 +62,14 @@
<<<<<<< Updated upstream
<mat-form-field appearance="fill" class="search-boolean">
<mat-label i18n="@@searchLabel">Imagen por defecto</mat-label>
<mat-select [(ngModel)]="filters['isDefault']" (selectionChange)="search()" placeholder="Seleccionar opción" >
<mat-select [(ngModel)]="filters['isDefault']" (selectionChange)="search()" placeholder="Seleccionar opción">
<mat-option [value]="''">Todos</mat-option>
<mat-option [value]="true"></mat-option>
<mat-option [value]="false">No</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="search-boolean">
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="searchInstalledStep" text="Filtra las imágenes para mostrar solo las instaladas en el servidor OgBoot.">
<mat-label i18n="@@searchLabel">Instalado servidor ogBoot</mat-label>
<mat-select [(ngModel)]="filters['installed']" (selectionChange)="search()" placeholder="Seleccionar opción">
<mat-option [value]="''">Todos</mat-option>
@ -76,7 +78,8 @@
</mat-select>
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="Aquí se muestra la lista de imágenes disponibles para administrar.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image" >
@ -139,16 +142,15 @@
<ng-container *ngIf="column.columnDef !== 'isDefault' && column.columnDef !== 'installed' && column.columnDef !== 'downloadUrl' && column.columnDef !== 'status' && column.columnDef !== 'name'">
{{ column.cell(image) }}
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<<<<<<< Updated upstream
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions">Acciones</th>
<td mat-cell *matCellDef="let image">
<td mat-cell *matCellDef="let image" joyrideStep="actionsStep" text="Administra cada imagen con opciones para ver, editar, eliminar y más.">
<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="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>
=======
<th mat-header-cell *matHeaderCellDef>{{ 'actionsColumnHeader' | translate }}</th>

View File

@ -12,6 +12,7 @@ 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";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-pxe-images',
@ -77,7 +78,8 @@ export class PXEimagesComponent implements OnInit {
public dialog: MatDialog,
private http: HttpClient,
private dataService: DataService,
private toastService: ToastrService
private toastService: ToastrService,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
@ -254,4 +256,23 @@ export class PXEimagesComponent implements OnInit {
this.toastService.error('Error al sincronizar');
});
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'serverInfoStep',
'titleStep',
'addImageStep',
'searchNameStep',
'searchDefaultImageStep',
'searchInstalledStep',
'tableStep',
'actionsStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -1,4 +1,3 @@
mat-form-field {
width: 100%;
margin-bottom: 20px;
@ -53,9 +52,9 @@ h3 {
.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 */
align-items: flex-start;
justify-content: space-between;
width: 100%;
}
.text-content {
@ -73,3 +72,15 @@ h3 {
margin-left: 8px;
cursor: pointer;
}
.actions-container {
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
}
.action-buttons {
display: flex;
gap: 8px;
}

View File

@ -7,7 +7,6 @@
<mat-dialog-content>
<div class="spacing-container">
<form [formGroup]="templateForm" (ngSubmit)="onSave()">
<mat-form-field appearance="fill">
<mat-label>{{ 'templateNameLabel' | translate }}</mat-label>
<input matInput formControlName="name" [placeholder]="'templateNamePlaceholder' | translate">

View File

@ -1,9 +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, MatDialog} 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";
import { DeleteModalComponent } from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
@Component({
selector: 'app-create-pxe-template',
@ -17,6 +17,39 @@ export class CreatePxeTemplateComponent implements OnInit {
isEditMode: boolean = false;
clients: any[] = [];
templateModels = {
ogLive: `#!ipxe
set timeout 0
set timeout-style hidden
set ISODIR __OGLIVE__
set default 0
set kernelargs __INFOHOST__
:try_iso
kernel http://__SERVERIP__/tftpboot/\${ISODIR}/ogvmlinuz \${kernelargs} || goto fallback
initrd http://__SERVERIP__/tftpboot/\${ISODIR}/oginitrd.img
boot
:fallback
set ISODIR ogLive
kernel http://__SERVERIP__/tftpboot/\${ISODIR}/ogvmlinuz \${kernelargs}
initrd http://__SERVERIP__/tftpboot/\${ISODIR}/oginitrd.img
boot`,
disco: `#!ipxe
iseq \${platform} efi && goto uefi_boot || goto bios_boot
:bios_boot
echo "Running in BIOS mode - Booting first disk"
chain http://__SERVERIP__/tftpboot/grub.exe --config-file="title FirstHardDisk;chainloader (hd0)+1;rootnoverify (hd0);boot" || echo "Failed to boot in BIOS mode"
exit
:uefi_boot
echo "Running in UEFI mode - Booting first disk"
sanboot --no-describe --drive 0 --filename \\EFI\\grub\\Boot\\grubx64.efi || echo "Failed to boot in UEFI mode"
exit`
};
constructor(
public dialogRef: MatDialogRef<CreatePxeTemplateComponent>,
public dialog: MatDialog,
@ -29,8 +62,8 @@ export class CreatePxeTemplateComponent implements OnInit {
ngOnInit() {
this.isEditMode = !!this.data;
if (this.isEditMode){
this.getPxeClients()
if (this.isEditMode) {
this.getPxeClients();
}
this.templateForm = this.fb.group({
@ -50,7 +83,7 @@ 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']
this.clients = data['hydra:member'];
},
error: error => {
console.error('Error al obtener los clientes PXE:', error);
@ -67,12 +100,10 @@ export class CreatePxeTemplateComponent implements OnInit {
this.http.post<any>(`${this.baseUrl}/pxe-templates`, payload).subscribe({
next: data => {
console.log('Plantilla PXE creada:', data);
this.toastService.success('Plantilla PXE creada exitosamente');
this.dialogRef.close(true);
},
error: error => {
console.error('Error al crear la plantilla PXE:', error);
this.toastService.error('Error al crear la plantilla PXE');
this.dialogRef.close(false);
}
@ -98,13 +129,19 @@ export class CreatePxeTemplateComponent implements OnInit {
});
}
loadTemplateModel(type: 'ogLive' | 'disco'): void {
const selectedContent = this.templateModels[type];
this.templateForm.get('templateContent')?.setValue(selectedContent);
this.toastService.info(`Plantilla ${type} cargada.`);
}
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 => {
@ -126,11 +163,12 @@ export class CreatePxeTemplateComponent implements OnInit {
this.toastService.success('Cliente eliminado exitosamente');
this.dialogRef.close();
},
error: (error) => {
error: error => {
this.toastService.error(error.error['hydra:description']);
}
});
}})
}
});
}
onCancel(): void {

View File

@ -20,7 +20,6 @@
</mat-expansion-panel>
</mat-accordion>
<div class="header-container">
<<<<<<< Updated upstream
<h2 class="title" i18n="@@adminPXETitle">Administrar plantillas PXE</h2>
@ -36,7 +35,9 @@
>>>>>>> Stashed changes
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<<<<<<< Updated upstream
<mat-form-field appearance="fill" class="search-string">
@ -53,14 +54,15 @@
<<<<<<< Updated upstream
<mat-form-field appearance="fill" class="search-boolean">
<mat-label i18n="@@searchLabel">Creada en ogBoot</mat-label>
<mat-select [(ngModel)]="filters['synchronized']" (selectionChange)="search()" placeholder="Seleccionar opción" >
<mat-select [(ngModel)]="filters['synchronized']" (selectionChange)="search()" placeholder="Seleccionar opción">
<mat-option [value]="''">Todos</mat-option>
<mat-option [value]="true"></mat-option>
<mat-option [value]="false">No</mat-option>
</mat-select>
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="Aquí se muestra la lista de plantillas PXE disponibles para administrar.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image" >
@ -94,7 +96,7 @@
<<<<<<< Updated upstream
<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;">
<td mat-cell *matCellDef="let template" style="text-align: center;" joyrideStep="actionsStep" text="Gestiona cada plantilla PXE con opciones para ver, editar, eliminar y más.">
<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>
@ -118,7 +120,7 @@
<mat-menu #menu="matMenu">
<<<<<<< Updated upstream
<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)="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>
=======

View File

@ -20,6 +20,7 @@ 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";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-pxe',
@ -72,7 +73,8 @@ export class PxeComponent {
public dialog: MatDialog,
private http: HttpClient,
private toastService: ToastrService,
private dataService: DataService
private dataService: DataService,
private joyrideService: JoyrideService
) { }
ngOnInit(): void {
@ -228,4 +230,22 @@ export class PxeComponent {
this.itemsPerPage = event.pageSize;
this.applyFilter();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'serverInfoStep',
'titleStep',
'addTemplateStep',
'searchNameStep',
'searchSyncStep',
'tableStep',
'actionsStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -1,25 +1,32 @@
<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 appearance="fill" class="full-width">
<mat-label>Unidad Organizativa</mat-label>
<mat-select [formControl]="unitControl" (selectionChange)="onUnitChange($event.value)">
<mat-option *ngFor="let unit of units" [value]="unit.uuid">{{ unit.name }}</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="selectedClients.length > 0">
<h3>Clientes seleccionados:</h3>
<ul>
<li *ngFor="let client of selectedClients">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Subunidad Organizativa</mat-label>
<mat-select [formControl]="childUnitControl" (selectionChange)="onChildUnitChange($event.value)">
<mat-option *ngFor="let child of childUnits" [value]="child.uuid">{{ child.name }}</mat-option>
</mat-select>
</mat-form-field>
<div class="checkbox-group">
<label>Clientes</label>
<div *ngIf="clients.length > 0">
<mat-checkbox *ngFor="let client of clients"
(change)="toggleClientSelection(client.uuid)"
[checked]="selectedClients.includes(client.uuid)">
{{ client.name }}
<button mat-icon-button color="warn" (click)="removeClient(client)">
<mat-icon>delete</mat-icon>
</button>
</li>
</ul>
</mat-checkbox>
</div>
<div *ngIf="clients.length === 0">
<p>No hay clientes disponibles</p>
</div>
</div>
</mat-dialog-content>

View File

@ -1,10 +1,8 @@
import { Component, OnInit, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import {Observable, startWith} from "rxjs";
import {map} from "rxjs/operators";
import {FormControl} from "@angular/forms";
import {ToastrService} from "ngx-toastr";
import { FormControl } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
@Component({
selector: 'app-add-clients-to-subnet',
@ -13,59 +11,93 @@ import {ToastrService} from "ngx-toastr";
})
export class AddClientsToSubnetComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
units: any[] = [];
childUnits: any[] = [];
clients: any[] = [];
selectedClients: any[] = [];
selectedClients: string[] = [];
loading: boolean = true;
filters: { [key: string]: string } = {};
filteredClients!: Observable<any[]>;
clientControl = new FormControl();
unitControl = new FormControl();
childUnitControl = new FormControl();
constructor(
private http: HttpClient,
public dialogRef: MatDialogRef<AddClientsToSubnetComponent>,
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.loadUnits();
}
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()))
loadUnits() {
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=50`).subscribe(
response => {
this.units = response['hydra:member'].filter((unit: { type: string; }) => unit.type === 'organizational-unit');
this.loading = false;
},
error => console.error('Error fetching organizational units:', error)
);
}
loadClients() {
this.http.get<any>( `${this.baseUrl}/clients?&page=1&itemsPerPage=10000&exists[subnet]=false`).subscribe(
response => {
this.clients = response['hydra:member'];
this.loading = false;
},
error => {
console.error('Error fetching parent units:', error);
this.loading = false;
onUnitChange(unitId: string): void {
const unit = this.units.find(unit => unit.uuid === unitId);
this.childUnits = unit ? this.getAllChildren(unit) : [];
this.clients = [];
this.childUnitControl.setValue(null);
this.selectedClients = [];
}
getAllChildren(unit: any): any[] {
let allChildren = [];
if (unit.children && unit.children.length > 0) {
for (const child of unit.children) {
allChildren.push(child);
allChildren = allChildren.concat(this.getAllChildren(child));
}
);
}
return allChildren;
}
onChildUnitChange(childUnitId: string): void {
const childUnit = this.childUnits.find(unit => unit.uuid === childUnitId);
this.clients = childUnit && childUnit.clients ? childUnit.clients : [];
this.selectedClients = [];
}
toggleClientSelection(clientId: string): void {
const index = this.selectedClients.indexOf(clientId);
if (index >= 0) {
this.selectedClients.splice(index, 1);
} else {
this.selectedClients.push(clientId);
}
}
toggleSelectAll(): void {
if (this.areAllClientsSelected()) {
this.selectedClients = [];
} else {
this.selectedClients = this.clients.map(client => client.uuid);
}
}
areAllClientsSelected(): boolean {
return this.selectedClients.length === this.clients.length;
}
save() {
this.selectedClients.forEach(client => {
const postData = {
client: client['@id']
};
this.selectedClients.forEach(clientId => {
const postData = { client: `/clients/${clientId}` };
this.http.post(`${this.baseUrl}/og-dhcp/server/${this.data.subnetUuid}/post-host`, postData).subscribe(
response => {
this.toastService.success(`Cliente ${client.name} asignado correctamente`);
this.toastService.success(`Cliente asignado correctamente`);
},
error => {
console.error(`Error al asignar el cliente ${client.name}:`, error);
this.toastService.error(`Error al asignar el cliente ${client.name}: ${error.error['hydra:description']}`);
console.error(`Error al asignar el cliente:`, error);
this.toastService.error(`Error al asignar el cliente: ${error.error['hydra:description']}`);
}
);
});
@ -76,27 +108,4 @@ export class AddClientsToSubnetComponent implements OnInit {
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

@ -13,9 +13,9 @@ form{
.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 */
align-items: flex-start;
justify-content: space-between;
width: 100%;
}
.text-content {
@ -33,4 +33,3 @@ form{
margin-left: 8px;
cursor: pointer;
}

View File

@ -16,21 +16,29 @@
<mat-label>Dirección IP</mat-label>
<input matInput [(ngModel)]="ipAddress" placeholder="Dirección IP" required>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Next Server</mat-label>
<input matInput [(ngModel)]="nextServer" placeholder="Next Server" required>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Boot File Name</mat-label>
<input matInput [(ngModel)]="bootFileName" placeholder="Boot File Name" required>
</mat-form-field>
<!-- Parámetros Avanzados -->
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>Parámetros avanzados</mat-panel-title>
</mat-expansion-panel-header>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Next Server</mat-label>
<input matInput [(ngModel)]="nextServer" placeholder="Next Server">
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Boot File Name</mat-label>
<input matInput [(ngModel)]="bootFileName" placeholder="Boot File Name">
</mat-form-field>
</mat-expansion-panel>
</div>
</mat-tab>
<mat-tab *ngIf="isEditMode" label="Clientes">
<mat-list>
<ng-container *ngFor="let client of clients">
<mat-list-item >
<mat-list-item>
<div class="list-item-content">
<mat-icon matListItemIcon>computer</mat-icon>
<div class="text-content">

View File

@ -58,8 +58,8 @@ export class CreateSubnetComponent implements OnInit {
name: this.name,
netmask: this.netmask,
ipAddress: this.ipAddress,
nextServer: this.nextServer,
bootFileName: this.bootFileName
nextServer: this.nextServer || null,
bootFileName: this.bootFileName || null
};
if (!this.data){

View File

@ -1,57 +1,62 @@
<mat-accordion class="example-headers-align">
<mat-expansion-panel hideToggle>
<mat-expansion-panel-header>
<mat-expansion-panel-header joyrideStep="serverInfoStep" text="Despliega este contenedor para acceder a la información y opciones de sincronización en el servidor OgDHCP.">
<mat-panel-title> Información en servidor ogDHCP </mat-panel-title>
</mat-expansion-panel-header>
<div class="example-button-row">
<button mat-flat-button color="primary" (click)="syncSubnets()"> Sincronizar base de datos</button>
<button mat-flat-button color="primary" (click)="syncSubnets()" joyrideStep="syncDbStep" text="Sincroniza la base de datos del servidor OgDHCP para actualizar la información de las subredes."> Sincronizar base de datos</button>
</div>
<div class="example-button-row">
<button mat-flat-button color="accent" (click)="openSubnetInfoDialog()">Ver Información</button>
<button mat-flat-button color="accent" (click)="openSubnetInfoDialog()" joyrideStep="viewInfoStep" text="Haz clic para ver información detallada de las subredes en el servidor.">Ver Información</button>
</div>
</mat-expansion-panel>
</mat-accordion>
<div class="header-container">
<h2 class="title" i18n="@@subnetsTitle">Administrar Subredes</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@subnetsTitle" joyrideStep="titleStep" text="Desde aquí puedes gestionar las subredes configuradas en el servidor OgDHCP.">Administrar Subredes</h2>
<div class="subnets-button-row">
<button mat-flat-button color="primary" (click)="addSubnet()">Añadir Subred</button>
<button mat-flat-button color="primary" (click)="addSubnet()" joyrideStep="addSubnetStep" text="Haz clic para añadir una nueva subred.">Añadir Subred</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep" text="Busca subredes por nombre para localizar una subred específica rápidamente.">
<mat-label i18n="@@searchLabel">Buscar nombre de la subred</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder">
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-string">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNetmaskStep" text="Busca subredes usando una netmask específica.">
<mat-label i18n="@@searchLabel">Buscar netmask</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['netmask']" i18n-placeholder="@@searchPlaceholder">
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['netmask']" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-string">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchIpStep" text="Busca subredes por dirección IP.">
<mat-label i18n="@@searchLabel">Buscar IP</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" i18n-placeholder="@@searchPlaceholder">
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-string">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchBootFileStep" text="Busca subredes según el nombre del archivo de arranque.">
<mat-label i18n="@@searchLabel">Buscar Boot file name</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['bootFileName']" i18n-placeholder="@@searchPlaceholder">
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['bootFileName']" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
</div>
<mat-divider class="divider"></mat-divider>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="Visualiza y administra las subredes listadas según los filtros aplicados.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let subnet">
<ng-container *ngIf="column.columnDef === 'synchronized'">
<mat-icon [color]="subnet[column.columnDef] ? 'primary' : 'warn'">
{{ subnet[column.columnDef] ? 'check_circle' : 'cancel' }}
@ -75,9 +80,10 @@
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let subnet" style="text-align: center;">
<td mat-cell *matCellDef="let subnet" style="text-align: center;" joyrideStep="actionsStep" text="Gestiona cada subred con opciones para editar, eliminar y más.">
<button mat-icon-button color="primary" (click)="editSubnet(subnet)" i18n="@@editSubnet">
<mat-icon>edit</mat-icon></button>
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="toggleAction(subnet, 'delete')"><mat-icon>delete</mat-icon></button>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
@ -93,8 +99,8 @@
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div class="paginator-container">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
<div class="paginator-container" joyrideStep="paginationStep" text="Navega entre las páginas de subredes usando el paginador.">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions" (page)="onPageChange($event)">
</mat-paginator>
</div>

View File

@ -7,8 +7,9 @@ import { HttpClient } from '@angular/common/http';
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
import { ToastrService } from 'ngx-toastr';
import { AddClientsToSubnetComponent } from './add-clients-to-subnet/add-clients-to-subnet.component';
import {ServerInfoDialogComponent} from "./server-info-dialog/server-info-dialog.component";
import {Observable} from "rxjs";
import { ServerInfoDialogComponent } from "./server-info-dialog/server-info-dialog.component";
import { Observable } from "rxjs";
import { JoyrideService } from 'ngx-joyride';
export interface Subnet {
'@id': string;
@ -32,7 +33,7 @@ export interface Subnet {
templateUrl: './og-dhcp-subnets.component.html',
styleUrls: ['./og-dhcp-subnets.component.css']
})
export class OgDhcpSubnetsComponent {
export class OgDhcpSubnetsComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
displayedColumns: string[] = ['id', 'name', 'netmask', 'ipAddress', 'nextServer', 'bootFileName', 'synchronized', 'serverId', 'clients', 'actions'];
dataSource = new MatTableDataSource<Subnet>([]);
@ -52,14 +53,15 @@ export class OgDhcpSubnetsComponent {
{ columnDef: 'ipAddress', header: 'IP Address', cell: (subnet: Subnet) => subnet.ipAddress },
{ columnDef: 'nextServer', header: 'Next Server', cell: (subnet: Subnet) => subnet.nextServer },
{ columnDef: 'bootFileName', header: 'Boot File Name', cell: (subnet: Subnet) => subnet.bootFileName },
{ columnDef: 'synchronized', header: 'Sincronizado', cell: (subnet: Subnet) => `${subnet.synchronized}`},
{ columnDef: 'synchronized', header: 'Sincronizado', cell: (subnet: Subnet) => `${subnet.synchronized}` },
{ columnDef: 'serverId', header: 'Id Servidor DHCP', cell: (subnet: Subnet) => subnet.serverId },
{ columnDef: 'clients', header: 'Lista de clientes', cell: (subnet: Subnet) => `${subnet.clients}`},
{ columnDef: 'clients', header: 'Lista de clientes', cell: (subnet: Subnet) => `${subnet.clients}` },
];
private apiUrl = `${this.baseUrl}/subnets`;
constructor(public dialog: MatDialog, private http: HttpClient, private toastService: ToastrService) {}
constructor(public dialog: MatDialog, private http: HttpClient, private toastService: ToastrService,
private joyrideService: JoyrideService) { }
ngOnInit() {
this.loadSubnets();
@ -93,7 +95,7 @@ export class OgDhcpSubnetsComponent {
});
}
toggleAction(subnet: any, action:string): void {
toggleAction(subnet: any, action: string): void {
switch (action) {
case 'get':
this.http.post(`${this.baseUrl}/og-dhcp/server/${subnet.uuid}/get`, {}).subscribe({
@ -149,7 +151,8 @@ export class OgDhcpSubnetsComponent {
this.toastService.error(error.error['hydra:description']);
}
});
}})
}
})
break;
default:
console.error('Acción no soportada:', action);
@ -216,4 +219,26 @@ export class OgDhcpSubnetsComponent {
this.itemsPerPage = event.pageSize;
this.loadSubnets();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'serverInfoStep',
'syncDbStep',
'viewInfoStep',
'titleStep',
'addSubnetStep',
'searchNameStep',
'searchNetmaskStep',
'searchIpStep',
'searchBootFileStep',
'tableStep',
'actionsStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -78,11 +78,17 @@ th {
.button-container {
display: flex;
flex-direction: column;
gap: 10px; /* Espacio entre botones */
gap: 10px;
margin-top: 50px;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 100px;
padding: 10px;
}
.btn:first-child {
margin-left: 0;

View File

@ -1,8 +1,13 @@
<div class="dashboard">
<h2>OgDhcp server Status</h2>
<div class="header-container" >
<h2 joyrideStep="titleStep" text="Esta sección muestra el estado general del servidor OgDhcp.">OgDhcp server Status</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
</div>
<div class="disk-usage-container">
<div class="disk-usage">
<div class="disk-usage" joyrideStep="diskUsageStep" text="Visualiza el uso del disco del servidor.">
<h3>Uso de disco</h3>
<ngx-charts-pie-chart
[view]="view"
@ -21,40 +26,40 @@
</div>
</div>
<div class="services-status">
<div class="services-status" joyrideStep="servicesStatusStep" text="Aquí puedes ver el estado de los servicios importantes del servidor.">
<h3>Servicios</h3>
<ul>
<li *ngFor="let service of getServices()">
<span
class="status-led"
[ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }"
></span>
<span
class="status-led"
[ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }"
></span>
{{ service.name }}: {{ service.status }}
</li>
</ul>
</div>
</div>
<div class="installed-oglives">
<div class="installed-oglives" joyrideStep="subnetsStep" text="Consulta la información de las subredes configuradas en el servidor.">
<h3>Subredes</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>Boot file name</th>
<th>Next server</th>
<th>Ip</th>
<th>Ordenadores</th>
</tr>
<tr>
<th>ID</th>
<th>Boot file name</th>
<th>Next server</th>
<th>Ip</th>
<th>Ordenadores</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let subnet of subnets">
<td>{{ subnet.id }}</td>
<td>{{ subnet['boot-file-name'] }}</td>
<td>{{ subnet['next-server'] }}</td>
<td>{{ subnet.subnet }}</td>
<td>{{ subnet.reservations.length }}</td>
</tr>
<tr *ngFor="let subnet of subnets">
<td>{{ subnet.id }}</td>
<td>{{ subnet['boot-file-name'] }}</td>
<td>{{ subnet['next-server'] }}</td>
<td>{{ subnet.subnet }}</td>
<td>{{ subnet.reservations.length }}</td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import {HttpClient} from "@angular/common/http";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-status',
@ -23,7 +24,8 @@ export class StatusComponent {
domain: ['#FF6384', '#3f51b5']
};
constructor(private http: HttpClient) {}
constructor(private http: HttpClient,
private joyrideService: JoyrideService) {}
ngOnInit(): void {
this.loadStatus();
@ -55,4 +57,18 @@ export class StatusComponent {
status: this.servicesStatus[key]
}));
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'diskUsageStep',
'servicesStatusStep',
'subnetsStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -1,22 +1,30 @@
<div class="header-container">
<h2 class="title" i18n="@@adminImagesTitle">Administrar sistemas operativos</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminImagesTitle" joyrideStep="osTitleStep" text="En esta pantalla, puedes gestionar los sistemas operativos disponibles.">
Administrar sistemas operativos
</h2>
<div class="calendar-button-row">
<button mat-flat-button color="primary" (click)="addSoftware()">Añadir sistema operativo</button>
<button mat-flat-button color="primary" (click)="addSoftware()" joyrideStep="addOsButton" text="Añade un nuevo sistema operativo a la lista.">Añadir sistema operativo</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchField" text="Busca un sistema operativo por nombre. Pulsa 'enter' para iniciar la búsqueda.">
<mat-label i18n="@@searchLabel">Buscar nombre de sistema operativo</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="table" text="Esta tabla muestra los sistemas operativos existentes.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image" >
<td mat-cell *matCellDef="let image">
<ng-container>
{{ column.cell(image) }}
</ng-container>
@ -24,17 +32,22 @@
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;" joyrideStep="actionsHeader" text="Acciones disponibles para cada sistema operativo.">Acciones</th>
<td mat-cell *matCellDef="let calendar" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editSoftware(calendar)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="deleteSoftware(calendar)" i18n="@@buttonDelete"><mat-icon>delete</mat-icon></button>
<button mat-icon-button color="primary" (click)="editSoftware(calendar)" i18n="@@editImage" joyrideStep="editButton" text="Editar el sistema operativo seleccionado.">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteSoftware(calendar)" i18n="@@buttonDelete" joyrideStep="deleteButton" text="Eliminar el sistema operativo seleccionado.">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div class="paginator-container">
<div class="paginator-container" joyrideStep="pagination" text="Navega entre las páginas de sistemas operativos.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"

View File

@ -9,6 +9,7 @@ import {CreateSoftwareComponent} from "../software/create-software/create-softwa
import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delete-modal.component";
import {PageEvent} from "@angular/material/paginator";
import {CreateOperativeSystemComponent} from "./create-operative-system/create-operative-system.component";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-operative-system',
@ -53,7 +54,8 @@ export class OperativeSystemComponent {
public dialog: MatDialog,
private http: HttpClient,
private dataService: DataService,
private toastService: ToastrService
private toastService: ToastrService,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
@ -124,4 +126,13 @@ export class OperativeSystemComponent {
this.length = event.length;
this.search();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: ['osTitleStep', 'addOsButton', 'searchField', 'table', 'actionsHeader', 'editButton', 'deleteButton', 'pagination'],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -0,0 +1,43 @@
.dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.repository-form {
width: 100%;
display: flex;
flex-direction: column;
}
.form-field {
width: 100%;
margin-bottom: 16px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
margin-top: 24px;
}
button {
margin-left: 8px;
}
@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,25 @@
<h2 mat-dialog-title> {{ repositoryId ? 'Editar' : 'Añadir' }} repositorio</h2>
<mat-dialog-content class="dialog-content">
<form [formGroup]="imageForm" (ngSubmit)="save()" class="repository-form">
<mat-form-field appearance="fill" class="form-field">
<mat-label>Nombre del repositorio</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field appearance="fill" class="form-field">
<mat-label>Ip</mat-label>
<input matInput formControlName="ip" name="description">
</mat-form-field>
<mat-form-field appearance="fill" class="form-field">
<mat-label>Comentarios</mat-label>
<input matInput formControlName="comments" name="comments">
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end" class="dialog-actions">
<button mat-button (click)="close()">Cancelar</button>
<button mat-button color="primary" (click)="save()">Guardar</button>
</mat-dialog-actions>

View File

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

View File

@ -0,0 +1,91 @@
import {Component, Inject} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ToastrService} from "ngx-toastr";
import {DataService} from "../../images/data.service";
@Component({
selector: 'app-create-repository',
templateUrl: './create-repository.component.html',
styleUrl: './create-repository.component.css'
})
export class CreateRepositoryComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
imageForm: FormGroup<any>;
repositoryId: string | null = null;
softwareProfiles: any[] = [];
constructor(
private fb: FormBuilder,
private http: HttpClient,
public dialogRef: MatDialogRef<CreateRepositoryComponent>,
private toastService: ToastrService,
private dataService: DataService,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.imageForm = this.fb.group({
name: ['', Validators.required],
ip: [''],
comments: [''],
});
}
ngOnInit() {
if (this.data) {
this.load()
}
}
load(): void {
this.dataService.getImage(this.data).subscribe({
next: (response) => {
this.imageForm = this.fb.group({
name: [response.name, Validators.required],
ip: [response.ip],
comments: [response.comments],
});
this.repositoryId = response['@id'];
},
error: (err) => {
console.error('Error fetching remote calendar:', err);
}
});
}
save(): void {
const payload = {
name: this.imageForm.value.name,
ip: this.imageForm.value.ip,
comments: this.imageForm.value.comments,
};
if (this.repositoryId) {
this.http.put(`${this.baseUrl}${this.repositoryId}`, payload).subscribe(
(response) => {
this.toastService.success('Imagen editada correctamente');
this.dialogRef.close();
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
console.error('Error al editar la imagen', error);
}
);
} else {
this.http.post(`${this.baseUrl}/image-repositories`, payload).subscribe(
(response) => {
this.toastService.success('Imagen añadida correctamente');
this.dialogRef.close();
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
console.error('Error al añadir la imagen', error);
}
);
}
}
close(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,102 @@
.title {
font-size: 24px;
}
.images-button-row {
display: flex;
justify-content: flex-start;
margin-top: 16px;
}
.divider {
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;
}
.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-boolean {
flex: 1;
padding: 5px;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
}
.example-headers-align .mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}
.example-headers-align .mat-mdc-form-field + .mat-mdc-form-field {
margin-left: 8px;
}
.example-button-row {
display: table-cell;
max-width: 600px;
}
.example-button-row .mat-mdc-button-base {
margin: 8px 8px 8px 0;
}

View File

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

View File

@ -6,7 +6,7 @@ import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import {CreateImageComponent} from "../images/create-image/create-image.component";
import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delete-modal.component";
import {CreateRepositoryComponent} from "./create-repository/create-repository.component";
import { CreateRepositoryComponent } from './create-repository/create-repository.component';
import { JoyrideService } from 'ngx-joyride';
@Component({

View File

@ -2,7 +2,6 @@
<mat-dialog-content class="form-container">
<mat-tab-group>
<!-- Primer tab: formulario de comandos -->
<mat-tab label="Formulario">
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()" class="form-group">
<mat-form-field appearance="fill" class="full-width" >

View File

@ -1,21 +1,21 @@
import {Component, Inject} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ToastrService} from "ngx-toastr";
import {DataService as SoftwareService} from "../../software/data.service";
import {DataService} from "../data.service";
import {Observable, startWith} from "rxjs";
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ToastrService } from 'ngx-toastr';
import { DataService as SoftwareService } from '../../software/data.service';
import { DataService } from '../data.service';
import { Observable, startWith } from 'rxjs';
import { debounceTime, switchMap, map } from 'rxjs/operators';
@Component({
selector: 'app-create-software-profile',
templateUrl: './create-software-profile.component.html',
styleUrl: './create-software-profile.component.css'
styleUrls: ['./create-software-profile.component.css']
})
export class CreateSoftwareProfileComponent {
export class CreateSoftwareProfileComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
formGroup: FormGroup<any>;
formGroup: FormGroup;
private apiUrl = `${this.baseUrl}/software-profiles`;
softwareCollection: any[] = [];
organizationalUnits: any[] = [];
@ -45,11 +45,13 @@ export class CreateSoftwareProfileComponent {
ngOnInit(): void {
if (this.data) {
this.load()
this.softwareProfileId = this.data['@id'];
this.patchFormData();
}
this.loadSoftware();
this.loadOrganizationalUnits()
this.loadOperativeSystems()
this.loadOrganizationalUnits();
this.loadOperativeSystems();
this.filteredSoftware = this.softwareControl.valueChanges.pipe(
startWith(''),
@ -58,42 +60,50 @@ export class CreateSoftwareProfileComponent {
);
}
patchFormData(): void {
this.formGroup.patchValue({
description: this.data.description || '',
comments: this.data.comments || '',
organizationalUnit: this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null,
operativeSystem: this.data.operativeSystem ? this.data.operativeSystem['@id'] : null,
});
}
loadSoftware() {
this.http.get<any>( `${this.baseUrl}/software?&page=1&itemsPerPage=10`).subscribe(
this.http.get<any>(`${this.baseUrl}/software?page=1&itemsPerPage=10`).subscribe(
response => {
this.softwareCollection = response['hydra:member'];
},
error => {
console.error('Error fetching parent units:', error);
console.error('Error fetching software:', error);
}
);
}
loadOrganizationalUnits() {
this.http.get<any>( `${this.baseUrl}/organizational-units?&page=1&itemsPerPage=10000`).subscribe(
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`).subscribe(
response => {
this.organizationalUnits = response['hydra:member'];
},
error => {
console.error('Error fetching parent units:', error);
console.error('Error fetching organizational units:', error);
}
);
}
loadOperativeSystems() {
this.http.get<any>( `${this.baseUrl}/operative-systems?&page=1&itemsPerPage=10000`).subscribe(
this.http.get<any>(`${this.baseUrl}/operative-systems?page=1&itemsPerPage=10000`).subscribe(
response => {
this.operativeSystems = response['hydra:member'];
},
error => {
console.error('Error fetching parent units:', error);
console.error('Error fetching operative systems:', error);
}
);
}
private _filterSoftware(value: string): Observable<any[]> {
return this.softwareDataService.getSoftwareCollection({ 'name': value}).pipe(
return this.softwareDataService.getSoftwareCollection({ name: value }).pipe(
map(response => response || [])
);
}
@ -109,24 +119,6 @@ export class CreateSoftwareProfileComponent {
this.softwareControl.setValue('');
}
load(): void {
console.log(this.data);
this.dataService.getSoftwareProfile(this.data).subscribe({
next: (response) => {
this.formGroup = this.fb.group({
description: [response.description],
comments: [response.comments],
organizationalUnit: [response.organizationalUnit ? response.organizationalUnit['@id'] : null],
operativeSystem: [response.operativeSystem ? response.operativeSystem['@id'] : null],
});
this.softwareProfileId = response['@id'];
},
error: (err) => {
console.error('Error fetching software:', err);
}
});
}
addSoftware() {
const software = this.softwareCollection.find(s => s.id === this.selectedSoftware);
if (software && !this.selectedSoftwares.includes(software)) {
@ -145,7 +137,6 @@ export class CreateSoftwareProfileComponent {
onSubmit(): void {
if (this.formGroup.valid) {
const payload = {
description: this.formGroup.value.description,
comments: this.formGroup.value.comments,
@ -156,24 +147,24 @@ export class CreateSoftwareProfileComponent {
if (this.softwareProfileId) {
this.http.put(`${this.baseUrl}${this.softwareProfileId}`, payload).subscribe(
(response) => {
() => {
this.toastService.success('Software editado correctamente');
this.dialogRef.close();
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
console.error('Error al editar el comando', error);
this.toastService.error(error.error['hydra:description']);
console.error('Error al editar el software', error);
}
);
} else {
this.http.post(`${this.baseUrl}/software-profiles`, payload).subscribe(
(response) => {
this.http.post(`${this.apiUrl}`, payload).subscribe(
() => {
this.toastService.success('Software añadido correctamente');
this.dialogRef.close();
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
console.error('Error al añadir comando', error);
this.toastService.error(error.error['hydra:description']);
console.error('Error al añadir software', error);
}
);
}

View File

@ -1,11 +1,14 @@
<div class="header-container">
<h2 class="title" i18n="@@adminImagesTitle">Administrar perfiles software</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminImagesTitle" joyrideStep="titleStep" text="En esta pantalla podrás administrar los diferentes perfiles de software">Administrar perfiles software</h2>
<div class="calendar-button-row">
<button mat-flat-button color="primary" (click)="addSoftware()">Añadir perfil software</button>
<button mat-flat-button color="primary" (click)="addSoftware()" joyrideStep="addStep" text="Crea nuevos perfiles de software.">Añadir perfil software</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<div class="search-container" joyrideStep="filterStep" text="Utiliza los filtros para buscar entre los perfiles de software existentes." >
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar nombre de perfil</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['description']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
@ -13,7 +16,7 @@
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="Aquí se listarán los perfiles existentes y sus detalles.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image" >
@ -29,7 +32,7 @@
</td>
</ng-container>
<ng-container matColumnDef="actions">
<ng-container matColumnDef="actions" joyrideStep="actionsStep" text="Utiliza los botones dedicados para realizar diferentes acciones sobre los perfiles.">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let calendar" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editSoftware(calendar)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>

View File

@ -5,10 +5,10 @@ import {MatDialog} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http";
import {DataService} from "../software/data.service";
import {ToastrService} from "ngx-toastr";
import {CreateSoftwareComponent} from "../software/create-software/create-software.component";
import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delete-modal.component";
import {PageEvent} from "@angular/material/paginator";
import {CreateSoftwareProfileComponent} from "./create-software-profile/create-software-profile.component";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-software-profile',
@ -58,7 +58,8 @@ export class SoftwareProfileComponent {
public dialog: MatDialog,
private http: HttpClient,
private dataService: DataService,
private toastService: ToastrService
private toastService: ToastrService,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
@ -90,7 +91,7 @@ export class SoftwareProfileComponent {
editSoftware(softwareProfile: any): void {
const dialogRef = this.dialog.open(CreateSoftwareProfileComponent, {
width: '600px',
data: softwareProfile['@id']
data: softwareProfile
});
dialogRef.afterClosed().subscribe(result => {
@ -129,4 +130,12 @@ export class SoftwareProfileComponent {
this.length = event.length;
this.search();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: ['titleStep','addStep', 'filterStep', 'tableStep', 'actionsStep'],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -1,11 +1,14 @@
<div class="header-container">
<h2 class="title" i18n="@@adminImagesTitle">Administrar Software</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminImagesTitle" joyrideStep="titleStep" text="Administra el software deisponible desde este componente.">Administrar Software</h2>
<div class="calendar-button-row">
<button mat-flat-button color="primary" (click)="addSoftware()">Añadir software</button>
<button mat-flat-button color="primary" (click)="addSoftware()" joyrideStep="addSoftwareStep" text="Utiliza este botón par añadir software nuevo.">Añadir software</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<div class="search-container" joyrideStep="searchStep" text="Utiliza los filtros para buscar entre el software listado.">
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar nombre de software</mat-label>
<input matInput name="searchBar" placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
@ -21,7 +24,7 @@
</mat-select>
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyride="tableStep" text="Aquí se mostrará todo el software disponible y sus características.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image" >

View File

@ -5,10 +5,10 @@ import {MatDialog} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http";
import {DataService} from "./data.service";
import {ToastrService} from "ngx-toastr";
import {CreateCalendarComponent} from "../calendar/create-calendar/create-calendar.component";
import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delete-modal.component";
import {PageEvent} from "@angular/material/paginator";
import {CreateSoftwareComponent} from "./create-software/create-software.component";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-software',
@ -63,7 +63,8 @@ export class SoftwareComponent {
public dialog: MatDialog,
private http: HttpClient,
private dataService: DataService,
private toastService: ToastrService
private toastService: ToastrService,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
@ -134,4 +135,18 @@ export class SoftwareComponent {
this.length = event.length;
this.search();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'addSoftwareStep',
'searchStep',
'tableStep',
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -20,6 +20,6 @@ button[mat-flat-button] {
margin: 0 5px;
}
.navbar-tittle{
.navbar-title{
cursor: pointer;
}

View File

@ -1,12 +1,35 @@
<mat-toolbar>
<span class="navbar-title" routerLink="/dashboard" i18n="@@webConsoleTitle">Opengnsys webconsole</span>
<span class="navbar-title" i18n="@@webConsoleTitle" matTooltip="Consola web de administración de Opengnsys" matTooltipShowDelay="1000">
Opengnsys webconsole
</span>
<button mat-icon-button (click)="onToggleSidebar()" matTooltip="Abrir o cerrar la barra lateral" matTooltipShowDelay="1000">
<mat-icon class="navbar-icon">menu</mat-icon>
</button>
<div class="navbar-button-row">
<button class="admin-button" *ngIf="isSuperAdmin" mat-button [matMenuTriggerFor]="menu" i18n="@@admin">Administración</button>
<button class="user-button" mat-button *ngIf="!isSuperAdmin" (click)="editUser()" i18n="@@editUser">Editar usuario</button>
<button class="admin-button" *ngIf="isSuperAdmin" mat-button [matMenuTriggerFor]="menu" i18n="@@admin"
matTooltip="Gestión de usuarios y roles de la aplicación" matTooltipShowDelay="1000">
Administración
</button>
<button class="user-button" mat-button *ngIf="!isSuperAdmin" (click)="editUser()" i18n="@@editUser"
matTooltip="Editar tu información de usuario" matTooltipShowDelay="1000">
Editar usuario
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item routerLink="/users" i18n="@@usersMenuItem">Usuarios</button>
<button mat-menu-item routerLink="/user-groups" i18n="@@rolesMenuItem">Roles</button>
<button mat-menu-item routerLink="/users" i18n="@@usersMenuItem" matTooltip="Ver y gestionar todos los usuarios" matTooltipShowDelay="1000">
Usuarios
</button>
<button mat-menu-item routerLink="/user-groups" i18n="@@rolesMenuItem" matTooltip="Gestionar roles de usuario" matTooltipShowDelay="1000">
Roles
</button>
</mat-menu>
<button mat-flat-button color="warn" routerLink="/auth/login" i18n="@@logout">Salir</button>
<button mat-flat-button color="warn" routerLink="/auth/login" i18n="@@logout"
matTooltip="Cerrar sesión y salir de la aplicación" matTooltipShowDelay="1000">
Salir
</button>
</div>
</mat-toolbar>

View File

@ -1,8 +1,8 @@
<app-header class="header" (toggleSidebar)="toggleSidebar()"></app-header>
<mat-drawer-container class="container" autosize>
<mat-drawer class="sidebar" mode="side" opened>
<app-sidebar [isVisible]="isSidebarVisible"></app-sidebar>
<mat-drawer class="sidebar" mode="side" [opened]="isSidebarVisible">
<app-sidebar></app-sidebar>
</mat-drawer>
<mat-drawer-content class="content">

View File

@ -1,11 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-main-layout',
templateUrl: './main-layout.component.html',
styleUrl: './main-layout.component.css',
styleUrls: ['./main-layout.component.css'],
})
export class MainLayoutComponent {
isSidebarVisible: boolean = false;
isSidebarVisible: boolean = true;
toggleSidebar() {
this.isSidebarVisible = !this.isSidebarVisible;

View File

@ -1,57 +1,55 @@
<mat-nav-list>
<mat-list-item disabled>
<span class="user-logged">
<span class="user-logged" matTooltip="Bienvenido, {{username}}" matTooltipShowDelay="1000">
<span i18n="@@welcomeUser">Bienvenido {{username}}</span>
</span>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item routerLink="/groups">
<mat-list-item routerLink="/groups" matTooltip="Gestionar grupos de usuarios" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">apartment</mat-icon>
<span i18n="@@groups">Grupos</span>
</span>
</mat-list-item>
<mat-list-item>
<span class="entry" (click)="toggleCommandSub()">
<mat-icon class="icon">playlist_play </mat-icon>
<mat-list-item (click)="toggleCommandSub()" matTooltip="Ver y ejecutar acciones predefinidas" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">playlist_play</mat-icon>
<span i18n="@@actions">Acciones</span>
</span>
</mat-list-item>
<!-- Submenu items for commands -->
<mat-nav-list *ngIf="showCommandSub" style="padding-left: 20px;">
<mat-list-item routerLink="/commands">
<mat-list-item routerLink="/commands" matTooltip="Lista de comandos disponibles" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">chevron_right</mat-icon>
<span i18n="@@gallery">Comandos</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/commands-groups">
<mat-list-item routerLink="/commands-groups" matTooltip="Gestionar grupos de comandos" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">chevron_right</mat-icon>
<span i18n="@@gallery">Grupos</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/commands-task">
<mat-list-item routerLink="/commands-task" matTooltip="Ver y gestionar tareas programadas" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">chevron_right</mat-icon>
<span i18n="@@gallery">Tareas</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/commands-logs">
<mat-list-item routerLink="/commands-logs" matTooltip="Revisar trazas de ejecución de comandos" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">notifications</mat-icon>
<span i18n="@@gallery">Trazas</span>
</span>
</mat-list-item>
</mat-nav-list>
<!-- End commands sub -->
<!-- OGDHCP -->
<mat-list-item (click)="toggleOgDhcpSub()">
<mat-list-item (click)="toggleOgDhcpSub()" matTooltip="Configurar y administrar DHCP" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">settings_ethernet</mat-icon>
<span i18n="@@images">DHCP</span>
@ -60,23 +58,21 @@
<!-- Submenu items ogdhcp -->
<mat-nav-list *ngIf="showOgDhcpSub" style="padding-left: 20px;">
<mat-list-item routerLink="/ogdhcp-status">
<mat-list-item routerLink="/ogdhcp-status" matTooltip="Estado actual del servicio DHCP" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">analytics</mat-icon>
<span i18n="@@gallery">Estado</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/subnets">
<mat-list-item routerLink="/subnets" matTooltip="Gestionar y crea subredes" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">lan</mat-icon>
<span i18n="@@gallery">Subredes</span>
</span>
</mat-list-item>
</mat-nav-list>
<!-- Submenu items ogdhcp -->
<mat-list-item (click)="toggleOgBootSub()">
<mat-list-item (click)="toggleOgBootSub()" matTooltip="Configurar y administrar opciones de arranque" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">desktop_windows</mat-icon>
<span i18n="@@images">Boot</span>
@ -85,41 +81,40 @@
<!-- Submenu items for ogBoot -->
<mat-nav-list *ngIf="showOgBootSub" style="padding-left: 20px;">
<mat-list-item routerLink="/ogboot-status">
<mat-list-item routerLink="/ogboot-status" matTooltip="Estado del servicio de arranque" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">analytics</mat-icon>
<span i18n="@@gallery">Estado</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/pxe-images">
<mat-list-item routerLink="/pxe-images" matTooltip="Ver imágenes disponibles para arranque PXE" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">album</mat-icon>
<span i18n="@@gallery">ogLive</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/pxe">
<mat-list-item routerLink="/pxe" matTooltip="Gestionar plantillas de arranque PXE" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">assignment</mat-icon>
<span i18n="@@upload">Plantillas PXE</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/pxe-boot-file">
<mat-list-item routerLink="/pxe-boot-file" matTooltip="Configurar archivos de arranque PXE" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">save</mat-icon>
<span i18n="@@upload">Arranque PXE</span>
</span>
</mat-list-item>
</mat-nav-list>
<!-- End ogBoot sub -->
<mat-list-item routerLink="/calendars">
<mat-list-item routerLink="/calendars" matTooltip="Gestionar calendarios de remotePC" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">calendar_month</mat-icon>
<span i18n="@@calendars">Calendarios</span>
</span>
</mat-list-item>
<mat-list-item (click)="toggleSoftwareSub()">
<mat-list-item (click)="toggleSoftwareSub()" matTooltip="Administrar configuraciones de software" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">terminal</mat-icon>
<span i18n="@@images">Software</span>
@ -128,19 +123,19 @@
<!-- Submenu items ogdhcp -->
<mat-nav-list *ngIf="showSoftwareSub" style="padding-left: 20px;">
<mat-list-item routerLink="/software">
<mat-list-item routerLink="/software" matTooltip="Ver lista de software disponible" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">list</mat-icon>
<span i18n="@@gallery">Listado </span>
<span i18n="@@gallery">Listado</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/software-profiles">
<mat-list-item routerLink="/software-profiles" matTooltip="Gestionar perfiles de software" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">folder_shared</mat-icon>
<span i18n="@@gallery">Perfiles</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/operative-systems">
<mat-list-item routerLink="/operative-systems" matTooltip="Configurar sistemas operativos" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">terminal</mat-icon>
<span i18n="@@gallery">S. Operativos</span>
@ -148,33 +143,31 @@
</mat-list-item>
</mat-nav-list>
<mat-list-item routerLink="/images">
<mat-list-item routerLink="/images" matTooltip="Gestionar imágenes del sistema" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">photo</mat-icon>
<span i18n="@@repositories">imágenes</span>
<span i18n="@@images">Imágenes</span>
</span>
</mat-list-item>
<mat-list-item class="disabled">
<mat-list-item routerLink="/repositories" matTooltip="Ver y gestionar repositorios de software" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">warehouse</mat-icon>
<span i18n="@@repositories">Repositorios</span>
</span>
</mat-list-item>
<mat-list-item class="disabled">
<mat-list-item class="disabled" matTooltip="Gestión de menús (opción deshabilitada)" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">list</mat-icon>
<span i18n="@@menus">Menús</span>
</span>
</mat-list-item>
<mat-list-item class="disabled">
<mat-list-item class="disabled" matTooltip="Función de búsqueda (opción deshabilitada)" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">search</mat-icon>
<span i18n="@@search">Buscar</span>
</span>
</mat-list-item>
</mat-nav-list>

View File

@ -1,45 +1,45 @@
<?xml version="1.0"?>
<testsuite name="Chrome 123.0.0.0 (Linux x86_64)" package="" timestamp="2024-10-24T11:02:26" id="0" hostname="Ubnt" tests="31" errors="0" failures="0" time="1.13">
<testsuite name="Chrome Headless 130.0.0.0 (Linux x86_64)" package="" timestamp="2024-10-29T10:46:18" id="0" hostname="alvaro-Latitude-3420" tests="31" errors="0" failures="0" time="0.938">
<properties>
<property name="browser.fullName" value="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"/>
<property name="browser.fullName" value="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/130.0.0.0 Safari/537.36"/>
</properties>
<testcase name="CreateOperativeSystemComponent should create" time="0.106" classname="CreateOperativeSystemComponent"/>
<testcase name="PxeBootFilesComponent should create" time="0.083" classname="PxeBootFilesComponent"/>
<testcase name="OgDhcpSubnetsComponent should create" time="0.068" classname="OgDhcpSubnetsComponent"/>
<testcase name="PXEimagesComponent should create" time="0.092" classname="PXEimagesComponent"/>
<testcase name="AddClientsToPxeComponent should create" time="0.024" classname="AddClientsToPxeComponent"/>
<testcase name="RolesComponent should create" time="0.024" classname="RolesComponent"/>
<testcase name="SoftwareProfileComponent should create" time="0.05" classname="SoftwareProfileComponent"/>
<testcase name="CreateCommandComponent should create" time="0.026" classname="CreateCommandComponent"/>
<testcase name="DashboardComponent should create the component" time="0.008" classname="DashboardComponent"/>
<testcase name="CalendarComponent should create" time="0.049" classname="CalendarComponent"/>
<testcase name="UsersComponent should create" time="0.023" classname="UsersComponent"/>
<testcase name="SoftwareComponent should create" time="0.053" classname="SoftwareComponent"/>
<testcase name="OgdhcpComponent should create" time="0.006" classname="OgdhcpComponent"/>
<testcase name="StatusComponent should create" time="0.032" classname="StatusComponent"/>
<testcase name="OperativeSystemComponent should create" time="0.04" classname="OperativeSystemComponent"/>
<testcase name="LoginComponent should create" time="0.036" classname="LoginComponent"/>
<testcase name="CommandsTaskComponent should create" time="0.041" classname="CommandsTaskComponent"/>
<testcase name="CommandsComponent should create" time="0.037" classname="CommandsComponent"/>
<testcase name="CreateSoftwareProfileComponent should create" time="0.067" classname="CreateSoftwareProfileComponent"/>
<testcase name="OgbootStatusComponent should create the component" time="0.017" classname="OgbootStatusComponent"/>
<testcase name="ClientsComponent should create" time="0.032" classname="ClientsComponent"/>
<testcase name="AdminComponent el primer botón debería tener el texto &quot;Usuarios&quot;" time="0.022" classname="AdminComponent"/>
<testcase name="AdminComponent el segundo botón debería tener el routerLink correcto" time="0.017" classname="AdminComponent"/>
<testcase name="AdminComponent el primer botón debería tener el routerLink correcto" time="0.016" classname="AdminComponent"/>
<testcase name="AdminComponent debería crear el componente" time="0.008" classname="AdminComponent"/>
<testcase name="AdminComponent el segundo botón debería tener el texto &quot;Roles&quot;" time="0.011" classname="AdminComponent"/>
<testcase name="AdminComponent debería contener dos botones" time="0.009" classname="AdminComponent"/>
<testcase name="AppComponent should create the app" time="0.009" classname="AppComponent"/>
<testcase name="CreateSoftwareComponent should create" time="0.058" classname="CreateSoftwareComponent"/>
<testcase name="PxeComponent should create the component" time="0.05" classname="PxeComponent"/>
<testcase name="CreateSoftwareComponent should create" time="0.127" classname="CreateSoftwareComponent"/>
<testcase name="UsersComponent should create" time="0.039" classname="UsersComponent"/>
<testcase name="CommandsComponent should create" time="0.054" classname="CommandsComponent"/>
<testcase name="AppComponent should create the app" time="0.004" classname="AppComponent"/>
<testcase name="AddClientsToPxeComponent should create" time="0.031" classname="AddClientsToPxeComponent"/>
<testcase name="CreateCommandComponent should create" time="0.029" classname="CreateCommandComponent"/>
<testcase name="OgbootStatusComponent should create the component" time="0.03" classname="OgbootStatusComponent"/>
<testcase name="ServerInfoDialogComponent should create" time="0.016" classname="ServerInfoDialogComponent"/>
<testcase name="AdminComponent debería crear el componente" time="0.015" classname="AdminComponent"/>
<testcase name="AdminComponent el segundo botón debería tener el texto &quot;Roles&quot;" time="0.008" classname="AdminComponent"/>
<testcase name="AdminComponent el primer botón debería tener el texto &quot;Usuarios&quot;" time="0.006" classname="AdminComponent"/>
<testcase name="AdminComponent debería contener dos botones" time="0.005" classname="AdminComponent"/>
<testcase name="AdminComponent el segundo botón debería tener el routerLink correcto" time="0.007" classname="AdminComponent"/>
<testcase name="AdminComponent el primer botón debería tener el routerLink correcto" time="0.005" classname="AdminComponent"/>
<testcase name="DashboardComponent should create the component" time="0.004" classname="DashboardComponent"/>
<testcase name="PXEimagesComponent should create" time="0.071" classname="PXEimagesComponent"/>
<testcase name="CalendarComponent should create" time="0.033" classname="CalendarComponent"/>
<testcase name="OgDhcpSubnetsComponent should create" time="0.054" classname="OgDhcpSubnetsComponent"/>
<testcase name="OgdhcpComponent should create" time="0.005" classname="OgdhcpComponent"/>
<testcase name="StatusComponent should create" time="0.017" classname="StatusComponent"/>
<testcase name="CreateOperativeSystemComponent should create" time="0.015" classname="CreateOperativeSystemComponent"/>
<testcase name="LoginComponent should create" time="0.027" classname="LoginComponent"/>
<testcase name="SoftwareComponent should create" time="0.04" classname="SoftwareComponent"/>
<testcase name="OperativeSystemComponent should create" time="0.032" classname="OperativeSystemComponent"/>
<testcase name="CreateSoftwareProfileComponent should create" time="0.105" classname="CreateSoftwareProfileComponent"/>
<testcase name="SoftwareProfileComponent should create" time="0.031" classname="SoftwareProfileComponent"/>
<testcase name="ClientsComponent should create" time="0.014" classname="ClientsComponent"/>
<testcase name="PxeComponent should create the component" time="0.034" classname="PxeComponent"/>
<testcase name="PxeBootFilesComponent should create" time="0.036" classname="PxeBootFilesComponent"/>
<testcase name="RolesComponent should create" time="0.015" classname="RolesComponent"/>
<testcase name="CommandsTaskComponent should create" time="0.029" classname="CommandsTaskComponent"/>
<system-out>
<![CDATA[Chrome 123.0.0.0 (Linux x86_64) ERROR: 'Error fetching images', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
,Chrome 123.0.0.0 (Linux x86_64) ERROR: 'Error fetching og lives', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
,Chrome 123.0.0.0 (Linux x86_64) LOG: 'Selected subnet UUID:', Object{}
,Chrome 123.0.0.0 (Linux x86_64) LOG: Object{}
,Chrome 123.0.0.0 (Linux x86_64) LOG: Object{}
<![CDATA[Chrome Headless 130.0.0.0 (Linux x86_64) LOG: 'Selected subnet UUID:', Object{}
,Chrome Headless 130.0.0.0 (Linux x86_64) ERROR: 'Error fetching images', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
,Chrome Headless 130.0.0.0 (Linux x86_64) ERROR: 'Error fetching og lives', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
,Chrome Headless 130.0.0.0 (Linux x86_64) LOG: Object{}
,Chrome Headless 130.0.0.0 (Linux x86_64) LOG: Object{}
]]>
</system-out>