diff --git a/CHANGELOG.md b/CHANGELOG.md index 394f881..7d4e7e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,23 @@ # Changelog +## [0.12.0] - 2025-5-13 +### Added +- Se ha añadido un nuevo modal del detalle de las acciones ejecutadas por cada cliente. +- Se ha añadido un modulo para la gestion de las tareas y acciones programadas. +- Se han añadido nuevos campos en el listado general de clientes. + +### Improved +- Se ha cambiado la pagina de detalles de un cliente, por un modal. +- Se han actualizado gran parte de las ayudas contextuales de las distintas parrillas de datos. +- Se ha mejorado y corregido los errores del particionador. +- Mejoras en la pantalla de trazas. +- Cambios en la estetica general de la aplicacion +- Añadida la primera version de la la integracion con ogGit +- Se ha mejorado la responsividad de la aplicacion, para pantallas pequeñas. + +### Fixed +- Se ha corregido un error que hacia que no apareciesen los calendarios en la pantalla de editar OU. +- En la pantalla de hacer deploy, al seleccionar imagen ahora deja desmarcarla. + ## [0.11.2] - 2025-4-16 ### Fixed - Se ha corregido un error en la actualizacion del estado de los pcs en la vista tarjetas. diff --git a/ogWebconsole/src/app/app-routing.module.ts b/ogWebconsole/src/app/app-routing.module.ts index cbd353a..f901d43 100644 --- a/ogWebconsole/src/app/app-routing.module.ts +++ b/ogWebconsole/src/app/app-routing.module.ts @@ -17,9 +17,7 @@ import { CalendarComponent } from "./components/calendar/calendar.component"; import { CommandsComponent } from './components/commands/main-commands/commands.component'; import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component'; import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component'; -import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component'; -import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component'; -import { ImagesComponent } from './components/images/images.component'; +import { TaskLogsComponent } from './components/task-logs/task-logs.component'; import {SoftwareComponent} from "./components/software/software.component"; import {SoftwareProfileComponent} from "./components/software-profile/software-profile.component"; import {OperativeSystemComponent} from "./components/operative-system/operative-system.component"; @@ -67,7 +65,6 @@ const routes: Routes = [ { path: 'clients/deploy-image', component: DeployImageComponent }, { path: 'clients/partition-assistant', component: PartitionAssistantComponent }, { path: 'clients/run-script', component: RunScriptAssistantComponent }, - { path: 'clients/:id', component: ClientMainViewComponent }, { path: 'clients/:id/create-image', component: CreateClientImageComponent }, { path: 'repositories', component: RepositoriesComponent }, { path: 'repository/:id', component: MainRepositoryViewComponent }, diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index 2bd3f99..bd95fa4 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -67,7 +67,7 @@ import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.co import { CreatePXEImageComponent } from './components/ogboot/pxe-images/create-image/create-image/create-image.component'; import { InfoImageComponent } from './components/ogboot/pxe-images/info-image/info-image/info-image.component'; import { PxeComponent } from './components/ogboot/pxe/pxe.component'; -import { CreatePxeTemplateComponent } from './components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component'; +import { CreatePxeTemplateComponent } from './components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component'; import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component'; import { MatExpansionPanel, MatExpansionPanelDescription, MatExpansionPanelTitle } from "@angular/material/expansion"; import { OgbootStatusComponent } from './components/ogboot/ogboot-status/ogboot-status.component'; @@ -87,9 +87,8 @@ import { CreateCommandGroupComponent } from './components/commands/commands-grou import { DetailCommandGroupComponent } from './components/commands/commands-groups/detail-command-group/detail-command-group.component'; import { CreateTaskComponent } from './components/commands/commands-task/create-task/create-task.component'; import { DetailTaskComponent } from './components/commands/commands-task/detail-task/detail-task.component'; -import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component'; +import { TaskLogsComponent } from './components/task-logs/task-logs.component'; import { MatSliderModule } from '@angular/material/slider'; -import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component'; import { ImagesComponent } from './components/images/images.component'; import { CreateImageComponent } from './components/images/create-image/create-image.component'; import { CreateClientImageComponent } from './components/groups/components/client-main-view/create-image/create-image.component'; @@ -118,7 +117,7 @@ import { CreateMultipleClientComponent } from './components/groups/shared/client import { ExportImageComponent } from './components/images/export-image/export-image.component'; import { ImportImageComponent } from "./components/repositories/import-image/import-image.component"; import { LoadingComponent } from './shared/loading/loading.component'; -import { InputDialogComponent } from './components/commands/commands-task/task-logs/input-dialog/input-dialog.component'; +import { InputDialogComponent } from './components/task-logs/input-dialog/input-dialog.component'; import { ManageOrganizationalUnitComponent } from './components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component'; import { BackupImageComponent } from './components/repositories/backup-image/backup-image.component'; import { ServerInfoDialogComponent } from "./components/ogdhcp/server-info-dialog/server-info-dialog.component"; @@ -130,7 +129,7 @@ import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clie import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component'; import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component'; import { ConvertImageComponent } from './components/repositories/convert-image/convert-image.component'; -import { registerLocaleData } from '@angular/common'; +import {NgOptimizedImage, registerLocaleData} from '@angular/common'; import localeEs from '@angular/common/locales/es'; import { GlobalStatusComponent } from './components/global-status/global-status.component'; import { ShowMonoliticImagesComponent } from './components/repositories/show-monolitic-images/show-monolitic-images.component'; @@ -143,6 +142,15 @@ import { import { EditImageComponent } from './components/repositories/edit-image/edit-image.component'; import { ShowGitImagesComponent } from './components/repositories/show-git-images/show-git-images.component'; import { RenameImageComponent } from './components/repositories/rename-image/rename-image.component'; +import { ClientDetailsComponent } from './components/groups/shared/client-details/client-details.component'; +import { PartitionTypeOrganizatorComponent } from './components/groups/shared/partition-type-organizator/partition-type-organizator.component'; +import { CreateTaskScheduleComponent } from './components/commands/commands-task/create-task-schedule/create-task-schedule.component'; +import { ShowTaskScheduleComponent } from './components/commands/commands-task/show-task-schedule/show-task-schedule.component'; +import { ShowTaskScriptComponent } from './components/commands/commands-task/show-task-script/show-task-script.component'; +import { CreateTaskScriptComponent } from './components/commands/commands-task/create-task-script/create-task-script.component'; +import { ViewParametersModalComponent } from './components/commands/commands-task/show-task-script/view-parameters-modal/view-parameters-modal.component'; +import { OutputDialogComponent } from './components/task-logs/output-dialog/output-dialog.component'; +import { ClientTaskLogsComponent } from './components/task-logs/client-task-logs/client-task-logs.component'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './locale/', '.json'); @@ -204,7 +212,6 @@ registerLocaleData(localeEs, 'es-ES'); TaskLogsComponent, ServerInfoDialogComponent, StatusComponent, - ClientMainViewComponent, ImagesComponent, CreateImageComponent, PartitionAssistantComponent, @@ -243,7 +250,16 @@ registerLocaleData(localeEs, 'es-ES'); SaveScriptComponent, EditImageComponent, ShowGitImagesComponent, - RenameImageComponent + RenameImageComponent, + ClientDetailsComponent, + PartitionTypeOrganizatorComponent, + CreateTaskScheduleComponent, + ShowTaskScheduleComponent, + ShowTaskScriptComponent, + CreateTaskScriptComponent, + ViewParametersModalComponent, + OutputDialogComponent, + ClientTaskLogsComponent ], bootstrap: [AppComponent], imports: [BrowserModule, @@ -292,7 +308,7 @@ registerLocaleData(localeEs, 'es-ES'); progressAnimation: 'increasing', closeButton: true } - ), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger + ), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger, NgOptimizedImage ], schemas: [ CUSTOM_ELEMENTS_SCHEMA, diff --git a/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.css b/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.css index 2f3d78a..f0c9645 100644 --- a/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.css +++ b/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.css @@ -22,7 +22,12 @@ .time-fields { display: flex; - gap: 15px; /* Espacio entre los campos */ + gap: 15px; +} + +.hour-fields { + display: flex; + gap: 15px; } .time-field { @@ -34,4 +39,74 @@ justify-content: flex-end; gap: 1em; padding: 1.5em; -} \ No newline at end of file +} + +.custom-text { + font-style: italic; + font-size: 0.875rem; + color: #666; + margin: 4px 0 12px; +} + +.weekday-toggle-group { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin: 40px 0 40px 0; + width: 100%; + justify-content: space-between; +} + +.weekday-toggle { + flex: 1 1 calc(14.28% - 10px); + padding: 10px 0; + border-radius: 999px; + border: 1px solid #ccc; + background-color: #f5f5f5; + cursor: pointer; + font-size: 14px; + text-align: center; + transition: all 0.2s ease; + min-width: 40px; +} + +.weekday-toggle.selected { + background-color: #1976d2; + color: white; + border-color: #1976d2; +} + + +.availability-summary { + background-color: #e3f2fd; + border-left: 4px solid #2196f3; + padding: 12px 16px; + margin-top: 16px; + border-radius: 6px; + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; +} + +.summary-text { + color: #0d47a1; + line-height: 1.4; +} + +.unavailability-summary { + background-color: #ffebee; + border-left: 4px solid #d32f2f; + padding: 12px 16px; + margin-top: 16px; + border-radius: 6px; + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; +} + +.summary-text { + color: #b71c1c; + line-height: 1.4; +} diff --git a/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.html b/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.html index b1de852..8e24c5c 100644 --- a/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.html +++ b/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.html @@ -1,24 +1,26 @@

{{ isEditMode ? ('editCalendar' | translate) : ('addCalendar' | translate) }}

- + {{ 'remoteAvailability' | translate }} - + + +
{{ 'selectWeekDays' | translate }} -
-
- - {{ day }} - -
-
- - {{ day }} - -
+

(Los dias y horas seleccionados se marcarán como aula no disponible para remote pc.)

