solve conflicts
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details

oggui/translations
Manuel Aranda Rosales 2024-11-15 13:16:29 +01:00
commit cd09659e6c
61 changed files with 1456 additions and 671 deletions

View File

@ -20,6 +20,7 @@
"@angular/router": "^18.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",
@ -11228,6 +11229,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

@ -22,6 +22,7 @@
"@angular/router": "^18.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

@ -120,6 +120,8 @@ import { CreateRepositoryComponent } from './components/repositories/create-repo
import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component';
import { DeployImageComponent } from './components/groups/components/client-main-view/deploy-image/deploy-image.component';
import { MainRepositoryViewComponent } from './components/repositories/main-repository-view/main-repository-view.component';
import { ExecuteCommandOuComponent } from './components/groups/shared/execute-command-ou/execute-command-ou.component';
import { JoyrideModule } from 'ngx-joyride';
@NgModule({
declarations: [
AppComponent,
@ -197,6 +199,7 @@ import { MainRepositoryViewComponent } from './components/repositories/main-repo
ExecuteCommandComponent,
DeployImageComponent,
MainRepositoryViewComponent,
ExecuteCommandOuComponent,
],
bootstrap: [AppComponent],
imports: [BrowserModule,
@ -225,6 +228,7 @@ import { MainRepositoryViewComponent } from './components/repositories/main-repo
MatDatepickerModule,
MatNativeDateModule,
MatSliderModule,
JoyrideModule.forRoot(),
ToastrModule.forRoot(
{
timeOut: 5000,

View File

@ -1,12 +1,17 @@
<div class="header-container">
<h2 class="title" i18n="@@adminImagesTitle">Administrar calendarios</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 joyrideStep="titleStep" text="En esta pantalla, puedes gestionar los calendarios de los equipos remotos conectados con el servicio UDS" class="title" i18n="@@adminImagesTitle">Administrar calendarios</h2>
<div class="calendar-button-row">
<button mat-flat-button color="primary" (click)="addImage()">Añadir calendario</button>
<button joyrideStep="addButtonStep" text="Haz clic aquí para añadir un nuevo calendario." mat-flat-button color="primary" (click)="addImage()">Añadir calendario</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-form-field joyrideStep="searchStep" text="Utiliza esta barra de búsqueda para filtrar los calendarios existentes." appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar nombre de calendario</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
@ -19,7 +24,7 @@
</div>
<div *ngIf="!loading">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="Aquí se muestran los calendarios existentes con sus características y configuraciones.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image" >
@ -35,7 +40,7 @@
</td>
</ng-container>
<ng-container matColumnDef="actions">
<ng-container matColumnDef="actions" joyrideStep="actionsStep" text="Accede a las acciones disponibles para cada calendario aquí.">
<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)="editCalendar(calendar)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>

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

@ -1,11 +1,16 @@
<div class="header-container">
<h2 class="title" i18n="@@adminCommandGroupsTitle">Administrar Grupos de Comandos</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminCommandGroupsTitle" joyrideStep="titleStep" text="Desde aquí puedes gestionar los grupos de comandos.">Administrar Grupos de Comandos</h2>
<div class="command-groups-button-row">
<button mat-flat-button color="primary" (click)="openCreateCommandGroupModal()">Añadir Grupo de Comandos</button>
<button mat-flat-button color="primary" (click)="openCreateCommandGroupModal()" joyrideStep="addCommandGroupStep" text="Haz clic para añadir un nuevo grupo de comandos.">Añadir Grupo de Comandos</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<div class="search-container" joyrideStep="searchStep" text="Busca un grupo de comandos específico ingresando su nombre y pulsando 'Enter'.">
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar nombre de grupo</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
@ -14,12 +19,12 @@
</mat-form-field>
</div>
<div *ngIf="loading" class="loading-container">
<div *ngIf="loading" class="loading-container" joyrideStep="loadingStep" text="Espera mientras se cargan los grupos de comandos.">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="!loading">
<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 grupos de comandos disponibles.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let commandGroup">
@ -27,7 +32,7 @@
{{ column.cell(commandGroup) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'commands'">
<ng-container *ngIf="column.columnDef === 'commands'" joyrideStep="viewCommandsStep" text="Haz clic para ver los comandos en este grupo.">
<button mat-button [matMenuTriggerFor]="menu">Ver comandos</button>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let command of commandGroup.commands">
@ -37,11 +42,12 @@
</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;">
<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>
<button mat-icon-button color="primary" (click)="editCommandGroup(client)" i18n="@@editImage"><mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="deleteCommandGroup(client)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
@ -53,7 +59,7 @@
</table>
</div>
<div class="paginator-container">
<div class="paginator-container" joyrideStep="paginationStep" text="Navega entre las páginas de grupos de comandos usando el paginador.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"

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,13 +1,16 @@
<div class="header-container">
<h2 class="title">Administrar Tareas</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" joyrideStep="titleStep" text="Desde aquí puedes gestionar las tareas de manera centralizada.">Administrar Tareas</h2>
<div class="task-button-row">
<button mat-flat-button color="primary" (click)="openCreateTaskModal()">Añadir Tarea</button>
<button mat-flat-button color="primary" (click)="openCreateTaskModal()" joyrideStep="addTaskStep" text="Haz clic para añadir una nueva tarea.">Añadir Tarea</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<div class="search-container" joyrideStep="searchStep" text="Busca una tarea específica ingresando su nombre y pulsando 'Enter'.">
<mat-form-field appearance="fill" class="search-string">
<mat-label>Buscar tarea</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
@ -16,12 +19,8 @@
</mat-form-field>
</div>
<div *ngIf="loading" class="loading-container">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="!loading">
<table mat-table [dataSource]="tasks" class="mat-elevation-z8">
<table mat-table [dataSource]="tasks" class="mat-elevation-z8" joyrideStep="tableStep" text="Aquí se muestra la lista de tareas disponibles.">
<ng-container matColumnDef="taskid">
<th mat-header-cell *matHeaderCellDef> Id</th>
<td mat-cell *matCellDef="let task"> {{ task.id }} </td>
@ -49,9 +48,9 @@
<ng-container matColumnDef="actions">
<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>
<button mat-icon-button color="primary" (click)="editTask(task)" i18n="@@editImage"><mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="deleteTask(task)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
@ -63,7 +62,8 @@
</table>
</div>
<mat-paginator [length]="length"
<mat-paginator joyrideStep="paginationStep" text="Navega entre las páginas de tareas usando el paginador."
[length]="length"
[pageSize]="itemsPerPage"
[pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">

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

@ -1,13 +1,18 @@
<div class="header-container">
<h2 class="title" i18n="@@adminCommandsTitle">Trazas</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminCommandsTitle" joyrideStep="titleStep" text="Desde aquí puedes ver las trazas y logs de las ejecuciones de comandos y procedimientos.">Trazas de comandos y procedimientos</h2>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="resetFilters()">Reiniciar filtros</button>
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="resetFiltersStep" text="Haz clic para reiniciar los filtros aplicados y ver todas las trazas.">Reiniciar filtros</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<mat-form-field appearance="fill" class="search-select">
<mat-form-field appearance="fill" class="search-select" joyrideStep="clientSelectStep" text="Selecciona un cliente para ver las trazas asociadas.">
<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">
@ -16,7 +21,7 @@
</mat-autocomplete>
</mat-form-field>
<mat-form-field appearance="fill" class="search-select">
<mat-form-field appearance="fill" class="search-select" joyrideStep="commandSelectStep" text="Selecciona un comando para ver las trazas específicas de ese comando.">
<input type="text" matInput [formControl]="commandControl" [matAutocomplete]="commandAuto" placeholder="Seleccione un comando">
<mat-autocomplete #commandAuto="matAutocomplete" [displayWith]="displayFnCommand" (optionSelected)="onOptionCommandSelected($event.option.value)">
<mat-option *ngFor="let command of filteredCommands | async" [value]="command">
@ -41,7 +46,7 @@
</div>
<div *ngIf="!loading">
<table mat-table [dataSource]="traces" class="mat-elevation-z8">
<table mat-table [dataSource]="traces" class="mat-elevation-z8" joyrideStep="tableStep" text="Aquí se muestra la lista de trazas de comandos y procedimientos, 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 trace">
@ -75,8 +80,7 @@
</table>
</div>
<div class="paginator-container">
<div class="paginator-container" joyrideStep="paginationStep" text="Navega entre las páginas de trazas usando el paginador.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"

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',
@ -68,7 +69,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();
@ -188,4 +190,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

@ -1,12 +1,16 @@
<div class="header-container">
<h2 class="title" i18n="@@adminCommandsTitle">Administrar Comandos</h2>
<div class="command-button-row">
<button mat-flat-button color="primary" (click)="openCreateCommandModal()">Añadir Comando</button>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminCommandsTitle" joyrideStep="titleStep" text="Desde aquí puedes gestionar los comandos disponibles.">Administrar Comandos</h2>
<div class="command-button-row" joyrideStep="addCommandStep" text="Haz clic para añadir un nuevo comando.">
<button mat-flat-button color="primary" (click)="openCreateCommandModal()" >Añadir Comando</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<div class="search-container" joyrideStep="searchStep" text="Busca un comando específico ingresando su nombre y pulsando 'Enter'.">
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar nombre de comando</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
@ -15,12 +19,12 @@
</mat-form-field>
</div>
<div *ngIf="loading" class="loading-container">
<div *ngIf="loading" class="loading-container" joyrideStep="loadingStep" text="Espera mientras se cargan los comandos.">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="!loading">
<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 comandos disponibles.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let command">
@ -36,10 +40,10 @@
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let command" style="text-align: center;">
<td mat-cell *matCellDef="let command" style="text-align: center;" joyrideStep="actionsStep" text="Usa estos botones para ejecutar, ver, editar o eliminar un comando.">
<button mat-icon-button color="info" (click)="executeCommand($event, command)"><mat-icon>play_arrow</mat-icon></button>
<button mat-icon-button color="info" (click)="viewDetails($event, command)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" [disabled]="command.readOnly" (click)="editCommand($event, command)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="primary" [disabled]="command.readOnly" (click)="editCommand($event, command)" i18n="@@editImage"><mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" [disabled]="command.readOnly" (click)="deleteCommand($event, command)"><mat-icon i18n="@@deleteElementTooltip">delete</mat-icon></button>
</td>
</ng-container>
@ -49,7 +53,7 @@
</table>
</div>
<div class="paginator-container">
<div class="paginator-container" joyrideStep="paginationStep" text="Navega entre las páginas de comandos usando el paginador.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"

View File

@ -8,6 +8,7 @@ import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/
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',
@ -49,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();
@ -131,4 +133,19 @@ export class CommandsComponent implements OnInit {
this.length = event.length;
this.search();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'addCommandStep',
'searchStep',
'tableStep',
'actionsStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -3,7 +3,6 @@
<mat-dialog-content class="form-container">
<form [formGroup]="form" class="command-form">
<!-- Selector de Unidad Organizativa -->
<mat-form-field appearance="fill" class="full-width">
<mat-label>Unidad Organizativa</mat-label>
<mat-select formControlName="unit">
@ -11,7 +10,6 @@
</mat-select>
</mat-form-field>
<!-- Selector de Subunidad Organizativa -->
<mat-form-field appearance="fill" class="full-width">
<mat-label>Subunidad Organizativa</mat-label>
<mat-select formControlName="childUnit">
@ -19,7 +17,6 @@
</mat-select>
</mat-form-field>
<!-- Selector de Cliente (PC) con checkboxes -->
<div class="checkbox-group">
<label>Clientes (PC)</label>
<div *ngIf="clients.length > 0">

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 {
@ -183,44 +187,49 @@ mat-spinner {
margin: 10px 10px;
}
.result-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.result-checkbox {
float: right;
margin: 0;
.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: 1.2rem;
font-weight: 600;
font-size: 1rem;
font-weight: 500;
color: #333;
margin-bottom: 5px;
}
.result-content {
padding-top: 10px;
padding-top: 5px;
color: #555;
font-size: 0.85rem;
}
.result-type {
font-size: 1rem;
font-weight: 500;
margin: 0;
.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-ip, .result-mac, .result-status {
font-size: 0.9rem;
margin: 5px 0;
.result-checkbox {
align-self: flex-start;
margin: 0 0 5px 0;
}
.result-internal-units,
.result-clients {
font-size: 0.9rem;
color: #007bff;
color: #007bff;
margin: 5px 0;
.result-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
}
.paginator-container {
@ -242,7 +251,6 @@ mat-card {
}
.red-card {
background-color: #f35f53;
background-color: #f35f53;
color: white;
}
@ -250,8 +258,6 @@ mat-card {
.green-card {
background-color: #4caf50;
color: white;
background-color: #4caf50;
color: white;
}
.view-mode-buttons button.active {
@ -273,7 +279,6 @@ mat-card {
}
.result-card-list .result-title {
font-size: 14px;
font-size: 14px;
font-weight: bold;
margin-right: 8px;
@ -284,12 +289,10 @@ mat-card {
flex-direction: row;
gap: 8px;
font-size: 12px;
font-size: 12px;
}
.result-card-list p {
margin: 0;
margin: 0;
}
.result-list {

View File

@ -1,7 +1,12 @@
<h2 class="title" i18n="@@searchTitle">Búsqueda avanzada</h2>
<div class="header-container">
<h2 class="title" i18n="@@searchTitle" joyrideStep="title2Step" text="Aquí puedes realizar una búsqueda avanzada.">Búsqueda avanzada</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
</div>
<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 i18n="@@selectFilterLabel">Seleccione filtro</mat-label>
<mat-select (selectionChange)="loadSelectedFilter($event.value)">
@ -14,7 +19,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> Cuadrícula
</button>
@ -26,7 +31,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 i18n="@@selectOptionLabel">Selecciona una opción</mat-label>
<mat-select [(value)]="selectedFilter1" (selectionChange)="applyFilter()">
@ -41,9 +46,9 @@
</mat-form-field>
<div class="button-group">
<button mat-raised-button color="primary" (click)="saveFilters()" i18n="@@saveFiltersButton">Guardar Filtros</button>
<button mat-raised-button color="accent" [disabled]="selectedElements.length === 0 || selectedFilter1 === 'ou'" (click)="sendActions()" i18n="@@sendFiltersButton" >Enviar Acción</button>
<button mat-flat-button color="primary" [disabled]="selectedElements.length === 0 || selectedFilter1 === 'ou'" (click)="onPxeBootFile()">Añadir fichero PXE</button>
<button mat-raised-button color="primary" (click)="saveFilters()" i18n="@@saveFiltersButton" joyrideStep="saveFiltersStep" text="Guarda tus filtros seleccionados para usarlos en el futuro.">Guardar Filtros</button>
<button mat-raised-button color="accent" (click)="sendActions()" i18n="@@sendFiltersButton" [disabled]="selectedElements.length === 0" joyrideStep="sendActionStep" text="Envía una acción a los elementos seleccionados.">Enviar Acción</button>
<button mat-flat-button color="primary" [disabled]="selectedElements.length === 0" (click)="onPxeBootFile()" joyrideStep="addPxeStep" text="Añade un archivo PXE a los elementos seleccionados.">Añadir fichero PXE</button>
<button mat-raised-button color="primary" [matMenuTriggerFor]="menu" [disabled]="selectedFilter1 === 'ou'">
Asistentes
</button>
@ -55,12 +60,12 @@
</div>
</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="5" 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">
@ -102,7 +107,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 {
@ -472,4 +474,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

@ -1,49 +1,57 @@
<div class="header-container">
<h2 class="title" i18n="@@adminImagesTitle">Administrar clientes</h2>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="resetFilters()">Reiniciar filtros</button>
<button mat-flat-button color="primary" (click)="addClient($event)">Añadir cliente</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar nombre de cliente</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>
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar IP</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" (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>
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar MAC</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['mac']" (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>
<mat-form-field appearance="fill" class="search-select">
<mat-label i18n="@@organizational-unit-label">U. Organizativa</mat-label>
<mat-select [(ngModel)]="filters['organizationalUnit.id']" (selectionChange)="search()">
<mat-option *ngFor="let unit of organizationalUnits" [value]="unit.id" >
{{ unit.name }}
</mat-option>
</mat-select>
</mat-form-field>
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminImagesTitle" joyrideStep="title3Step" text="En esta pantalla se podran administrar todos los clientes del sistema sin jerarquias.">Administrar clientes</h2>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="resetFiltersStep" text="Reinicia los filtros aplicados para mostrar todos los clientes.">Reiniciar filtros</button>
<button mat-flat-button color="primary" (click)="addClient($event)" joyrideStep="addClientStep" text="Añade un nuevo cliente a la lista.">Añadir cliente</button>
</div>
</div>
<div *ngIf="!loading" class="loading-container">
<mat-spinner></mat-spinner>
</div>
<mat-divider class="divider"></mat-divider>
<div *ngIf="loading">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<div class="search-container" joyrideStep="searchContainerStep" text="Filtra los clientes por nombre, IP, MAC o unidad organizativa.">
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar nombre de cliente</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>
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar IP</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" (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>
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar MAC</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['mac']" (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>
<mat-form-field appearance="fill" class="search-select" >
<mat-label i18n="@@organizational-unit-label">U. Organizativa</mat-label>
<mat-select [(ngModel)]="filters['organizationalUnit.id']" (selectionChange)="search()">
<mat-option *ngFor="let unit of organizationalUnits" [value]="unit.id" >
{{ unit.name }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<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">
<td mat-cell *matCellDef="let client" >
<ng-container *ngIf="column.columnDef === 'name'">
<div class="client-info">
<div class="client-name">{{ client.name }}</div>
@ -51,7 +59,6 @@
<div class="client-mac">{{ client.mac }}</div>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef === 'status'">
<mat-chip [ngClass]="{
'chip-og-live': client.status === 'og-live',
@ -64,21 +71,19 @@
{{ 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;">
<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="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>
@ -88,7 +93,8 @@
<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="paginationStep" text="Navega entre las páginas de resultados utilizando el paginador.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"

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

@ -1,21 +1,27 @@
<div class="header-container">
<h2 class="title" i18n="@@adminImagesTitle">Administrar unidades organizativas</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminImagesTitle" joyrideStep="titleS4tep" text="Gestiona las unidades organizativas desde esta pantalla.">Administrar unidades organizativas</h2>
<div class="button-row">
<button mat-flat-button color="primary" (click)="resetFilters()">Reiniciar filtros</button>
<button mat-flat-button color="primary" (click)="addOrganizationalUnit($event)">Añadir OU</button>
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="resetFiltersStep" text="Reinicia los filtros aplicados para mostrar todas las unidades organizativas.">Reiniciar filtros</button>
<button mat-flat-button color="primary" (click)="addOrganizationalUnit($event)" joyrideStep="addOUStep" text="Añade una nueva unidad organizativa.">Añadir OU</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<div class="search-container" joyrideStep="searchContainerStep" text="Filtra las unidades organizativas por nombre o tipo.">
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">Buscar nombre de OU</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>
<mat-form-field appearance="fill" class="search-boolean">
<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>
@ -24,28 +30,29 @@
</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'" >
<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>
<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>
<ng-container *ngIf="column.columnDef === 'type'">
<mat-chip>{{ ou.type }}</mat-chip>
</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 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>
<button mat-icon-button color="info" [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
@ -64,7 +71,8 @@
<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="paginationStep" text="Navega entre las páginas de unidades organizativas con el paginador.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"

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

@ -194,8 +194,7 @@ mat-spinner {
.result-card {
width: 100%;
max-width: 250px;
height: 250px; /* Fijo para mantener la forma cuadrada */
}
height: 250px;
.paginator-container {
display: flex;
@ -210,3 +209,7 @@ mat-spinner {
mat-card {
margin-bottom: 20px;
}
.mat-tooltip {
white-space: pre-line;
}

View File

@ -1,21 +1,39 @@
<mat-tab-group (selectedTabChange)="onTabChange($event)">
<mat-tab label="General">
<div class="header-container">
<h2 class="title" i18n="@@adminGroupsTitle">Administrar grupos</h2>
<div class="groups-button-row">
<button mat-flat-button color="primary" (click)="addOU($event)" i18n="@@newOrganizationalUnitButton">Nueva Unidad Organizativa</button>
<button mat-flat-button color="primary" (click)="addClient($event)" i18n="@@newClientButton">Nuevo Cliente</button>
<button mat-raised-button (click)="openBottomSheet()" i18n="@@legendButton">Leyenda</button>
<div class="header-container" joyrideStep="tabsStep" text="Utiliza las pestañás para acceder a las diferentes opciones de visualización y busqueda de unidades organizativas y clientes.">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminGroupsTitle" joyrideStep="titleStep" text="En esta pantalla, puedes gestionar las unidades organizativas y sus clientes.">Administrar grupos</h2>
<div class="groups-button-row" joyrideStep="addStep" text="Utiliza estos botones para crear nuevas unidades organizativas o clientes.">
<button mat-flat-button color="primary" (click)="addOU($event)"
i18n="@@newOrganizationalUnitButton"
matTooltip="Abrir modal para crear unidades organizativas de cualquier tipo (Centro, Aula, Grupo de aulas o Grupo de clientes)"
matTooltipShowDelay="1000">Nueva Unidad Organizativa</button>
<button mat-flat-button color="primary" (click)="addClient($event)"
i18n="@@newClientButton"
matTooltipShowDelay="1000">Nuevo Cliente</button>
<button mat-raised-button (click)="openBottomSheet()"
i18n="@@legendButton"
joyrideStep="keyStep" text="La leyenda te mostrará los tipos de unidades organizativas y sus iconos correspondientes"
matTooltipShowDelay="1000">Leyenda</button>
</div>
</div>
<div class="groupLists-container">
<mat-card class="card unidad-card">
<mat-card class="card unidad-card"
joyrideStep="unitStep" text="Esta es la sección donde se mostrarán las unidades organizativas de tipo 'Centro'"
matTooltipShowDelay="1000"
matTooltipPosition="above">
<mat-card-title i18n="@@organizationalUnitTitle">Centros</mat-card-title>
<mat-card-content>
<mat-spinner *ngIf="loading"></mat-spinner>
<mat-list *ngIf="!loading">
<mat-list-item *ngFor="let unidad of organizationalUnits"
[ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}" (click)="onSelectUnidad(unidad)">
[ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}"
(click)="onSelectUnidad(unidad)">
<div class="item-content">
<mat-icon>apartment</mat-icon>
{{ unidad.name }}
@ -23,48 +41,37 @@
<mat-icon mat-button [matMenuTriggerFor]="menu" (click)="$event.stopPropagation()">more_vert</mat-icon>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="onTreeClick($event, unidad)">
<mat-icon
#tooltip="matTooltip"
matTooltip="Visualizar en forma de arbol"
matTooltipHideDelay="0"
i18n="@@viewTreeTooltip">account_tree
</mat-icon>
<mat-icon matTooltip="Visualizar unidad en forma de árbol"
matTooltipHideDelay="0"
i18n="@@viewTreeTooltip">account_tree</mat-icon>
<span i18n="@@viewTreeMenu">Ver organigrama</span>
</button>
<button mat-menu-item (click)="onEditClick($event, unidad.type, unidad.uuid)">
<mat-icon
#tooltip="matTooltip"
matTooltip="Editar unidad organizativa"
matTooltipHideDelay="0"
i18n="@@editUnitTooltip">edit
</mat-icon>
<mat-icon matTooltip="Editar esta unidad organizativa"
matTooltipHideDelay="0"
i18n="@@editUnitTooltip">edit</mat-icon>
<span i18n="@@editUnitMenu">Editar</span>
</button>
<button mat-menu-item (click)="onShowClick($event, unidad)">
<mat-icon
#tooltip="matTooltip"
matTooltip="Visualizar unidad organizativa"
matTooltipHideDelay="0"
i18n="@@viewUnitTool">visibility
</mat-icon>
<span i18n="@@viewUnitMenu">Visualizar datos</span>
<mat-icon matTooltip="Ver detalles de la unidad organizativa"
matTooltipHideDelay="0"
i18n="@@viewUnitTooltip">visibility</mat-icon>
<span i18n="@@viewUnitMenu">Ver datos</span>
</button>
<button mat-menu-item (click)="addOU($event, unidad)">
<mat-icon
#tooltip="matTooltip"
matTooltip="Crear unidad organizativa interna"
matTooltipHideDelay="0"
i18n="@@addInternalUnitTool">add_home_work
</mat-icon>
<mat-icon matTooltip="Crear una nueva unidad organizativa interna"
matTooltipHideDelay="0"
i18n="@@addInternalUnitTooltip">add_home_work</mat-icon>
<span i18n="@@addInternalUnitMenu">Añadir unidad organizativa</span>
</button>
<button mat-menu-item (click)="addClient($event, unidad)">
<mat-icon
#tooltip="matTooltip"
matTooltip="Crear cliente en esta unidad organizativa"
matTooltipHideDelay="0"
i18n="@@addClientDevice">devices
</mat-icon>
<mat-icon matTooltip="Registrar un cliente en esta unidad organizativa"
matTooltipHideDelay="0"
i18n="@@addClientTooltip">devices</mat-icon>
<span i18n="@@addClientMenu">Crear cliente</span>
</button>
</mat-menu>
@ -74,7 +81,11 @@
</mat-list>
</mat-card-content>
</mat-card>
<mat-card class="card elements-card">
<mat-card class="card elements-card"
joyrideStep="elementsStep" text="Esta es la sección para visualizar unidades internas del centro seleccionado y navegar por ellas."
matTooltipShowDelay="1000"
matTooltipPosition="above">
<mat-card-title>
<div class="title-with-breadcrumb">
<span i18n="@@internalElementsTitle"></span>
@ -93,7 +104,9 @@
<mat-icon>info</mat-icon>
<span i18n="@@noInternalElementsMessage">No hay elementos internos</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>
@ -108,24 +121,44 @@
<mat-icon mat-button [matMenuTriggerFor]="menu" (click)="$event.stopPropagation()">more_vert</mat-icon>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="onEditClick($event, child.type, child.uuid)">
<mat-icon class="edit-icon" #tooltip="matTooltip" matTooltip="Editar elemento" matTooltipHideDelay="0" i18n="@@editElementTooltip">edit</mat-icon>
<mat-icon matTooltip="Editar elemento"
matTooltipHideDelay="0"
i18n="@@editElementTooltip">edit</mat-icon>
<span i18n="@@editElementMenu">Editar</span>
</button>
<button *ngIf="child.type !== 'client'" mat-menu-item (click)="onShowClick($event, child)">
<mat-icon class="edit-icon" #tooltip="matTooltip" matTooltip="Visualizar unidad organizativa" matTooltipHideDelay="0" i18n="@@viewUnitTooltip">visibility</mat-icon>
<span i18n="@@viewUnitMenu">Visualizar datos</span>
<mat-icon matTooltip="Ver detalles de la unidad organizativa"
matTooltipHideDelay="0"
i18n="@@viewUnitTooltip">visibility</mat-icon>
<span i18n="@@viewUnitMenu">Ver datos</span>
</button>
<button *ngIf="child.type !== 'client'" mat-menu-item (click)="addOU($event, child)">
<mat-icon class="edit-icon" #tooltip="matTooltip" matTooltip="Crear unidad organizativa interna" matTooltipHideDelay="0" i18n="@@addInternalUnitTooltip">add_home_work</mat-icon>
<mat-icon matTooltip="Crear una nueva unidad organizativa interna"
matTooltipHideDelay="0"
i18n="@@addInternalUnitTooltip">add_home_work</mat-icon>
<span i18n="@@addInternalUnitMenu">Añadir unidad organizativa</span>
</button>
<button *ngIf="child.type !== 'client'" mat-menu-item (click)="addClient($event, child)">
<mat-icon class="edit-icon" #tooltip="matTooltip" matTooltip="Crear cliente en esta unidad organizativa" matTooltipHideDelay="0" i18n="@@addClientTooltip">devices</mat-icon>
<mat-icon matTooltip="Registrar un cliente en esta unidad organizativa"
matTooltipHideDelay="0"
i18n="@@addClientTooltip">devices</mat-icon>
<span i18n="@@addClientMenu">Crear cliente</span>
</button>
<button mat-menu-item (click)="onDeleteClick($event, child.uuid, child.name, child.type)">
<mat-icon class="delete-icon" #tooltip="matTooltip" matTooltip="Borrar elemento" matTooltipHideDelay="0" i18n="@@deleteElementTooltip">delete</mat-icon>
<span i18n="@@deleteElementMenu">Borrar elemento</span>
<mat-icon matTooltip="Eliminar este elemento"
matTooltipHideDelay="0"
i18n="@@deleteElementTooltip">delete</mat-icon>
<span i18n="@@deleteElementMenu">Eliminar elemento</span>
</button>
<button mat-menu-item (click)="onExecuteCommand($event, child.uuid, child.name, child.type)">
<mat-icon matTooltip="Ejecutar comando en este elemento"
matTooltipHideDelay="0">play_arrow</mat-icon>
<span>Ejecutar comando</span>
</button>
</mat-menu>
</div>
@ -136,12 +169,15 @@
</mat-card>
</div>
</mat-tab>
<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">
<app-organizational-unit-tab-view #organizationalUnitTab></app-organizational-unit-tab-view>
</mat-tab>

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,45 @@
<h2 mat-dialog-title>Ejecutar Comando o Grupo de Comandos</h2>
<mat-dialog-content class="form-container">
<form [formGroup]="form" class="command-form">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione Comando</mat-label>
<mat-select formControlName="selectedCommand" (selectionChange)="form.get('selectedCommandGroup')?.reset()">
<mat-option *ngFor="let command of commands" [value]="command.uuid">{{ command.name }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione Grupo de Comandos</mat-label>
<mat-select formControlName="selectedCommandGroup" (selectionChange)="form.get('selectedCommand')?.reset()">
<mat-option *ngFor="let group of commandGroups" [value]="group.uuid">{{ group.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]="form.get('clientSelection')?.value.includes(client.uuid)">
{{ client.name }}
</mat-checkbox>
</div>
<div *ngIf="clients.length === 0">
<p>No hay clientes disponibles</p>
</div>
</div>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="closeModal()">Cancelar</button>
<button mat-button
(click)="executeCommand()"
[disabled]="!form.get('clientSelection')?.value.length ||
(!form.get('selectedCommand')?.value && !form.get('selectedCommandGroup')?.value)">
Ejecutar
</button>
</mat-dialog-actions>

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

@ -1,78 +1,64 @@
<div class="header-container">
<h2 class="title">Administrar imágenes</h2>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="addImage()">Añadir imagen</button>
</div>
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" joyrideStep="imagesTitleStep" text="En esta pantalla, puedes gestionar las imágenes disponibles.">
Administrar imágenes
</h2>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="addImage()" joyrideStep="addImageButton" text="Añade una nueva imagen a la lista.">
Añadir imagen
</button>
</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>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image">
<!-- Chip for 'status' column -->
<ng-container *ngIf="column.columnDef === 'status'">
<mat-chip [ngClass]="{
'status-success': image[column.columnDef] === 'success',
'status-failed': image[column.columnDef] === 'failed',
'status-pending': image[column.columnDef] === 'pending'
}"
>
{{ image[column.columnDef] }}
</mat-chip>
</ng-container>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="imagesTable" text="Esta tabla muestra las imágenes disponibles.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef === 'remotePc'">
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
</mat-icon>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'remotePc'">
{{ column.cell(image) }}
</ng-container>
</td>
</ng-container>
>>>>>>> c168c87fc9ebcc4b5cf9a68effa16bdb2a848e00
<!-- Icon for 'remotePc' column -->
<ng-container *ngIf="column.columnDef === 'remotePc'">
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
</mat-icon>
</ng-container>
<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>
<!-- Default cell content for other columns -->
<ng-container *ngIf="column.columnDef !== 'status' && column.columnDef !== 'remotePc'">
{{ column.cell(image) }}
</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 image" style="text-align: center;">
<button mat-icon-button color="info" (click)="showImageInfo($event, image)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" (click)="editImage($event, image)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="deleteImage($event, image)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="toggleAction(image, 'get-aux')">Obtener ficheros auxiliares</button>
<button mat-menu-item [disabled]="!image.imageFullsum" (click)="toggleAction(image, 'get-aux')">Eliminar imagen temporalmente</button>
</mat-menu>
</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]="[5, 10, 20, 40, 100]"
(page)="onPageChange($event)">
</mat-paginator>
</div>
<div class="paginator-container" joyrideStep="imagesPagination" text="Navega entre las páginas de imágenes.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"
[pageSizeOptions]="[5, 10, 20, 40, 100]"
(page)="onPageChange($event)">
</mat-paginator>
</div>

View File

@ -10,6 +10,7 @@ import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delet
import {ServerInfoDialogComponent} from "../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component";
import {Observable} from "rxjs";
import {InfoImageComponent} from "../ogboot/pxe-images/info-image/info-image/info-image.component";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-images',
@ -70,7 +71,8 @@ export class ImagesComponent implements OnInit {
constructor(
public dialog: MatDialog,
private http: HttpClient,
private toastService: ToastrService
private toastService: ToastrService,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
@ -178,4 +180,20 @@ export class ImagesComponent implements OnInit {
break;
}
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'imagesTitleStep',
'addImageButton',
'searchImageField',
'imagesTable',
'actionsHeader',
'editImageButton',
'deleteImageButton',
'imagesPagination'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

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

View File

@ -1,59 +1,64 @@
<div class="dashboard">
<h2>OgBoot server Status</h2>
<div class="header-container" >
<h2 joyrideStep="titleStep" text="Esta sección muestra el estado general del servidor OgBoot.">OgBoot 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">
<h3>Uso de disco</h3>
<ngx-charts-pie-chart
[view]="view"
[scheme]="colorScheme"
[results]="diskUsageChartData"
[gradient]="gradient"
[doughnut]="isDoughnut"
[labels]="showLabels"
[legend]="showLegend">
</ngx-charts-pie-chart>
<div class="disk-usage-info">
<p>Total: {{ diskUsage.total }}</p>
<p>Ocupado: {{ diskUsage.used }}</p>
<p>Disponible: {{ diskUsage.available }}</p>
<p>Libre: {{ diskUsage.percentage }}</p>
</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 class="disk-usage-container">
<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"
[scheme]="colorScheme"
[results]="diskUsageChartData"
[gradient]="gradient"
[doughnut]="isDoughnut"
[labels]="showLabels"
[legend]="showLegend">
</ngx-charts-pie-chart>
<div class="disk-usage-info">
<p>Total: {{ formatBytes(diskUsage.total) }}</p>
<p>Ocupado: {{ formatBytes(diskUsage.used) }}</p>
<p>Disponible: {{ formatBytes(diskUsage.available) }}</p>
<p>Libre: {{ diskUsage.percentage }}%</p>
</div>
</div>
<div class="installed-oglives">
<h3>OGLives instalados</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>Kernel</th>
<th>Architecture</th>
<th>Revision</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let oglive of installedOglives">
<td>{{ oglive.id }}</td>
<td>{{ oglive.kernel }}</td>
<td>{{ oglive.architecture }}</td>
<td>{{ oglive.revision }}</td>
</tr>
</tbody>
</table>
<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" joyrideStep="oglivesStep" text="Consulta la información de los OGLives instalados en el servidor.">
<h3>OGLives instalados</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>Kernel</th>
<th>Architecture</th>
<th>Revision</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let oglive of installedOglives">
<td>{{ oglive.id }}</td>
<td>{{ oglive.kernel }}</td>
<td>{{ oglive.architecture }}</td>
<td>{{ oglive.revision }}</td>
</tr>
</tbody>
</table>
</div>
</div>

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',
@ -23,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();
@ -56,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,10 +1,12 @@
<div class="header-container">
<h2 class="title">Netboot avanzado</h2>
<h2 class="title" joyrideStep="titleStep" text="Esta sección permite la asignación masiva de plantillas de arranque a clientes.">Netboot avanzado</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
</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>
<mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
<mat-option *ngIf="loadingUnits" disabled>Cargando unidades...</mat-option>
@ -15,7 +17,7 @@
<mat-error *ngIf="taskForm.get('organizationalUnit')?.invalid">Este campo es obligatorio</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="search-boolean">
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="selectClassStep" text="Selecciona el aula para configurar las plantillas en sus dispositivos.">
<mat-label>Selecciona aula</mat-label>
<mat-select formControlName="selectedChild" (selectionChange)="onChildChange()">
<mat-option *ngIf="selectedUnitChildren.length === 0" disabled>No hay aulas disponibles</mat-option>
@ -29,17 +31,17 @@
<mat-divider class="divider"></mat-divider>
<div class="global-selectors">
<mat-form-field appearance="fill" class="selected-global">
<mat-form-field appearance="fill" class="selected-global" joyrideStep="applyToAllStep" text="Selecciona una plantilla para aplicarla a todos los clientes de esta aula.">
<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">
<mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="Administra las plantillas de Netboot para cada cliente en esta tabla.">
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef> Id </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.id}} </mat-cell>
@ -53,7 +55,7 @@
<ng-container matColumnDef="ogLive">
<mat-header-cell *matHeaderCellDef> Plantilla </mat-header-cell>
<mat-cell *matCellDef="let client">
<mat-form-field appearance="fill">
<mat-form-field appearance="fill" joyrideStep="selectTemplateStep" text="Selecciona una plantilla específica para cada cliente.">
<mat-label>Seleccione una plantilla</mat-label>
<mat-select [(ngModel)]="client.ogLive" [name]="'ogLive' + client.id">
<mat-option [value]="null">Ninguna</mat-option>
@ -65,7 +67,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

@ -1,41 +1,47 @@
<mat-accordion class="example-headers-align">
<mat-expansion-panel hideToggle>
<mat-expansion-panel-header>
<mat-expansion-panel-header joyrideStep="serverInfoStep" text="Accede a información y opciones de sincronización en el servidor OgBoot.">
<mat-panel-title> Información en servidor ogBoot </mat-panel-title>
</mat-expansion-panel-header>
<div class="button-row">
<button mat-flat-button color="primary" (click)="syncOgBoot()"> Sincronizar base de datos</button>
<button mat-flat-button color="primary" (click)="syncOgBoot()" > Sincronizar base de datos</button>
</div>
<div class="button-row">
<button mat-flat-button color="accent" (click)="openSubnetInfoDialog()">Ver Información</button>
<button mat-flat-button color="accent" (click)="openSubnetInfoDialog()" >Ver Información</button>
</div>
</mat-expansion-panel>
</mat-accordion>
<div class="header-container">
<h2 class="title" i18n="@@adminImagesTitle">Administrar imágenes</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminImagesTitle" joyrideStep="titleStep" text="Desde aquí puedes gestionar las imágenes configuradas en el servidor OgBoot.">Administrar imágenes</h2>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="addImage()">Añadir imagen</button>
<button mat-flat-button color="primary" (click)="addImage()" joyrideStep="addImageStep" text="Haz clic para añadir una nueva imagen.">Añadir imagen</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 imágenes por nombre para encontrar rápidamente una imagen específica.">
<mat-label i18n="@@searchLabel">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 i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-boolean">
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="searchDefaultImageStep" text="Filtra las imágenes para mostrar solo las imágenes por defecto o no por defecto.">
<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>
@ -44,11 +50,11 @@
</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" >
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef === 'isDefault' || column.columnDef === 'installed'">
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
@ -76,15 +82,14 @@
<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">
<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>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
@ -100,7 +105,8 @@
<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="paginationStep" text="Navega entre las páginas de imágenes usando el paginador.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"

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 {
@ -255,4 +257,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

@ -1,9 +1,8 @@
<h2 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Añadir' }} plantilla </h2>
<h2 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Añadir' }} plantilla</h2>
<mat-dialog-content>
<div class="spacing-container">
<form [formGroup]="templateForm" (ngSubmit)="onSave()">
<mat-form-field appearance="fill">
<mat-label>Nombre de la Plantilla</mat-label>
<input matInput formControlName="name" placeholder="Introduce el nombre de la plantilla">
@ -23,9 +22,19 @@
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button type="button" (click)="onCancel()">Cancelar</button>
<button mat-raised-button color="primary" type="submit" (click)="onSave()" [disabled]="!templateForm.valid">
{{ isEditMode ? 'Actualizar' : 'Crear' }}
</button>
<mat-dialog-actions>
<div class="actions-container">
<button mat-flat-button color="accent" [matMenuTriggerFor]="templateMenu">Cargar plantilla modelo</button>
<mat-menu #templateMenu="matMenu">
<button mat-menu-item (click)="loadTemplateModel('ogLive')">ogLive</button>
<button mat-menu-item (click)="loadTemplateModel('disco')">Arranque por disco</button>
</mat-menu>
<div class="action-buttons">
<button mat-button type="button" (click)="onCancel()">Cancelar</button>
<button mat-raised-button color="primary" type="submit" (click)="onSave()" [disabled]="!templateForm.valid">
{{ isEditMode ? 'Actualizar' : 'Crear' }}
</button>
</div>
</div>
</mat-dialog-actions>

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

@ -1,10 +1,10 @@
<mat-accordion class="example-headers-align">
<mat-expansion-panel hideToggle>
<mat-expansion-panel-header>
<mat-expansion-panel-header joyrideStep="serverInfoStep" text="Accede a información y opciones de sincronización en el servidor OgBoot.">
<mat-panel-title> Información en servidor ogBoot </mat-panel-title>
</mat-expansion-panel-header>
<div class="example-button-row">
<button mat-flat-button color="primary" (click)="syncTemplates()"> Sincronizar base de datos</button>
<button mat-flat-button color="primary" (click)="syncTemplates()" > Sincronizar base de datos</button>
</div>
<div class="example-button-row">
<button mat-flat-button color="accent" (click)="openSubnetInfoDialog()">Ver Información</button>
@ -12,34 +12,39 @@
</mat-expansion-panel>
</mat-accordion>
<div class="header-container">
<h2 class="title" i18n="@@adminPXETitle">Administrar plantillas PXE</h2>
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminPXETitle" joyrideStep="titleStep" text="Desde aquí puedes gestionar las plantillas PXE configuradas en el servidor OgBoot.">Administrar plantillas PXE</h2>
<div class="pxe-button-row">
<button mat-flat-button color="primary" (click)="addPxeTemplate()">Añadir plantilla PXE</button>
<button mat-flat-button color="primary" (click)="addPxeTemplate()" joyrideStep="addTemplateStep" text="Haz clic para añadir una nueva plantilla PXE.">Añadir plantilla PXE</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 plantillas PXE por nombre para localizar rápidamente una plantilla específica.">
<mat-label i18n="@@searchLabel">Buscar nombre de plantilla</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>
<mat-form-field appearance="fill" class="search-boolean">
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="searchSyncStep" text="Filtra para ver solo las plantillas creadas en el servidor OgBoot.">
<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" >
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef === 'synchronized'">
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
@ -52,19 +57,19 @@
</td>
</ng-container>
<ng-container matColumnDef="actions" >
<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>
<button mat-icon-button color="info" [disabled]="template.clientsLength === 0" (click)="editClients($event, template)"><mat-icon i18n="@@deleteElementTooltip">computer</mat-icon></button>
<button mat-icon-button color="primary" (click)="editPxeTemplate(template)" i18n="@@editImage"><mat-icon>edit</mat-icon></button>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="toggleAction(template, 'create')">Crear 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>
</mat-menu>
@ -74,7 +79,8 @@
<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="paginationStep" text="Navega entre las páginas de plantillas PXE usando el paginador.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"

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 {
@ -229,4 +231,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);
@ -217,4 +220,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

@ -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,23 +119,6 @@ export class CreateSoftwareProfileComponent {
this.softwareControl.setValue('');
}
load(): void {
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)) {
@ -144,7 +137,6 @@ export class CreateSoftwareProfileComponent {
onSubmit(): void {
if (this.formGroup.valid) {
const payload = {
description: this.formGroup.value.description,
comments: this.formGroup.value.comments,
@ -155,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

@ -1,17 +1,35 @@
<mat-toolbar>
<span class="navbar-title" i18n="@@webConsoleTitle">Opengnsys webconsole</span>
<button mat-icon-button (click)="onToggleSidebar()">
<mat-icon class="navbar-icon" >menu</mat-icon>
<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="trace-button" routerLink="/commands-logs" mat-button i18n="@@admin"><mat-icon>notifications</mat-icon></button>
<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="/var-envs" i18n="@@rolesMenuItem">Variables de entorno</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,51 +1,49 @@
<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-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>
@ -54,23 +52,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>
@ -79,41 +75,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>
@ -122,19 +117,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>
@ -142,33 +137,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="@@images">imágenes</span>
<span i18n="@@images">Imágenes</span>
</span>
</mat-list-item>
<mat-list-item>
<span class="entry" routerLink="/repositories">
<mat-icon class="icon">warehouse </mat-icon>
<span i18n="@@actions">Repositorios</span>
<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>