+
+
+
{{ 'startTime' | translate }} @@ -30,12 +32,24 @@
+ + + +
+ block + + El aula estará no disponible para Remote PC los días: + {{ getSelectedDays().join(', ') }} de + {{ busyFromHour }} a {{ busyToHour }}. + +
{{ 'reasonLabel' | translate }} + Razón por la cual el aula SI está disponible para su uso en Remote PC
@@ -53,6 +67,32 @@
+
+ + {{ 'startTime' | translate }} + + + + + {{ 'endTime' | translate }} + + +
+ + + +
+ info + + El aula estará disponible para reserva desde el + {{ availableFromDate | date:'fullDate' }} hasta el + {{ availableToDate | date:'fullDate' }} + + en el horario de {{ busyFromHour }} a {{ busyToHour }}. + + +
+
diff --git a/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.ts b/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.ts index 716818f..25440b8 100644 --- a/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.ts +++ b/ogWebconsole/src/app/components/calendar/create-calendar-rule/create-calendar-rule.component.ts @@ -64,8 +64,8 @@ export class CreateCalendarRuleComponent { this.dialogRef.close(); } - toggleAdditionalForm(): void { - this.showAdditionalForm = !this.showAdditionalForm; + getSelectedDays(): string[] { + return Object.keys(this.busyWeekDays || {}).filter(day => this.busyWeekDays[day]); } getSelectedDaysIndices() { @@ -74,6 +74,11 @@ export class CreateCalendarRuleComponent { .filter(index => index !== -1); } + convertDateToLocalISO(date: Date): string { + const adjustedDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + return adjustedDate.toISOString(); + } + submitRule(): void { this.getSelectedDaysIndices() const selectedDaysArray = Object.keys(this.busyWeekDays).map((day, index) => this.busyWeekDays[index]); @@ -83,8 +88,8 @@ export class CreateCalendarRuleComponent { busyWeekDays: this.selectedDaysIndices, busyFromHour: this.busyFromHour, busyToHour: this.busyToHour, - availableFromDate: this.availableFromDate, - availableToDate: this.availableToDate, + availableFromDate: this.availableFromDate ? this.convertDateToLocalISO(this.availableFromDate) : null, + availableToDate: this.availableToDate ? this.convertDateToLocalISO(this.availableToDate) : null, isRemoteAvailable: this.isRemoteAvailable, availableReason: this.availableReason }; @@ -93,7 +98,7 @@ export class CreateCalendarRuleComponent { this.http.put(`${this.baseUrl}${this.ruleId}`, formData) .subscribe({ next: (response) => { - this.toastService.success('Calendar updated successfully'); + this.toastService.success('Calendar rule updated successfully'); this.dialogRef.close(true); }, error: (error) => { @@ -105,7 +110,7 @@ export class CreateCalendarRuleComponent { this.http.post(`${this.baseUrl}/remote-calendar-rules`, formData) .subscribe({ next: (response) => { - this.toastService.success('Calendar created successfully'); + this.toastService.success('Calendar rule created successfully'); this.dialogRef.close(true); }, error: (error) => { diff --git a/ogWebconsole/src/app/components/calendar/create-calendar/create-calendar.component.css b/ogWebconsole/src/app/components/calendar/create-calendar/create-calendar.component.css index fa815f9..cbad9e3 100644 --- a/ogWebconsole/src/app/components/calendar/create-calendar/create-calendar.component.css +++ b/ogWebconsole/src/app/components/calendar/create-calendar/create-calendar.component.css @@ -25,7 +25,7 @@ .time-fields { display: flex; - gap: 15px; /* Espacio entre los campos */ + gap: 15px; } .time-field { @@ -34,24 +34,25 @@ .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 { - flex-grow: 1; /* Permite que este contenedor ocupe el espacio disponible */ - margin-right: 16px; /* Espaciado a la derecha para separar de los íconos */ + flex-grow: 1; + margin-right: 16px; margin-left: 10px; + margin-bottom: 16px; } .icon-container { display: flex; - align-items: center; /* Alinea los íconos verticalmente */ + align-items: center; } .right-icon { - margin-left: 8px; /* Espaciado entre los íconos */ + margin-left: 8px; cursor: pointer; } @@ -60,4 +61,15 @@ justify-content: flex-end; gap: 1em; padding: 1.5em; -} \ No newline at end of file +} + +.rule-available { + background-color: #e8f5e9; + border-left: 4px solid #4caf50; +} + +.rule-unavailable { + background-color: #ffebee; + border-left: 4px solid #f44336; +} + diff --git a/ogWebconsole/src/app/components/calendar/create-calendar/create-calendar.component.html b/ogWebconsole/src/app/components/calendar/create-calendar/create-calendar.component.html index fd76357..52dc365 100644 --- a/ogWebconsole/src/app/components/calendar/create-calendar/create-calendar.component.html +++ b/ogWebconsole/src/app/components/calendar/create-calendar/create-calendar.component.html @@ -6,7 +6,7 @@ mode_edit - +
{{ 'rulesHeader' | translate }}
+
+ +
+ + + diff --git a/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.spec.ts b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.spec.ts new file mode 100644 index 0000000..bd363f4 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.spec.ts @@ -0,0 +1,52 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ClientDetailsComponent } from './client-details.component'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { ToastrModule } from 'ngx-toastr'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { ConfigService } from '@services/config.service'; +import { LoadingComponent } from 'src/app/shared/loading/loading.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatDividerModule } from '@angular/material/divider'; +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatTableModule } from '@angular/material/table'; + +describe('ClientDetailsComponent', () => { + let component: ClientDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + const mockConfigService = { + apiUrl: 'http://mock-api-url', + mercureUrl: 'http://mock-mercure-url' + }; + await TestBed.configureTestingModule({ + declarations: [ClientDetailsComponent, LoadingComponent], + imports: [ + MatDialogModule, + HttpClientTestingModule, + ToastrModule.forRoot(), + MatDividerModule, + TranslateModule.forRoot(), + CommonModule, + MatTableModule, + MatProgressSpinnerModule + ], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } }, + { provide: ConfigService, useValue: mockConfigService } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ClientDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.ts b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.ts similarity index 51% rename from ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.ts rename to ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.ts index bf77392..401a40f 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.ts @@ -1,4 +1,5 @@ -import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { DatePipe } from "@angular/common"; import { MatTableDataSource } from "@angular/material/table"; @@ -14,11 +15,11 @@ interface ClientInfo { } @Component({ - selector: 'app-client-main-view', - templateUrl: './client-main-view.component.html', - styleUrl: './client-main-view.component.css' + selector: 'app-client-details', + templateUrl: './client-details.component.html', + styleUrl: './client-details.component.css' }) -export class ClientMainViewComponent implements OnInit { +export class ClientDetailsComponent { baseUrl: string; @ViewChild('assistantContainer') assistantContainer!: ElementRef; clientUuid: string; @@ -34,39 +35,30 @@ export class ClientMainViewComponent implements OnInit { partitions: any[] = []; commands: any[] = []; chartDisk: any[] = []; - view: [number, number] = [300, 200]; + view: [number, number] = [260, 160]; showLegend: boolean = true; - arrayCommands: any[] = [ - { name: 'Enceder', slug: 'power-on' }, - { name: 'Apagar', slug: 'power-off' }, - { name: 'Reiniciar', slug: 'reboot' }, - { name: 'Iniciar Sesión', slug: 'login' }, - { name: 'Crear imagen', slug: 'create-image' }, - { name: 'Clonar/desplegar imagen', slug: 'deploy-image' }, - { name: 'Eliminar Imagen Cache', slug: 'delete-image-cache' }, - { name: 'Particionar y Formatear', slug: 'partition' }, - { name: 'Inventario Software', slug: 'software-inventory' }, - { name: 'Inventario Hardware', slug: 'hardware-inventory' }, - { name: 'Ejecutar comando', slug: 'run-script' }, - ]; - datePipe: DatePipe = new DatePipe('es-ES'); columns = [ { columnDef: 'diskNumber', header: 'Disco', - cell: (partition: any) => `${partition.diskNumber}`, + cell: (partition: any) => partition.diskNumber, }, { columnDef: 'partitionNumber', header: 'Particion', - cell: (partition: any) => `${partition.partitionNumber}` + cell: (partition: any) => partition.partitionNumber + }, + { + columnDef: 'partitionCode', + header: 'Tipo de partición', + cell: (partition: any) => partition.partitionCode }, { columnDef: 'description', header: 'Sistema de ficheros', - cell: (partition: any) => `${partition.filesystem}` + cell: (partition: any) => partition.filesystem }, { columnDef: 'size', @@ -76,13 +68,13 @@ export class ClientMainViewComponent implements OnInit { { columnDef: 'memoryUsage', header: 'Uso', - cell: (partition: any) => `${partition.memoryUsage} %` + cell: (partition: any) => `${partition.memoryUsage}%` }, { columnDef: 'operativeSystem', - header: 'SO', - cell: (partition: any) => `${partition.operativeSystem?.name}` - }, + header: 'SO/Imagen', + cell: (partition: any) => partition.operativeSystem?.name + } ]; displayedColumns = [...this.columns.map(column => column.columnDef)]; isDiskUsageEmpty: boolean = true; @@ -93,7 +85,8 @@ export class ClientMainViewComponent implements OnInit { private dialog: MatDialog, private configService: ConfigService, private router: Router, - private toastService: ToastrService + private toastService: ToastrService, + @Inject(MAT_DIALOG_DATA) public data: { clientData: any } ) { this.baseUrl = this.configService.apiUrl; const url = window.location.href; @@ -102,31 +95,16 @@ export class ClientMainViewComponent implements OnInit { } ngOnInit() { - this.clientData = history.state.clientData['@id']; - this.loadClient(this.clientData) - this.loadCommands() - this.calculateDiskUsage(); - this.loading = false; - } - - - loadClient = (uuid: string) => { - this.http.get(`${this.baseUrl}${uuid}`).subscribe({ - next: data => { - this.clientData = data; - this.updateGeneralData(); - this.updateNetworkData(); - this.loadPartitions() - this.loading = false; - }, - error: error => { - console.error('Error al obtener el cliente:', error); - } - }); - } - - navigateToGroups() { - this.router.navigate(['/groups']); + if (this.data && this.data.clientData) { + this.clientData = this.data.clientData; + this.updateGeneralData(); + this.updateNetworkData(); + this.calculateDiskUsage(); + this.loadPartitions(); + this.loading = false; + } else { + console.error('No se recibieron datos del cliente.'); + } } updateGeneralData() { @@ -143,7 +121,7 @@ export class ClientMainViewComponent implements OnInit { updateNetworkData() { this.networkData = [ { property: 'Padre', value: this.clientData?.organizationalUnit?.name || '' }, - { property: 'Pxe', value: this.clientData?.template?.name || '' }, + { property: 'Pxe', value: this.clientData?.pxeTemplate?.name || '' }, { property: 'Remote Pc', value: this.clientData.remotePc || '' }, { property: 'Subred', value: this.clientData?.subnet || '' }, { property: 'OGlive', value: this.clientData?.ogLive?.name || '' }, @@ -198,16 +176,14 @@ export class ClientMainViewComponent implements OnInit { this.isDiskUsageEmpty = this.diskUsageData.length === 0; } - onEditClick(event: MouseEvent, uuid: string): void { - event.stopPropagation(); - const dialogRef = this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' }); - dialogRef.afterClosed().subscribe(); - } - loadPartitions(): void { - this.http.get(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[diskNumber, partitionNumber]=ASC`).subscribe({ + if (!this.clientData?.id) { + return; + } + + this.http.get(`${this.baseUrl}/partitions?client.id=${this.clientData.id}&order[diskNumber, partitionNumber]=ASC`).subscribe({ next: data => { - const filteredPartitions = data['hydra:member'].filter((partition: any) => partition.partitionNumber !== 0); + const filteredPartitions = data['hydra:member']; this.dataSource = filteredPartitions; this.partitions = filteredPartitions; this.calculateDiskUsage(); @@ -218,96 +194,7 @@ export class ClientMainViewComponent implements OnInit { }); } - - loadCommands(): void { - this.http.get(`${this.baseUrl}/commands?`).subscribe({ - next: data => { - this.commands = data['hydra:member']; - }, - error: error => { - console.error('Error al obtener las particiones:', error); - } - }); - } - - onCommandSelect(action: any): void { - if (action === 'partition') { - this.openPartitionAssistant(); - } - - if (action === 'create-image') { - this.openCreateImageAssistant(); - } - - if (action === 'deploy-image') { - this.openDeployImageAssistant(); - } - - if (action === 'reboot') { - this.rebootClient(); - } - - if (action === 'power-off') { - this.powerOffClient(); - } - - if (action === 'power-on') { - this.powerOnClient(); - } - } - - rebootClient(): void { - this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/reboot`, {}).subscribe( - response => { - this.toastService.success('Cliente actualizado correctamente'); - }, - error => { - this.toastService.error('Error de conexión con el cliente'); - } - ); - } - - powerOnClient(): void { - const payload = { - client: this.clientData['@id'] - } - - this.http.post(`${this.baseUrl}${this.clientData.repository['@id']}/wol`, payload).subscribe( - response => { - this.toastService.success('Cliente actualizado correctamente'); - }, - error => { - this.toastService.error('Error de conexión con el cliente'); - } - ); - } - - powerOffClient(): void { - this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/power-off`, {}).subscribe( - response => { - this.toastService.success('Cliente actualizado correctamente'); - }, - error => { - this.toastService.error('Error de conexión con el cliente'); - } - ); - } - - openPartitionAssistant(): void { - this.router.navigate([`/clients/${this.clientData.uuid}/partition-assistant`]).then(r => { - console.log('navigated', r); - }); - } - - openCreateImageAssistant(): void { - this.router.navigate([`/clients/${this.clientData.uuid}/create-image`]).then(r => { - console.log('navigated', r); - }); - } - - openDeployImageAssistant(): void { - this.router.navigate([`/clients/deploy-image`]).then(r => { - console.log('navigated', r); - }); + onNoClick(): void { + this.dialog.closeAll(); } } diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html index 1a4d246..69f2a51 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html @@ -67,7 +67,7 @@ {{ 'templateLabel' | translate }} - + {{ template.name }} diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.ts b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.ts index 163fbcb..d38075c 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.ts @@ -86,7 +86,7 @@ export class ManageClientComponent implements OnInit { netDriver: null, mac: ['', Validators.pattern(/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/)], ip: ['', Validators.required], - template: [null], + pxeTemplate: [null], hardwareProfile: [null], ogLive: [null], repository: [null], @@ -110,7 +110,8 @@ export class ManageClientComponent implements OnInit { repository: unit.networkSettings?.repository?.['@id'], hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'], ogLive: unit.networkSettings?.ogLive?.['@id'], - menu: unit.networkSettings?.menu?.['@id'] + menu: unit.networkSettings?.menu?.['@id'], + pxeTemplate: unit.networkSettings?.pxeTemplate?.['@id'], })); const initialUnitId = this.clientForm.get('organizationalUnit')?.value; @@ -229,6 +230,7 @@ export class ManageClientComponent implements OnInit { ogLive: selectedUnit.ogLive || null, menu: selectedUnit.menu || null, netiface: selectedUnit.netiface || null, + pxeTemplate: selectedUnit.pxeTemplate || null }); } } @@ -250,7 +252,7 @@ export class ManageClientComponent implements OnInit { organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null, repository: data.repository ? data.repository['@id'] : null, ogLive: data.ogLive ? data.ogLive['@id'] : null, - template: data.template ? data.template['@id'] : null, + pxeTemplate: data.pxeTemplate ? data.pxeTemplate['@id'] : null, menu: data.menu ? data.menu['@id'] : null, maintenance: data.maintenance }); diff --git a/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html b/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html index 6cd66de..3d02ad5 100644 --- a/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html +++ b/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html @@ -22,11 +22,11 @@ school -
Disponible acceso remoto
+
{{ 'remoteAccess' | translate }}
school -
No disponible acceso remoto
+
{{ 'noRemoteAccess' | translate }}
- + \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html index dc980dd..739256b 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html @@ -1,12 +1,13 @@
-

{{ isEditMode ? 'Editar' : 'Crear' }} Unidad Organizativa

-
+

{{ isEditMode ? ('edit' | translate) : ('createButton' | translate) }} {{ + 'labelOrganizationalUnit' | translate }}

+
- - General + + {{ 'generalTabLabel' | translate }}
- Tipo + {{ 'typeLabel' | translate }} {{ typeTranslations[type] }} @@ -14,11 +15,11 @@ - Nombre + {{ 'nameColumnHeader' | translate }} - Padre + {{ 'createOrgUnitparentLabel' | translate }} {{ getSelectedParentName() }} @@ -31,7 +32,7 @@ - Descripción + {{ 'descriptionLabel' |translate }} @@ -41,22 +42,23 @@
- Información del aula + {{'classroomInfoStepLabel' | translate}}
- Localización + {{ 'locationLabel' | translate }} - Aforo + {{ 'capacityLabel' | translate }} - El aforo no puede ser negativo + {{ 'capacityWarning' | translate }} - Calendario Asociado + {{ 'associatedCalendarLabel' | translate }} {{ calendar.name }} @@ -64,13 +66,13 @@
- Proyector - Pizarra + {{ 'projectorAlt' | translate }} + {{ 'boardToggle' | translate }}
- Configuración de Red + {{ 'networkSettingsStepLabel' | translate }}
OgLive @@ -81,7 +83,15 @@ - Repositorio + {{ 'templateLabel' | translate }} + + + {{ template.name }} + + + + + {{ 'repositoryLabel' | translate }} {{ repository.name }} @@ -97,17 +107,17 @@ - Máscara de Red + {{ 'netmaskLabel' | translate }} - - Interfaz de red - - - {{ type.name }} - - - + + {{ 'netifaceLabel' | translate }} + + + {{ type.name }} + + + Router @@ -117,7 +127,7 @@ - Modo P2P + {{ 'p2pModeLabel' | translate }} {{ option.name }} @@ -125,23 +135,23 @@ - Tiempo P2P + {{ 'p2pTimeLabel' | translate }} - Mcast IP + {{ 'mcastIpLabel' | translate }} - Mcast Speed + {{ 'mcastSpeedLabel' | translate }} - Mcast Port + {{ 'mcastPortLabel' | translate }} - Mcast Mode + {{ 'mcastModeLabel' | translate }} {{ option.name }} @@ -149,7 +159,7 @@ - Menu + {{ 'menuLabel' | translate }} {{ menu.name }} @@ -157,28 +167,28 @@ - Perfil de Hardware + {{ 'hardwareProfileLabel' | translate }} {{ unit.description }} - Formato de URL incorrecto + {{ 'urlFormatError' | translate }}
- Información Adicional + {{ 'additionalInfoStepLabel' | translate }}
- Comentarios + {{ 'commentsLabel' | translate }}
- + + isEditMode ? ('edit' | translate) : ('createButton' | translate) }}
-
+
\ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts index 87c49df..1ae3a86 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts @@ -30,6 +30,7 @@ export class ManageOrganizationalUnitComponent implements OnInit { isEditMode: boolean; currentCalendar: any = []; ogLives: any[] = []; + pxeTemplates: any[] = []; menus: any[] = []; repositories: any[] = []; parentUnitsWithPaths: { id: string, name: string, path: string }[] = []; @@ -77,6 +78,7 @@ export class ManageOrganizationalUnitComponent implements OnInit { this.networkSettingsFormGroup = this._formBuilder.group({ ogLive: [null], repository: [null], + pxeTemplate: [null], proxy: [null], dns: [null], netmask: [null], @@ -111,6 +113,7 @@ export class ManageOrganizationalUnitComponent implements OnInit { this.loadOgLives(), this.loadRepositories(), this.loadMenus(), + this.loadPxeTemplates() ]; Promise.all(observables).then(() => { @@ -144,6 +147,7 @@ export class ManageOrganizationalUnitComponent implements OnInit { repository: unit.networkSettings?.repository?.['@id'], hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'], ogLive: unit.networkSettings?.ogLive?.['@id'], + pxeTemplate: unit.networkSettings?.pxeTemplate?.['@id'], menu: unit.networkSettings?.menu?.['@id'], mcastIp: unit.networkSettings?.mcastIp, mcastSpeed: unit.networkSettings?.mcastSpeed, @@ -183,6 +187,7 @@ export class ManageOrganizationalUnitComponent implements OnInit { repository: selectedUnit.repository || null, hardwareProfile: selectedUnit.hardwareProfile || null, ogLive: selectedUnit.ogLive || null, + pxeTemplate: selectedUnit.pxeTemplate || null, menu: selectedUnit.menu || null, mcastIp: selectedUnit.mcastIp || null, mcastSpeed: selectedUnit.mcastSpeed || null, @@ -219,6 +224,23 @@ export class ManageOrganizationalUnitComponent implements OnInit { }); } + loadPxeTemplates(): Promise { + return new Promise((resolve, reject) => { + const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`; + + this.http.get(url).subscribe( + response => { + this.pxeTemplates = response['hydra:member']; + resolve(); + }, + error => { + console.error('Error fetching pxe templates:', error); + reject(error); + } + ); + }); + } + loadOgLives(): Promise { return new Promise((resolve, reject) => { const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`; @@ -287,22 +309,6 @@ export class ManageOrganizationalUnitComponent implements OnInit { }); } - loadCurrentCalendar(uuid: string): void { - this.loading = true; - const apiUrl = `${this.baseUrl}/remote-calendars/${uuid}`; - this.http.get(apiUrl).subscribe( - response => { - this.currentCalendar = response; - this.loading = false; - }, - error => { - console.error('Error loading current calendar', error); - this.toastService.error('Error loading current calendar'); - this.loading = false; - } - ); - } - onCalendarChange(event: any) { this.generalFormGroup.value.remoteCalendar = event.value; } @@ -315,6 +321,10 @@ export class ManageOrganizationalUnitComponent implements OnInit { this.networkSettingsFormGroup.value.repository = event.value; } + onPxeTemplateChange(event: any) { + this.networkSettingsFormGroup.value.pxeTemplate = event.value; + } + loadData(uuid: string): Promise { return new Promise((resolve, reject) => { const url = `${this.baseUrl}/organizational-units/${uuid}`; @@ -347,7 +357,8 @@ export class ManageOrganizationalUnitComponent implements OnInit { menu: data.networkSettings?.menu ? data.networkSettings.menu['@id'] : null, hardwareProfile: data.networkSettings?.hardwareProfile ? data.networkSettings.hardwareProfile['@id'] : null, ogLive: data.networkSettings?.ogLive ? data.networkSettings.ogLive['@id'] : null, - repository: data.networkSettings?.repository ? data.networkSettings.repository['@id'] : null + repository: data.networkSettings?.repository ? data.networkSettings.repository['@id'] : null, + pxeTemplate: data.networkSettings?.pxeTemplate ? data.networkSettings.pxeTemplate['@id'] : null }); this.classroomInfoFormGroup.patchValue({ location: data.location, diff --git a/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.css b/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.css new file mode 100644 index 0000000..8172cde --- /dev/null +++ b/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.css @@ -0,0 +1,55 @@ +.modal-content { + max-height: 80vh; + overflow-y: auto; + background-color: #fff; + display: flex; + flex-direction: column; + padding: 1em 4em 2em 4em; + box-sizing: border-box; +} + +.client-section { + margin-bottom: 2em; +} + +.client-title { + font-size: 1.5em; + font-weight: 500; + color: #3f51b5; + margin-bottom: 1em; + border-bottom: 2px solid #e0e0e0; + padding-bottom: 0.25em; +} + +.partition-table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; +} + +.partition-table th, +.partition-table td { + padding: 0.75em 1em; + text-align: left; + font-size: 0.95em; + border-bottom: 1px solid #ddd; +} + +.partition-table th { + background-color: #ebebeb; + font-weight: 500; + color: #252525; +} + +.partition-table tr:hover td { + background-color: #f9f9f9; +} + +.summary-row { + font-weight: 500; + background-color: #ebebeb; +} + +.partition-table tr:hover td { + background-color: unset; +} \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.html b/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.html new file mode 100644 index 0000000..25c1fe7 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.html @@ -0,0 +1,47 @@ + +
+

+ {{ group.clientNames.length === 1 ? group.clientNames[0] : group.clientNames.join(', ') }} +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Disco{{ element.diskNumber }}Partición{{ element.partitionNumber }}Tipo{{ element.partitionCode }}Tamaño (MB){{ element.size | number }}File System + {{ element.filesystem || '-' }} + Resumen +
+
+ + +

Sin particiones disponibles.

+
+
+
\ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.spec.ts b/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.spec.ts new file mode 100644 index 0000000..aba862d --- /dev/null +++ b/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.spec.ts @@ -0,0 +1,42 @@ +import { TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { PartitionTypeOrganizatorComponent } from './partition-type-organizator.component'; +import { MatTableModule } from '@angular/material/table'; + +describe('PartitionTypeOrganizatorComponent', () => { + let component: PartitionTypeOrganizatorComponent; + let fixture: any; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PartitionTypeOrganizatorComponent], + imports: [ + MatDialogModule, + MatTableModule + ], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { + provide: MAT_DIALOG_DATA, + useValue: [ + { + name: 'Client 1', + partitions: [ + { diskNumber: 1, partitionNumber: 1, partitionCode: 'EXT4', size: 1024, filesystem: 'ext4', memoryUsage: 50 }, + { diskNumber: 1, partitionNumber: 2, partitionCode: 'NTFS', size: 2048, filesystem: 'ntfs', memoryUsage: 75 } + ] + } + ] + } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(PartitionTypeOrganizatorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.ts b/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.ts new file mode 100644 index 0000000..99337c6 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/shared/partition-type-organizator/partition-type-organizator.component.ts @@ -0,0 +1,97 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; + +interface Partition { + diskNumber: number; + partitionNumber: number; + partitionCode: string; + size: number; + filesystem: string | null; + isSummary?: boolean; +} + +interface SimplifiedClient { + name: string; + partitions: Partition[]; +} + +interface GroupedClientPartitions { + clientNames: string[]; + partitions: Partition[]; +} + +@Component({ + selector: 'app-partition-type-organizator', + templateUrl: './partition-type-organizator.component.html', + styleUrl: './partition-type-organizator.component.css' +}) +export class PartitionTypeOrganizatorComponent implements OnInit { + displayedColumns: string[] = ['diskNumber', 'partitionNumber', 'partitionCode', 'size', 'filesystem']; + groupedPartitions: GroupedClientPartitions[] = []; + + constructor(@Inject(MAT_DIALOG_DATA) public data: SimplifiedClient[]) { } + + ngOnInit(): void { + const simplifiedClients = this.simplifyClients(this.data); + this.groupedPartitions = this.groupClientsByPartitions(simplifiedClients); + } + + private simplifyClients(clients: SimplifiedClient[]): SimplifiedClient[] { + return clients.map(client => { + const partitionZero = client.partitions.find(p => p.partitionNumber === 0); + const otherPartitions = client.partitions.filter(p => p.partitionNumber !== 0); + + const simplifiedPartitions: Partition[] = otherPartitions.map(p => ({ + diskNumber: p.diskNumber, + partitionNumber: p.partitionNumber, + partitionCode: p.partitionCode, + size: p.size, + filesystem: p.filesystem, + isSummary: false + })); + + if (partitionZero) { + simplifiedPartitions.push({ + diskNumber: partitionZero.diskNumber, + partitionNumber: partitionZero.partitionNumber, + partitionCode: partitionZero.partitionCode, + size: partitionZero.size, + filesystem: null, + isSummary: true + }); + } + + return { + name: client.name, + partitions: simplifiedPartitions + }; + }); + } + + private groupClientsByPartitions(clients: SimplifiedClient[]): GroupedClientPartitions[] { + const groups: GroupedClientPartitions[] = []; + + clients.forEach(client => { + const normalizedPartitions = this.normalizePartitions(client.partitions); + + const existingGroup = groups.find(group => + JSON.stringify(this.normalizePartitions(group.partitions)) === JSON.stringify(normalizedPartitions) + ); + + if (existingGroup) { + existingGroup.clientNames.push(client.name); + } else { + groups.push({ + clientNames: [client.name], + partitions: client.partitions + }); + } + }); + + return groups; + } + + private normalizePartitions(partitions: Partition[]): Partition[] { + return [...partitions].sort((a, b) => a.partitionNumber - b.partitionNumber); + } +} diff --git a/ogWebconsole/src/app/components/images/create-image/create-image.component.html b/ogWebconsole/src/app/components/images/create-image/create-image.component.html index daf3d74..7db7a37 100644 --- a/ogWebconsole/src/app/components/images/create-image/create-image.component.html +++ b/ogWebconsole/src/app/components/images/create-image/create-image.component.html @@ -69,4 +69,4 @@ - \ No newline at end of file + diff --git a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.css b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.css index d2ea8a8..f4ae6a5 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.css +++ b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.css @@ -51,21 +51,3 @@ .mat-elevation-z8 { box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2); } - -.example-headers-align .mat-expansion-panel-header-description { - justify-content: space-between; - align-items: center; -} - -.example-headers-align .mat-mdc-form-field+.mat-mdc-form-field { - margin-left: 8px; -} - -.example-button-row { - display: table-cell; - max-width: 600px; -} - -.example-button-row .mat-mdc-button-base { - margin: 8px 8px 8px 0; -} \ No newline at end of file diff --git a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html index f8caba7..fb12d46 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html +++ b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html @@ -37,7 +37,7 @@ - {{ 'idColumnHeader' | translate }} @@ -50,12 +50,12 @@ - {{ 'nameColumnHeader' | translate }} + {{ 'ipLabel' | translate }} {{ element.ip }} - {{ 'nameColumnHeader' | translate }} + {{ 'macLabel' | translate }} {{ element.mac }} @@ -77,4 +77,4 @@ - \ No newline at end of file + diff --git a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts index 95be23e..b2413ce 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts @@ -4,6 +4,7 @@ import { HttpClient } from '@angular/common/http'; import { ToastrService } from 'ngx-toastr'; import { ConfigService } from '@services/config.service'; import { JoyrideService } from 'ngx-joyride'; +import {MatTableDataSource} from "@angular/material/table"; @Component({ selector: 'app-pxe-boot-files', @@ -15,7 +16,7 @@ export class PxeBootFilesComponent implements OnInit { availableOrganizationalUnits: any[] = []; selectedUnitChildren: any[] = []; - dataSource: any[] = []; + dataSource = new MatTableDataSource(); taskForm: FormGroup; units: any[] = []; ogLiveOptions: any[] = []; @@ -72,14 +73,14 @@ export class PxeBootFilesComponent implements OnInit { loadChildUnits(event: any) { this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${event.value.id}`).subscribe( response => { - this.dataSource = response['hydra:member']; + this.dataSource.data = response['hydra:member']; }, error => console.error('Error fetching child units:', error) ); } applyToAll(): void { - this.dataSource = this.dataSource.map(client => ({ + this.dataSource.data = this.dataSource.data.map(client => ({ ...client, ogLive: this.globalOgLive || client.ogLive })); @@ -88,7 +89,7 @@ export class PxeBootFilesComponent implements OnInit { saveOgLiveTemplates(): void { const groupedByTemplate: { [key: string]: string[] } = {}; - this.dataSource.forEach(client => { + this.dataSource.data.forEach(client => { if (client.ogLive) { if (!groupedByTemplate[client.ogLive]) { groupedByTemplate[client.ogLive] = []; @@ -107,10 +108,10 @@ export class PxeBootFilesComponent implements OnInit { this.http.post(url, payload).subscribe({ next: () => { - this.toastService.success(`Clientes guardados correctamente para la plantilla ${templateId}`); + this.toastService.success(`Clientes guardados correctamente para la plantilla`); }, error: () => { - this.toastService.error(`Error al guardar clientes para la plantilla ${templateId}`); + this.toastService.error(`Error al guardar clientes para la plantilla`); } }); }); diff --git a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html index bf13061..6f86ccf 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html +++ b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html @@ -8,7 +8,7 @@
- + - diff --git a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.ts b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.ts index f627fba..6d8dfb5 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.ts @@ -25,8 +25,8 @@ export class PXEimagesComponent implements OnInit { images: { downloadUrl: string; name: string; uuid: string }[] = []; dataSource = new MatTableDataSource(); length: number = 0; - itemsPerPage: number = 10; - page: number = 1; + itemsPerPage: number = 20; + page: number = 0; pageSizeOptions: number[] = [5, 10, 20, 40, 100]; selectedElements: string[] = []; loading: boolean = false; @@ -94,12 +94,15 @@ export class PXEimagesComponent implements OnInit { } search(): void { - this.dataService.getImages(this.filters).subscribe( + this.loading = true; + this.http.get(`${this.apiUrl}?page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe( data => { - this.dataSource.data = data; + this.dataSource.data = data['hydra:member']; + this.length = data['hydra:totalItems']; + this.loading = false; }, error => { - console.error('Error fetching og lives', error); + this.loading = false; } ); } @@ -160,19 +163,6 @@ export class PXEimagesComponent implements OnInit { } } - editImage(image: any): void { - const dialogRef = this.dialog.open(CreatePXEImageComponent, { - width: '700px', - data: image - }); - - dialogRef.afterClosed().subscribe(result => { - if (result) { - this.search(); - } - }); - } - deleteImage(image: any): void { const dialogRef = this.dialog.open(DeleteModalComponent, { width: '400px', @@ -203,22 +193,11 @@ export class PXEimagesComponent implements OnInit { const dialogRef = this.dialog.open(InfoImageComponent, { data: { data }, width: '700px' }); } - applyFilter() { - this.http.get(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({ - next: (response) => { - this.dataSource.data = response['hydra:member']; - this.length = response['hydra:totalItems']; - }, - error: (error) => { - console.error('Error al cargar las imágenes:', error); - } - }); - } - - onPageChange(event: PageEvent) { + onPageChange(event: any): void { this.page = event.pageIndex; this.itemsPerPage = event.pageSize; - this.applyFilter(); + this.length = event.length; + this.search(); } loadAlert(): Observable { @@ -248,6 +227,7 @@ export class PXEimagesComponent implements OnInit { this.joyrideService.startTour({ steps: [ 'titleStep', + 'viewInfoStep', 'addImageStep', 'searchNameStep', 'searchDefaultImageStep', diff --git a/ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.html b/ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.html deleted file mode 100644 index 75a76cc..0000000 --- a/ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.html +++ /dev/null @@ -1,43 +0,0 @@ -

{{ isEditMode ? ('editTemplateTitle' | translate) : ('addTemplateTitle' | translate) }}

- - -
-
- - {{ 'templateNameLabel' | translate }} - - - {{ 'templateNameError' | translate }} - - - - - {{ 'templateContentLabel' | translate }} - - - {{ 'templateContentError' | translate }} - - -
-
-
- - -
- - - - - - -
- - -
-
-
\ No newline at end of file diff --git a/ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.css b/ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.css similarity index 52% rename from ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.css rename to ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.css index 65a0a2c..af4ebe4 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.css +++ b/ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.css @@ -1,6 +1,3 @@ -mat-form-field { - width: 100%; -} pre { background-color: #eceff1; @@ -12,41 +9,17 @@ pre { color: #333; } -mat-dialog-actions { - margin-top: 20px; + +.dialog-content { display: flex; - justify-content: flex-end; + flex-direction: column; + padding: 40px; } -button { - margin-left: 10px; -} - -button[type="submit"] { - background-color: #3f51b5; - color: #fff; -} - -button[type="submit"]:disabled { - background-color: #c5cae9; -} - -h2 { - margin-bottom: 20px; - font-size: 1.5rem; - color: #000000; - text-align: center; -} - -h3 { - margin-top: 30px; - font-size: 1.2rem; - color: #000000; -} - -.spacing-container { - margin-top: 20px; - margin-bottom: 16px; +.pxe-form { + width: 100%; + display: flex; + flex-direction: column; } .list-item-content { @@ -56,6 +29,11 @@ h3 { width: 100%; } +.form-field { + width: 100%; + margin-top: 16px; +} + .text-content { flex-grow: 1; margin-right: 16px; @@ -72,13 +50,11 @@ h3 { cursor: pointer; } -.actions-container { +.action-container { display: flex; - justify-content: space-between; - width: 100%; - align-items: center; - margin-bottom: 1rem; - padding-right: 1rem; + justify-content: flex-end; + gap: 1em; + padding: 1.5em; } .action-buttons { diff --git a/ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.html b/ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.html new file mode 100644 index 0000000..f9b4836 --- /dev/null +++ b/ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.html @@ -0,0 +1,44 @@ +

{{ isEditMode ? ('editTemplateTitle' | translate) : ('addTemplateTitle' | translate) }}

+ + +
+ + {{ 'templateNameLabel' | translate }} + + + {{ 'templateNameError' | translate }} + + + + + {{ 'templateContent' | translate }} + + + {{ 'templateContentError' | translate }} + + + + + {{ 'isDefaultLabel' | translate }} + + +
+
+ +
+ + + + + + +
+ + +
+
diff --git a/ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.ts b/ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.ts similarity index 61% rename from ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.ts rename to ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.ts index e2f7ee6..ae7c9d1 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.ts @@ -20,35 +20,35 @@ export class CreatePxeTemplateComponent implements OnInit { 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 + 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`, + :fallback + set ISODIR ogLive + kernel http://__SERVERIP__/tftpboot/\${ISODIR}/ogvmlinuz \${kernelargs} + initrd http://__SERVERIP__/tftpboot/\${ISODIR}/oginitrd.img + boot`, - disco: `#!ipxe + disco: `#!ipxe -iseq \${platform} efi && goto uefi_boot || goto bios_boot + 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 + :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` + :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( @@ -72,7 +72,8 @@ exit` this.templateForm = this.fb.group({ name: [this.data?.name || '', Validators.required], - templateContent: [this.data?.templateContent || '', Validators.required] + templateContent: [this.data?.templateContent || '', Validators.required], + isDefault: [this.data?.isDefault || false] }); } @@ -99,7 +100,8 @@ exit` const formValues = this.templateForm.value; const payload = { name: formValues.name, - templateContent: formValues.templateContent + templateContent: formValues.templateContent, + isDefault: formValues.isDefault, }; this.http.post(`${this.baseUrl}/pxe-templates`, payload).subscribe({ @@ -117,7 +119,8 @@ exit` const formValues = this.templateForm.value; const payload = { name: formValues.name, - templateContent: formValues.templateContent + templateContent: formValues.templateContent, + isDefault: formValues.isDefault, }; this.http.patch(`${this.baseUrl}/pxe-templates/${this.data.uuid}`, payload).subscribe({ @@ -138,42 +141,6 @@ exit` 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( - () => { - this.toastService.success('Clientes asignados correctamente'); - }, - error => { - this.toastService.error(error.error['hydra:description']); - } - ); - } - - deleteClient(client: any): void { - const dialogRef = this.dialog.open(DeleteModalComponent, { - width: '300px', - data: { name: client.name } - }); - - dialogRef.afterClosed().subscribe(result => { - if (result) { - this.http.post(`${this.baseUrl}/pxe-templates/${this.data.uuid}/delete-client`, { client: client['@id'] }).subscribe({ - next: () => { - this.toastService.success('Cliente eliminado exitosamente'); - this.dialogRef.close(); - }, - error: error => { - this.toastService.error(error.error['hydra:description']); - } - }); - } - }); - } - onCancel(): void { this.dialogRef.close(false); } diff --git a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html index a6cb871..02fd939 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html +++ b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html @@ -1,5 +1,5 @@
-
@@ -7,7 +7,7 @@ translate }}
- +
@@ -22,10 +22,10 @@ search {{ 'searchHint' | translate }} - - {{ 'createdInOgbootLabel' | translate }} - + {{ 'isDefaultLabel' | translate }} + {{ 'allOption' | translate }} {{ 'yesOption' | translate }} @@ -36,16 +36,31 @@ + text="{{ 'tableDatePxeTemplateText' | translate }}"> @@ -60,7 +75,7 @@ - diff --git a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts index 55cb6ad..9c35f3a 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts @@ -1,9 +1,8 @@ import { HttpClient } from '@angular/common/http'; import {Component, OnInit} from '@angular/core'; -import { CreatePxeTemplateComponent } from './create-pxeTemplate/create-pxe-template.component'; +import { CreatePxeTemplateComponent } from './manage-pxeTemplate/create-pxe-template.component'; import { MatDialog } from '@angular/material/dialog'; import { MatTableDataSource } from '@angular/material/table'; -import { PageEvent } from '@angular/material/paginator'; import { ToastrService } from 'ngx-toastr'; import { DatePipe } from '@angular/common'; import { DataService } from './data.service'; @@ -26,8 +25,8 @@ export class PxeComponent implements OnInit{ currentPage: number = 1; dataSource = new MatTableDataSource(); length: number = 0; - itemsPerPage: number = 10; - page: number = 1; + itemsPerPage: number = 20; + page: number = 0; pageSizeOptions: number[] = [5, 10, 20, 40, 100]; selectedElements: string[] = []; loading: boolean = false; @@ -45,17 +44,22 @@ export class PxeComponent implements OnInit{ { columnDef: 'name', header: 'Nombre de la plantilla', - cell: (user: any) => `${user.name}` + cell: (user: any) => user.name }, { columnDef: 'synchronized', header: 'Sincronizado', - cell: (user: any) => `${user.synchronized}` + cell: (user: any) => user.synchronized + }, + { + columnDef: 'isDefault', + header: 'Plantilla por defecto', + cell: (user: any) => user.isDefault }, { columnDef: 'createdAt', header: 'Fecha de creación', - cell: (user: any) => `${this.datePipe.transform(user.createdAt, 'dd/MM/yyyy hh:mm:ss')}` + cell: (user: any) => this.datePipe.transform(user.createdAt, 'dd/MM/yyyy hh:mm:ss') } ]; displayedColumns = [...this.columns.map(column => column.columnDef), 'actions']; @@ -80,12 +84,15 @@ export class PxeComponent implements OnInit{ } search(): void { - this.dataService.getPxeTemplates(this.filters).subscribe( + this.loading = true; + this.http.get(`${this.apiUrl}?page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe( data => { - this.dataSource.data = data; + this.dataSource.data = data['hydra:member']; + this.length = data['hydra:totalItems']; + this.loading = false; }, error => { - console.error('Error fetching pxe templates', error); + this.loading = false; } ); } @@ -100,10 +107,9 @@ export class PxeComponent implements OnInit{ }); } - editPxeTemplate(template: any) { const dialogRef = this.dialog.open(CreatePxeTemplateComponent, { - data: template, // Pasa los datos del template para edición + data: template, width: '800px' }); @@ -128,7 +134,6 @@ export class PxeComponent implements OnInit{ this.search(); }, error: (error) => { - console.error('Error al eliminar la subred', error); this.toastService.error(error.error['hydra:description']); } }); @@ -143,19 +148,7 @@ export class PxeComponent implements OnInit{ showTemplate(event: MouseEvent, data: any): void { event.stopPropagation(); - const dialogRef = this.dialog.open(ShowTemplateContentComponent, { data: { data }, width: '700px' }); - } - - applyFilter() { - this.http.get(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({ - next: (response) => { - this.dataSource.data = response['hydra:member']; - this.length = response['hydra:totalItems']; - }, - error: (error) => { - console.error('Error al cargar las imágenes:', error); - } - }); + const dialogRef = this.dialog.open(ShowTemplateContentComponent, { data: { data }, width: '800px' }); } loadAlert(): Observable { @@ -181,20 +174,21 @@ export class PxeComponent implements OnInit{ ); } - onPageChange(event: PageEvent) { + onPageChange(event: any): void { this.page = event.pageIndex; this.itemsPerPage = event.pageSize; - this.applyFilter(); + this.length = event.length; + this.search(); } - iniciarTour(): void { + initTour(): void { this.joyrideService.startTour({ steps: [ - 'serverInfoStep', 'titleStep', + 'viewInfoStep', 'addTemplateStep', 'searchNameStep', - 'searchSyncStep', + 'searchIsDefaultStep', 'tableStep', 'actionsStep', 'paginationStep' diff --git a/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.css b/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.css index d01dcc9..b314670 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.css +++ b/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.css @@ -26,8 +26,15 @@ background-color: #f5f5f5; padding: 16px; border-radius: 4px; - font-size: 12px; + font-size: 14px; overflow-x: auto; white-space: pre; border: 1px solid #dcdcdc; } + +.action-container { + display: flex; + justify-content: flex-end; + gap: 1em; + padding: 1.5em; +} diff --git a/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.html b/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.html index 52d8e53..2724d18 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.html +++ b/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.html @@ -1,4 +1,10 @@ -
-

{{ 'detailsTitle' | translate: { name: data.data.name } }}

-
{{ data.data.templateContent }}
+ +
+

{{ 'detailsTitle' | translate: { name: data.data.name } }}

+
{{ data.data.templateContent }}
+
+
+ +
+
diff --git a/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.ts b/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.ts index 5ae59ac..ed2fb3b 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe/show-template-content/show-template-content.component.ts @@ -1,5 +1,5 @@ import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA } from "@angular/material/dialog"; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import { HttpClient } from "@angular/common/http"; import { ConfigService } from "@services/config.service"; @@ -15,8 +15,13 @@ export class ShowTemplateContentComponent { constructor( @Inject(MAT_DIALOG_DATA) public data: any, private http: HttpClient, - private configService: ConfigService + private configService: ConfigService, + public dialogRef: MatDialogRef ) { this.baseUrl = this.configService.apiUrl; } + + close(): void { + this.dialogRef.close(); + } } diff --git a/ogWebconsole/src/app/components/repositories/repositories.component.css b/ogWebconsole/src/app/components/repositories/repositories.component.css index 75b57f7..62feff0 100644 --- a/ogWebconsole/src/app/components/repositories/repositories.component.css +++ b/ogWebconsole/src/app/components/repositories/repositories.component.css @@ -79,25 +79,6 @@ table { margin-bottom: 30px; } -.example-headers-align .mat-expansion-panel-header-description { - justify-content: space-between; - align-items: center; -} - -.example-headers-align .mat-mdc-form-field+.mat-mdc-form-field { - margin-left: 8px; -} - -.example-button-row { - display: table-cell; - max-width: 600px; -} - -.example-button-row .mat-mdc-button-base { - margin: 8px 8px 8px 0; - -} - .header-container-title { flex-grow: 1; text-align: left; diff --git a/ogWebconsole/src/app/components/repositories/repositories.component.html b/ogWebconsole/src/app/components/repositories/repositories.component.html index 3ddf7ec..ab330e5 100644 --- a/ogWebconsole/src/app/components/repositories/repositories.component.html +++ b/ogWebconsole/src/app/components/repositories/repositories.component.html @@ -1,31 +1,33 @@
-
-

+

{{ 'repositoryTitle' | translate }}

- +
-
+
- Buscar nombre de repositorio + {{ 'search' | translate }} {{ 'repository' | translate }} search Pulsar 'enter' para buscar - Buscar IP de repositorio + {{ 'search' | translate }} IP de {{ 'repository' | translate }} search @@ -33,7 +35,7 @@
-
{{ column.header }} - - {{ image[column.columnDef] ? 'check_circle' : 'cancel' }} - + + + {{ 'yesOption' | translate }} + + + {{ 'noOption' | translate }} + + - + + + + {{ 'yesOption' | translate }} + + + {{ 'noOption' | translate }} + + + + {{ column.cell(image) }}
+
- - +
{{ column.header }} @@ -42,15 +44,30 @@ - - + + Acciones + {{ 'actions' | translate }}
-
+
diff --git a/ogWebconsole/src/app/components/repositories/repositories.component.ts b/ogWebconsole/src/app/components/repositories/repositories.component.ts index 4a398de..1c2dd73 100644 --- a/ogWebconsole/src/app/components/repositories/repositories.component.ts +++ b/ogWebconsole/src/app/components/repositories/repositories.component.ts @@ -23,7 +23,7 @@ export class RepositoriesComponent implements OnInit { private apiUrl: string; dataSource = new MatTableDataSource(); length: number = 0; - itemsPerPage: number = 10; + itemsPerPage: number = 20; page: number = 0; loading: boolean = false; filters: { [key: string]: string } = {}; @@ -37,7 +37,7 @@ export class RepositoriesComponent implements OnInit { { columnDef: 'name', header: 'Nombre de repositorio', - cell: (repository: any) => `${repository.name}` + cell: (repository: any) => repository.name }, { columnDef: 'user', @@ -47,12 +47,12 @@ export class RepositoriesComponent implements OnInit { { columnDef: 'ip', header: 'Ip', - cell: (repository: any) => `${repository.ip}` + cell: (repository: any) => repository.ip }, { columnDef: 'images', - header: 'Imágenes', - cell: (repository: any) => `${repository.images}` + header: 'Gestionar imágenes', + cell: (repository: any) => repository.images }, { columnDef: 'createdAt', @@ -60,7 +60,7 @@ export class RepositoriesComponent implements OnInit { cell: (repository: any) => `${this.datePipe.transform(repository.createdAt, 'dd/MM/yyyy hh:mm:ss')}` } ]; - isGitModuleInstalled: boolean = false; + isGitModuleInstalled: boolean = true; displayedColumns: string[] = ['id', 'name', 'ip', 'user', 'images', 'createdAt', 'actions']; constructor( @@ -166,11 +166,17 @@ export class RepositoriesComponent implements OnInit { this.search(); } - iniciarTour(): void { + initTour(): void { this.joyrideService.startTour({ steps: [ 'repositoryTitleStep', 'addStep', + 'searchStep', + 'tableDateStep', + 'monolithicImageStep', + 'gitImageStep', + 'actionsStep', + 'paginationStep' ], showPrevButton: true, themeColor: '#3f51b5' diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.html b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.html index 5b0d65d..e591807 100644 --- a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.html +++ b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.html @@ -10,13 +10,10 @@
- - + -
@@ -48,10 +45,10 @@ {{ column.header }} - - - {{ image.image[column.columnDef] ? 'check_circle' : 'cancel' }} - + + + {{ image.isGlobal ? 'Sí' : 'No' }} + Acciones - + + @@ -86,21 +85,16 @@ menu - + - + (click)="toggleAction(image, 'show-branches')">Ver ramas - - @@ -117,4 +111,4 @@ - \ No newline at end of file + diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts index 2608b8a..45728b9 100644 --- a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts +++ b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts @@ -10,11 +10,9 @@ import {Router} from "@angular/router"; import {Observable} from "rxjs"; import {ServerInfoDialogComponent} from "../../ogdhcp/server-info-dialog/server-info-dialog.component"; import {ImportImageComponent} from "../import-image/import-image.component"; -import {ConvertImageComponent} from "../convert-image/convert-image.component"; import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component"; import {ExportImageComponent} from "../../images/export-image/export-image.component"; import {BackupImageComponent} from "../backup-image/backup-image.component"; -import {ConvertImageToVirtualComponent} from "../convert-image-to-virtual/convert-image-to-virtual.component"; import {EditImageComponent} from "../edit-image/edit-image.component"; @Component({ @@ -22,7 +20,7 @@ import {EditImageComponent} from "../edit-image/edit-image.component"; templateUrl: './show-git-images.component.html', styleUrl: './show-git-images.component.css' }) -export class ShowGitImagesComponent { +export class ShowGitImagesComponent implements OnInit{ baseUrl: string; private apiUrl: string; dataSource = new MatTableDataSource(); @@ -41,29 +39,34 @@ baseUrl: string; cell: (image: any) => `${image.id}` }, { - columnDef: 'name', - header: 'Nombre de imagen', - cell: (image: any) => `${image.image.name}` + columnDef: 'repositoryName', + header: 'Nombre del repositorio', + cell: (image: any) => image.image?.name }, { - columnDef: 'version', - header: 'Version', - cell: (image: any) => `${image.version ? image.version : '0'}` + columnDef: 'name', + header: 'Nombre de imagen', + cell: (image: any) => image.name + }, + { + columnDef: 'tag', + header: 'Tag', + cell: (image: any) => image.tag }, { columnDef: 'isGlobal', header: 'Imagen global', - cell: (image: any) => `${image.image?.isGlobal}` + cell: (image: any) => image.image?.isGlobal }, { columnDef: 'status', header: 'Estado', - cell: (image: any) => `${image.status}` + cell: (image: any) => image.status }, { columnDef: 'description', header: 'Descripción', - cell: (image: any) => `${image.description ? image.description : 'Sin descripción'}` + cell: (image: any) => image.description }, { columnDef: 'createdAt', @@ -84,11 +87,10 @@ baseUrl: string; @Inject(MAT_DIALOG_DATA) public data: any ) { this.baseUrl = this.configService.apiUrl; - this.apiUrl = `${this.baseUrl}/image-image-repositories`; + this.apiUrl = `${this.baseUrl}/git-image-repositories`; } ngOnInit(): void { - console.error() if (this.data) { this.loadData(); } @@ -140,30 +142,7 @@ baseUrl: string; return this.http.get(`${this.apiUrl}/server/${image.uuid}/get`, {}); } - showImageInfo(event: MouseEvent, image:any) { - event.stopPropagation(); - this.loading = true; - this.loadImageAlert(image).subscribe( - response => { - this.alertMessage = response; - - this.dialog.open(ServerInfoDialogComponent, { - width: '800px', - data: { - message: this.alertMessage - } - }); - this.loading = false; - }, - error => { - this.toastService.error(error.error['hydra:description']); - this.loading = false; - } - ); - } - importImage(): void { - console.log(this.data) this.dialog.open(ImportImageComponent, { width: '600px', data: { @@ -177,33 +156,8 @@ baseUrl: string; }); } - convertImage(): void { - this.dialog.open(ConvertImageComponent, { - width: '600px', - data: { - repositoryUuid: this.data.repositoryUuid, - name: this.data.repositoryName - } - }).afterClosed().subscribe((result) => { - if (result) { - this.loadData(); - } - }); - } - toggleAction(image: any, action:string): void { switch (action) { - case 'get-aux': - this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/create-aux-files`, {}).subscribe({ - next: (message) => { - this.toastService.success('Petición de creación de archivos auxiliares enviada'); - this.loadData() - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - } - }); - break; case 'delete-trash': if (!image.imageFullsum) { const dialogRef = this.dialog.open(DeleteModalComponent, { @@ -278,17 +232,6 @@ baseUrl: string; } }); break; - case 'status': - this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/status`, {}).subscribe({ - next: (response: any) => { - this.toastService.info(response?.output); - this.loadData() - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - } - }); - break; case 'transfer': this.http.get(`${this.baseUrl}${image.image['@id']}`).subscribe({ next: (response) => { @@ -335,17 +278,15 @@ baseUrl: string; } }); break; - case 'convert-image-to-virtual': - this.http.get(`${this.baseUrl}${image.image['@id']}`).subscribe({ + case 'show-tags': + this.http.get(`${this.baseUrl}/git-image-repositories/server/${image.uuid}/get-tags`, {}).subscribe({ next: (response) => { - this.dialog.open(ConvertImageToVirtualComponent, { - width: '600px', + this.dialog.open(ServerInfoDialogComponent, { + width: '800px', data: { - image: response, - imageImageRepository: image + repositories: response } }); - this.router.navigate(['/commands-logs']); }, error: (error) => { this.toastService.error(error.error['hydra:description']); @@ -376,11 +317,11 @@ baseUrl: string; } loadAlert(): Observable { - return this.http.post(`${this.baseUrl}/image-repositories/server/${this.data.repositoryUuid}/get-collection`, {}); + return this.http.post(`${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/get-collection`, {}); } syncRepository() { - this.http.post(`${this.baseUrl}/image-repositories/server/${this.data.repositoryUuid}/sync`, {}) + this.http.post(`${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/sync`, {}) .subscribe(response => { this.toastService.success('Sincronización completada'); this.loadData() @@ -393,12 +334,12 @@ baseUrl: string; openImageInfoDialog() { this.loadAlert().subscribe( response => { - this.alertMessage = response.output; + this.alertMessage = response.repositories; this.dialog.open(ServerInfoDialogComponent, { width: '800px', data: { - message: this.alertMessage + repositories: this.alertMessage } }); }, @@ -408,6 +349,10 @@ baseUrl: string; ); } + goToPage( image: any) { + window.location.href = `http://192.168.68.20:3000/oggit/${image.image.name}`; + } + onNoClick(): void { this.dialogRef.close(); } diff --git a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html index 592c09d..698f326 100644 --- a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html +++ b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html @@ -6,7 +6,7 @@ -

Gestionar imágenes monolíticas en {{data.repositoryName}}

+

{{ 'monolithicImage' | translate }} {{data.repositoryName}}

@@ -48,11 +48,24 @@ {{ column.header }} - - - {{ image.image[column.columnDef] ? 'check_circle' : 'cancel' }} - + +
+ {{ image.name }} +
+
+ Disco:{{ image.partitionInfo?.numDisk }} | Partición:{{ image.partitionInfo?.numPartition }} | + FileSystem:{{ image.partitionInfo?.filesystem }} | Code:{{ image.partitionInfo?.partitionCode }} +
+
+ {{ image.partitionInfo?.osName }} +
+ + + {{ image.isGlobal ? 'Sí' : 'No' }} + + + + *ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status' && column.columnDef !== 'isGlobal' && column.columnDef !== 'name'"> {{ column.cell(image) }} diff --git a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts index 2179996..1c27c32 100644 --- a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts +++ b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts @@ -44,7 +44,7 @@ export class ShowMonoliticImagesComponent implements OnInit { { columnDef: 'name', header: 'Nombre de imagen', - cell: (image: any) => `${image.name}` + cell: (image: any) => image.name }, { columnDef: 'version', @@ -54,17 +54,17 @@ export class ShowMonoliticImagesComponent implements OnInit { { columnDef: 'isGlobal', header: 'Imagen global', - cell: (image: any) => `${image.image?.isGlobal}` + cell: (image: any) => image.image?.isGlobal }, { columnDef: 'status', header: 'Estado', - cell: (image: any) => `${image.status}` + cell: (image: any) => image.status }, { columnDef: 'description', header: 'Descripción', - cell: (image: any) => `${image.description ? image.description : 'Sin descripción'}` + cell: (image: any) => image.description }, { columnDef: 'createdAt', @@ -164,7 +164,6 @@ export class ShowMonoliticImagesComponent implements OnInit { } importImage(): void { - console.log(this.data) this.dialog.open(ImportImageComponent, { width: '600px', data: { @@ -410,9 +409,7 @@ export class ShowMonoliticImagesComponent implements OnInit { this.dialog.open(ServerInfoDialogComponent, { width: '800px', - data: { - message: this.alertMessage - } + data: this.alertMessage }); }, error => { diff --git a/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.css b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.css new file mode 100644 index 0000000..fa2c0ba --- /dev/null +++ b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.css @@ -0,0 +1,124 @@ +.modal-content { + max-height: 85vh; + overflow-y: auto; + padding: 1rem; +} + +.header-container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 10px; + border-bottom: 1px solid #ddd; +} + +.header-container-title { + flex-grow: 1; + text-align: left; + margin-left: 1em; +} + +.calendar-button-row { + display: flex; + gap: 15px; +} + +.lists-container { + padding: 16px; +} + +.imagesLists-container { + flex: 1; +} + +.card.unidad-card { + height: 100%; + box-sizing: border-box; +} + +table { + width: 100%; +} + +.search-container { + display: flex; + justify-content: space-between; + align-items: center; + margin: 1.5rem 0rem 1.5rem 0rem; + box-sizing: border-box; +} + +.search-string { + flex: 1; + padding: 5px; +} + +.search-boolean { + flex: 1; + padding: 5px; +} + +.search-select { + flex: 2; + padding: 5px; +} + +.mat-elevation-z8 { + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2); +} + +.progress-container { + display: flex; + align-items: center; + gap: 10px; +} + +.paginator-container { + display: flex; + justify-content: end; + margin-bottom: 30px; +} + +.chip-failed { + background-color: #e87979 !important; + color: white; +} + +.chip-success { + background-color: #46c446 !important; + color: white; +} + +.chip-pending { + background-color: #bebdbd !important; + color: black; +} + +.chip-in-progress { + background-color: #f5a623 !important; + color: white; +} + +.status-progress-flex { + display: flex; + align-items: center; + gap: 8px; +} + +button.cancel-button { + display: flex; + align-items: center; + justify-content: center; + padding: 5px; +} + +.cancel-button { + color: red; + background-color: transparent; + border: none; + padding: 0; +} + +.cancel-button mat-icon { + color: red; +} diff --git a/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.html b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.html new file mode 100644 index 0000000..2b89d16 --- /dev/null +++ b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.html @@ -0,0 +1,161 @@ + \ No newline at end of file diff --git a/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.spec.ts b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.spec.ts new file mode 100644 index 0000000..1b0b661 --- /dev/null +++ b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { ClientTaskLogsComponent } from './client-task-logs.component'; +import { JoyrideService } from 'ngx-joyride'; +import { ToastrModule } from 'ngx-toastr'; +import { ConfigService } from '@services/config.service'; +import { MatIconModule } from '@angular/material/icon'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { LoadingComponent } from 'src/app/shared/loading/loading.component'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('ClientTaskLogsComponent', () => { + let component: ClientTaskLogsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + const mockJoyrideService = jasmine.createSpyObj('JoyrideService', ['startTour']); + const mockConfigService = { + apiUrl: 'http://mock-api-url' + }; + + await TestBed.configureTestingModule({ + declarations: [ClientTaskLogsComponent, LoadingComponent], + imports: [HttpClientTestingModule, + ToastrModule.forRoot(), + MatIconModule, + TranslateModule.forRoot(), + MatFormFieldModule, + MatPaginatorModule, + MatSelectModule, + BrowserAnimationsModule + ], + providers: [ + { provide: MatDialogRef, useValue: { close: jasmine.createSpy('close') } }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: JoyrideService, useValue: mockJoyrideService }, + { provide: ConfigService, useValue: mockConfigService } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + fixture = TestBed.createComponent(ClientTaskLogsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.ts b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.ts new file mode 100644 index 0000000..2db9e1b --- /dev/null +++ b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.ts @@ -0,0 +1,304 @@ +import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { ConfigService } from '@services/config.service'; +import { DatePipe } from '@angular/common'; +import { DeleteModalComponent } from 'src/app/shared/delete_modal/delete-modal/delete-modal.component'; +import { ToastrService } from 'ngx-toastr'; +import { TranslationService } from '@services/translation.service'; +import { Observable } from 'rxjs'; +import { FormControl } from '@angular/forms'; +import { OutputDialogComponent } from '../output-dialog/output-dialog.component'; +import { InputDialogComponent } from '../input-dialog/input-dialog.component'; +import { ProgressBarMode } from '@angular/material/progress-bar'; +import { JoyrideService } from 'ngx-joyride'; +import { map, startWith } from 'rxjs/operators'; +import { COMMAND_TYPES } from 'src/app/shared/constants/command-types'; + +@Component({ + selector: 'app-client-task-logs', + templateUrl: './client-task-logs.component.html', + styleUrls: ['./client-task-logs.component.css'] +}) +export class ClientTaskLogsComponent implements OnInit { + baseUrl: string; + mercureUrl: string; + traces: any[] = []; + groupedTraces: any[] = []; + commands: any[] = []; + length: number = 0; + itemsPerPage: number = 20; + page: number = 0; + loading: boolean = true; + pageSizeOptions: number[] = [10, 20, 30, 50]; + datePipe: DatePipe = new DatePipe('es-ES'); + mode: ProgressBarMode = 'buffer'; + progress = 0; + bufferValue = 0; + dateRange = new FormControl(); + + filteredCommands2 = Object.keys(COMMAND_TYPES).map(key => ({ + name: key, + value: key, + label: COMMAND_TYPES[key] + })); + + columns = [ + { + columnDef: 'id', + header: 'ID', + cell: (trace: any) => `${trace.id}`, + }, + { + columnDef: 'command', + header: 'Comando', + cell: (trace: any) => trace.command + }, + { + columnDef: 'status', + header: 'Estado', + cell: (trace: any) => trace.status + }, + { + columnDef: 'executedAt', + header: 'Ejecución', + cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'), + }, + { + columnDef: 'finishedAt', + header: 'Finalización', + cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'), + }, + ]; + displayedColumns = [...this.columns.map(column => column.columnDef), 'actions']; + + filters: { [key: string]: string } = {}; + filteredCommands!: Observable; + commandControl = new FormControl(); + + constructor(private http: HttpClient, + @Inject(MAT_DIALOG_DATA) public data: { client: any }, + private joyrideService: JoyrideService, + private dialog: MatDialog, + private cdr: ChangeDetectorRef, + private configService: ConfigService, + private toastService: ToastrService, + private translationService: TranslationService + ) { + this.baseUrl = this.configService.apiUrl; + this.mercureUrl = this.configService.mercureUrl; + } + + ngOnInit(): void { + this.loadTraces(); + this.loadCommands(); + this.filteredCommands = this.commandControl.valueChanges.pipe( + startWith(''), + map(value => (typeof value === 'string' ? value : value?.name)), + map(name => (name ? this._filterCommands(name) : this.commands.slice())) + ); + + const eventSource = new EventSource(`${this.mercureUrl}?topic=` + + encodeURIComponent(`traces`)); + + eventSource.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data && data['@id']) { + this.updateTracesStatus(data['@id'], data.status, data.progress); + } + } + } + + private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void { + const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid); + if (traceIndex !== -1) { + const updatedTraces = [...this.traces]; + + updatedTraces[traceIndex] = { + ...updatedTraces[traceIndex], + status: newStatus, + progress: progress + }; + + this.traces = updatedTraces; + this.cdr.detectChanges(); + + console.log(`Estado actualizado para la traza ${clientUuid}: ${newStatus}`); + } else { + console.warn(`Traza con UUID ${clientUuid} no encontrado en la lista.`); + } + } + + private _filterCommands(name: string): any[] { + const filterValue = name.toLowerCase(); + return this.commands.filter(command => command.name.toLowerCase().includes(filterValue)); + } + + onOptionCommandSelected(selectedCommand: any): void { + this.filters['command'] = selectedCommand.id; + this.loadTraces(); + } + + onOptionStatusSelected(selectedStatus: any): void { + this.filters['status'] = selectedStatus; + this.loadTraces(); + } + + openInputModal(inputData: any): void { + this.dialog.open(InputDialogComponent, { + width: '70vw', + height: '60vh', + data: { input: inputData } + }); + } + + openOutputModal(outputData: any): void { + this.dialog.open(OutputDialogComponent, { + width: '500px', + data: { input: outputData } + }); + } + + cancelTrace(trace: any): void { + this.dialog.open(DeleteModalComponent, { + width: '300px', + data: { name: trace.jobId }, + }).afterClosed().subscribe((result) => { + if (result) { + this.http.post(`${this.baseUrl}/traces/server/${trace.uuid}/cancel`, {}).subscribe({ + next: () => { + this.toastService.success('Transmision de imagen cancelada'); + this.loadTraces(); + }, + error: (error) => { + this.toastService.error(error.error['hydra:description']); + console.error(error.error['hydra:description']); + } + }); + } + }); + } + + loadTraces(): void { + const clientId = this.data.client?.id; + if (!clientId) return; + + this.loading = true; + + let params = new HttpParams() + .set('client.id', clientId) + .set('page', (this.page + 1).toString()) + .set('itemsPerPage', this.itemsPerPage.toString()); + + if (this.filters['command']) { + params = params.set('command.id', this.filters['command']); + } + + if (this.filters['status']) { + params = params.set('status', this.filters['status']); + } + + const range = this.dateRange?.value; + if (range?.start && range?.end) { + const fromDate = this.datePipe.transform(range.start, 'yyyy-MM-dd'); + const toDate = this.datePipe.transform(range.end, 'yyyy-MM-dd'); + + params = params.set('executedAt[after]', fromDate!); + params = params.set('executedAt[before]', toDate!); + } + + const url = `${this.baseUrl}/traces`; + + this.http.get(url, { params }).subscribe( + (data) => { + this.traces = data['hydra:member']; + this.length = data['hydra:totalItems']; + this.loading = false; + }, + (error) => { + console.error('Error fetching client traces', error); + this.loading = false; + } + ); + } + + loadCommands() { + this.loading = true; + this.http.get(`${this.baseUrl}/commands?&page=1&itemsPerPage=10000`).subscribe( + response => { + this.commands = response['hydra:member']; + this.loading = false; + }, + error => { + console.error('Error fetching commands:', error); + this.loading = false; + } + ); + } + + resetFilters(clientSearchCommandInput: any, clientSearchStatusInput: any) { + this.loading = true; + clientSearchCommandInput.value = null; + clientSearchStatusInput.value = null; + this.dateRange.reset(); + this.filters = {}; + this.loadTraces(); + } + + groupByCommandId(traces: any[]): any[] { + const grouped: { [key: string]: any[] } = {}; + + traces.forEach(trace => { + const commandId = trace.command.id; + if (!grouped[commandId]) { + grouped[commandId] = []; + } + grouped[commandId].push(trace); + }); + + return Object.keys(grouped).map(key => ({ + commandId: key, + traces: grouped[key] + })); + } + + onPageChange(event: any): void { + this.page = event.pageIndex; + this.itemsPerPage = event.pageSize; + this.length = event.length; + this.loadTraces(); + } + + translateCommand(command: string): string { + return this.translationService.getCommandTranslation(command); + } + + clearCommandFilter(event: Event, clientSearchCommandInput: any): void { + event.stopPropagation(); + delete this.filters['command']; + clientSearchCommandInput.value = null; + this.loadTraces() + } + + clearStatusFilter(event: Event, clientSearchStatusInput: any): void { + event.stopPropagation(); + delete this.filters['status']; + clientSearchStatusInput.value = null; + this.loadTraces() + } + + iniciarTour(): void { + this.joyrideService.startTour({ + steps: [ + 'titleStep', + 'resetFiltersStep', + 'clientSelectStep', + 'commandSelectStep', + 'tableStep', + 'paginationStep' + ], + showPrevButton: true, + themeColor: '#3f51b5' + }); + } +} diff --git a/ogWebconsole/src/app/components/task-logs/input-dialog/input-dialog.component.css b/ogWebconsole/src/app/components/task-logs/input-dialog/input-dialog.component.css new file mode 100644 index 0000000..e69de29 diff --git a/ogWebconsole/src/app/components/commands/commands-task/task-logs/input-dialog/input-dialog.component.html b/ogWebconsole/src/app/components/task-logs/input-dialog/input-dialog.component.html similarity index 100% rename from ogWebconsole/src/app/components/commands/commands-task/task-logs/input-dialog/input-dialog.component.html rename to ogWebconsole/src/app/components/task-logs/input-dialog/input-dialog.component.html diff --git a/ogWebconsole/src/app/components/commands/commands-task/task-logs/input-dialog/input-dialog.component.spec.ts b/ogWebconsole/src/app/components/task-logs/input-dialog/input-dialog.component.spec.ts similarity index 100% rename from ogWebconsole/src/app/components/commands/commands-task/task-logs/input-dialog/input-dialog.component.spec.ts rename to ogWebconsole/src/app/components/task-logs/input-dialog/input-dialog.component.spec.ts diff --git a/ogWebconsole/src/app/components/commands/commands-task/task-logs/input-dialog/input-dialog.component.ts b/ogWebconsole/src/app/components/task-logs/input-dialog/input-dialog.component.ts similarity index 100% rename from ogWebconsole/src/app/components/commands/commands-task/task-logs/input-dialog/input-dialog.component.ts rename to ogWebconsole/src/app/components/task-logs/input-dialog/input-dialog.component.ts diff --git a/ogWebconsole/src/app/components/task-logs/output-dialog/output-dialog.component.css b/ogWebconsole/src/app/components/task-logs/output-dialog/output-dialog.component.css new file mode 100644 index 0000000..e69de29 diff --git a/ogWebconsole/src/app/components/task-logs/output-dialog/output-dialog.component.html b/ogWebconsole/src/app/components/task-logs/output-dialog/output-dialog.component.html new file mode 100644 index 0000000..eed69e9 --- /dev/null +++ b/ogWebconsole/src/app/components/task-logs/output-dialog/output-dialog.component.html @@ -0,0 +1,7 @@ +

{{ 'inputDetails' | translate }}

+
+
{{ data.input | json }}
+
+
+ +
diff --git a/ogWebconsole/src/app/components/task-logs/output-dialog/output-dialog.component.spec.ts b/ogWebconsole/src/app/components/task-logs/output-dialog/output-dialog.component.spec.ts new file mode 100644 index 0000000..12643ba --- /dev/null +++ b/ogWebconsole/src/app/components/task-logs/output-dialog/output-dialog.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { OutputDialogComponent } from './output-dialog.component'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('OutputDialogComponent', () => { + let component: OutputDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [OutputDialogComponent], + imports: [ + TranslateModule.forRoot() + ], + providers: [ + { provide: MatDialogRef, useValue: { close: jasmine.createSpy('close') } }, + { provide: MAT_DIALOG_DATA, useValue: { input: {} } } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(OutputDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/ogWebconsole/src/app/components/task-logs/output-dialog/output-dialog.component.ts b/ogWebconsole/src/app/components/task-logs/output-dialog/output-dialog.component.ts new file mode 100644 index 0000000..fe47c50 --- /dev/null +++ b/ogWebconsole/src/app/components/task-logs/output-dialog/output-dialog.component.ts @@ -0,0 +1,18 @@ +import {Component, Inject} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; + +@Component({ + selector: 'app-output-dialog', + templateUrl: './output-dialog.component.html', + styleUrl: './output-dialog.component.css' +}) +export class OutputDialogComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { input: any } + ) {} + + close(): void { + this.dialogRef.close(); + } +} diff --git a/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.css b/ogWebconsole/src/app/components/task-logs/task-logs.component.css similarity index 100% rename from ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.css rename to ogWebconsole/src/app/components/task-logs/task-logs.component.css diff --git a/ogWebconsole/src/app/components/task-logs/task-logs.component.html b/ogWebconsole/src/app/components/task-logs/task-logs.component.html new file mode 100644 index 0000000..06d54ee --- /dev/null +++ b/ogWebconsole/src/app/components/task-logs/task-logs.component.html @@ -0,0 +1,182 @@ +
+ + +
+

{{ 'adminCommandsTitle' | + translate }}

+
+ +
+ +
+
+ +
+ + + + +
+ {{ client.name }} + + {{ client.ip }} — {{ client.mac }} + +
+
+ +
+ + Por favor, ingrese el nombre del cliente +
+ + + {{ 'commandSelectStepText' | translate }} + + + {{ translateCommand(command.name) }} + + + + + + + + Estado + + Fallido + Pendiente de ejecutar + Ejecutando + Completado con éxito + Cancelado + + + +
+ + + +
+ + + + + + + + + + + + + +
{{ column.header }} + + + + +
+ + + {{trace.progress}}% +
+
+ +
+ + {{ + trace.status === 'failed' ? 'Error' : + trace.status === 'in-progress' ? 'En ejecución' : + trace.status === 'success' ? 'Completado' : + trace.status === 'pending' ? 'Pendiente' : + trace.status === 'cancelled' ? 'Cancelado' : + trace.status + }} + + +
+
+
+ + +
+ {{ translateCommand(trace.command) }} + {{ trace.jobId }} +
+
+ + +
+ {{ trace.client?.name }} + {{ trace.client?.ip }} +
+
+ + + +
+ {{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}} +
+
+ + +
+ {{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}} +
+
+ + {{ column.cell(trace) }} + + +
+
{{ 'columnActions' | translate }} + + +
+
+ +
+ + +
\ No newline at end of file diff --git a/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.ts b/ogWebconsole/src/app/components/task-logs/task-logs.component.ts similarity index 71% rename from ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.ts rename to ogWebconsole/src/app/components/task-logs/task-logs.component.ts index 60a233f..79425f4 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.ts +++ b/ogWebconsole/src/app/components/task-logs/task-logs.component.ts @@ -8,9 +8,12 @@ import { JoyrideService } from 'ngx-joyride'; import { MatDialog } from "@angular/material/dialog"; import { InputDialogComponent } from "./input-dialog/input-dialog.component"; import { ProgressBarMode } from '@angular/material/progress-bar'; -import { DeleteModalComponent } from "../../../../shared/delete_modal/delete-modal/delete-modal.component"; +import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component"; import { ToastrService } from "ngx-toastr"; import { ConfigService } from '@services/config.service'; +import {OutputDialogComponent} from "./output-dialog/output-dialog.component"; +import {TranslationService} from "@services/translation.service"; +import { COMMAND_TYPES } from '../../shared/constants/command-types'; @Component({ selector: 'app-task-logs', @@ -34,6 +37,12 @@ export class TaskLogsComponent implements OnInit { progress = 0; bufferValue = 0; + filteredCommands2 = Object.keys(COMMAND_TYPES).map(key => ({ + name: key, + value: key, + label: COMMAND_TYPES[key] + })); + columns = [ { columnDef: 'id', @@ -43,45 +52,30 @@ export class TaskLogsComponent implements OnInit { { columnDef: 'command', header: 'Comando', - cell: (trace: any) => `${trace.command}` + cell: (trace: any) => trace.command }, { columnDef: 'client', - header: 'Client', - cell: (trace: any) => `${trace.client?.name}` + header: 'Cliente', + cell: (trace: any) => trace.client?.name }, { columnDef: 'status', header: 'Estado', - cell: (trace: any) => `${trace.status}` - }, - { - columnDef: 'jobId', - header: 'Hilo de trabajo', - cell: (trace: any) => `${trace.jobId}` - }, - { - columnDef: 'input', - header: 'Input', - cell: (trace: any) => `${trace.input}` - }, - { - columnDef: 'output', - header: 'Logs', - cell: (trace: any) => `${trace.output}` + cell: (trace: any) => trace.status }, { columnDef: 'executedAt', - header: 'Programación de ejecución', - cell: (trace: any) => `${this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss')}`, + header: 'Ejecución', + cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'), }, { columnDef: 'finishedAt', header: 'Finalización', - cell: (trace: any) => `${this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss')}`, + cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'), }, ]; - displayedColumns = [...this.columns.map(column => column.columnDef)]; + displayedColumns = [...this.columns.map(column => column.columnDef), 'actions']; filters: { [key: string]: string } = {}; filteredClients!: Observable; @@ -94,7 +88,8 @@ export class TaskLogsComponent implements OnInit { private dialog: MatDialog, private cdr: ChangeDetectorRef, private configService: ConfigService, - private toastService: ToastrService + private toastService: ToastrService, + private translationService: TranslationService ) { this.baseUrl = this.configService.apiUrl; this.mercureUrl = this.configService.mercureUrl; @@ -103,7 +98,7 @@ export class TaskLogsComponent implements OnInit { ngOnInit(): void { this.loadTraces(); this.loadCommands(); - //this.loadClients(); + this.loadClients(); this.filteredCommands = this.commandControl.valueChanges.pipe( startWith(''), map(value => (typeof value === 'string' ? value : value?.name)), @@ -147,11 +142,17 @@ export class TaskLogsComponent implements OnInit { } - private _filterClients(name: string): any[] { - const filterValue = name.toLowerCase(); - return this.clients.filter(client => client.name.toLowerCase().includes(filterValue)); + private _filterClients(value: string): any[] { + const filterValue = value.toLowerCase(); + + return this.clients.filter(client => + client.name?.toLowerCase().includes(filterValue) || + client.ip?.toLowerCase().includes(filterValue) || + client.mac?.toLowerCase().includes(filterValue) + ); } + private _filterCommands(name: string): any[] { const filterValue = name.toLowerCase(); return this.commands.filter(command => command.name.toLowerCase().includes(filterValue)); @@ -161,12 +162,13 @@ export class TaskLogsComponent implements OnInit { return client && client.name ? client.name : ''; } - displayFnCommand(command: any): string { - return command && command.name ? command.name : ''; + onOptionCommandSelected(selectedCommand: any): void { + this.filters['command'] = selectedCommand.name; + this.loadTraces(); } - onOptionCommandSelected(selectedCommand: any): void { - this.filters['command.id'] = selectedCommand.id; + onOptionStatusSelected(selectedStatus: any): void { + this.filters['status'] = selectedStatus; this.loadTraces(); } @@ -177,11 +179,19 @@ export class TaskLogsComponent implements OnInit { openInputModal(inputData: any): void { this.dialog.open(InputDialogComponent, { - width: '700px', + width: '70vw', + height: '60vh', data: { input: inputData } }); } + openOutputModal(outputData: any): void { + this.dialog.open(OutputDialogComponent, { + width: '500px', + data: { input: outputData } + }); + } + cancelTrace(trace: any): void { this.dialog.open(DeleteModalComponent, { width: '300px', @@ -239,30 +249,24 @@ export class TaskLogsComponent implements OnInit { loadClients() { this.loading = true; - this.http.get(`${this.baseUrl}/clients?&page=1&itemsPerPage=10000`).subscribe( + this.http.get(`${this.baseUrl}/clients?page=1&itemsPerPage=10000`).subscribe( response => { - const clientIds = response['hydra:member'].map((client: any) => client['@id']); - const clientDetailsRequests: Observable[] = clientIds.map((id: string) => this.http.get(`${this.baseUrl}${id}`)); - forkJoin(clientDetailsRequests).subscribe( - (clients: any[]) => { - this.clients = clients; - this.loading = false; - }, - (error: any) => { - console.error('Error fetching client details:', error); - this.loading = false; - } - ); + this.clients = response['hydra:member']; + this.loading = false; }, - (error: any) => { + error => { console.error('Error fetching clients:', error); this.loading = false; } ); } - resetFilters() { + + resetFilters(clientSearchCommandInput: any, clientSearchStatusInput: any, clientSearchClientInput: any) { this.loading = true; + clientSearchCommandInput.value = null; + clientSearchStatusInput.value = null; + clientSearchClientInput.value = null; this.filters = {}; this.loadTraces(); } @@ -291,6 +295,31 @@ export class TaskLogsComponent implements OnInit { this.loadTraces(); } + translateCommand(command: string): string { + return this.translationService.getCommandTranslation(command); + } + + clearCommandFilter(event: Event, clientSearchCommandInput: any): void { + event.stopPropagation(); + delete this.filters['command']; + clientSearchCommandInput.value = null; + this.loadTraces() + } + + clearStatusFilter(event: Event, clientSearchStatusInput: any): void { + event.stopPropagation(); + delete this.filters['status']; + clientSearchStatusInput.value = null; + this.loadTraces() + } + + clearClientFilter(event: Event, clientSearchClientInput: any): void { + event.stopPropagation(); + delete this.filters['client.id']; + clientSearchClientInput.value = null; + this.loadTraces() + } + iniciarTour(): void { this.joyrideService.startTour({ steps: [ diff --git a/ogWebconsole/src/app/layout/header/header.component.css b/ogWebconsole/src/app/layout/header/header.component.css index ec5f5c3..72b60ae 100644 --- a/ogWebconsole/src/app/layout/header/header.component.css +++ b/ogWebconsole/src/app/layout/header/header.component.css @@ -2,12 +2,8 @@ mat-toolbar { /*height: 7vh;*/ min-height: 65px; min-width: 375px; - background-color: #3f51b5; - color: white; -} - -.trace-button .mat-icon { - color: #ffffff; + background-color: #e2e8f0; + color: black; } .navbar-actions-row { @@ -51,6 +47,10 @@ mat-toolbar { display: none; } +.trace-button { + color: #3f51b5; +} + @media (min-width: 1500px) { .hide-on-small { display: inline; @@ -80,4 +80,4 @@ mat-toolbar { .smallScreenMenubutton { margin-right: 2vh; } -} \ No newline at end of file +} diff --git a/ogWebconsole/src/app/layout/header/header.component.html b/ogWebconsole/src/app/layout/header/header.component.html index 3413417..e9c1a50 100644 --- a/ogWebconsole/src/app/layout/header/header.component.html +++ b/ogWebconsole/src/app/layout/header/header.component.html @@ -1,7 +1,6 @@ - Opengnsys webconsole