diff --git a/ogWebconsole/src/app/app-routing.module.ts b/ogWebconsole/src/app/app-routing.module.ts index c4208b1..c87b1d9 100644 --- a/ogWebconsole/src/app/app-routing.module.ts +++ b/ogWebconsole/src/app/app-routing.module.ts @@ -31,7 +31,7 @@ import { } from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component"; import {RepositoriesComponent} from "./components/repositories/repositories.component"; import { - CreateImageComponent + CreateClientImageComponent } from "./components/groups/components/client-main-view/create-image/create-image.component"; import { DeployImageComponent @@ -63,10 +63,10 @@ const routes: Routes = [ { path: 'commands-task', component: CommandsTaskComponent }, { path: 'commands-logs', component: TaskLogsComponent }, { path: 'calendars', component: CalendarComponent }, + { path: 'clients/deploy-image', component: DeployImageComponent }, + { path: 'clients/partition-assistant', component: PartitionAssistantComponent }, { path: 'clients/:id', component: ClientMainViewComponent }, - { path: 'clients/:id/partition-assistant', component: PartitionAssistantComponent }, - { path: 'clients/:id/create-image', component: CreateImageComponent }, - { path: 'clients/:id/deploy-image', component: DeployImageComponent }, + { path: 'clients/:id/create-image', component: CreateClientImageComponent }, { path: 'images', component: ImagesComponent }, { 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 633da32..a079e76 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -52,7 +52,6 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { ToastrModule } from 'ngx-toastr'; import { ShowOrganizationalUnitComponent } from './components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component'; import { MatGridList, MatGridTile } from "@angular/material/grid-list"; -import { TreeViewComponent } from './components/groups/shared/tree-view/tree-view.component'; import { MatNestedTreeNode, MatTree, @@ -102,6 +101,7 @@ 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'; import { PartitionAssistantComponent } from './components/groups/components/client-main-view/partition-assistant/partition-assistant.component'; import { SoftwareComponent } from './components/software/software.component'; import { CreateSoftwareComponent } from './components/software/create-software/create-software.component'; @@ -124,6 +124,9 @@ import { MatSortModule } from '@angular/material/sort'; import { MenusComponent } from './components/menus/menus.component'; import { CreateMenuComponent } from './components/menus/create-menu/create-menu.component'; import { CreateMultipleClientComponent } from './components/groups/shared/clients/create-multiple-client/create-multiple-client.component'; +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'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './locale/', '.json'); } @@ -152,7 +155,6 @@ export function HttpLoaderFactory(http: HttpClient) { ClassroomViewComponent, ClientViewComponent, ShowOrganizationalUnitComponent, - TreeViewComponent, LegendComponent, ClassroomViewDialogComponent, SaveFiltersDialogComponent, @@ -174,6 +176,7 @@ export function HttpLoaderFactory(http: HttpClient) { CreateCommandComponent, CalendarComponent, CreateCalendarComponent, + CreateClientImageComponent, CreateCalendarRuleComponent, CommandsGroupsComponent, CommandsTaskComponent, @@ -205,7 +208,10 @@ export function HttpLoaderFactory(http: HttpClient) { EnvVarsComponent, MenusComponent, CreateMenuComponent, - CreateMultipleClientComponent + CreateMultipleClientComponent, + ExportImageComponent, + ImportImageComponent, + LoadingComponent, ], bootstrap: [AppComponent], imports: [BrowserModule, diff --git a/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.css b/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.css index 53254bd..9d1195b 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.css +++ b/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.css @@ -90,3 +90,10 @@ table { color: white; } +.header-container-title { + flex-grow: 1; + text-align: left; + padding-left: 1em; +} + + diff --git a/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.html b/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.html index 5ca167b..6c350ea 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.html +++ b/ogWebconsole/src/app/components/commands/commands-task/task-logs/task-logs.component.html @@ -2,7 +2,11 @@ -

{{ 'adminCommandsTitle' | translate }}

+ +
+

{{ 'adminCommandsTitle' | translate }}

+
+
+ + - - + + + + - diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts index 15066ba..9f6bdc9 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts @@ -1,4 +1,4 @@ -import {Component, Inject, Input, OnInit} from '@angular/core'; +import {Component, Inject, Input, OnInit, SimpleChanges} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog'; import { HttpClient } from '@angular/common/http'; import { FormBuilder, FormGroup } from '@angular/forms'; @@ -11,7 +11,10 @@ import {ToastrService} from "ngx-toastr"; styleUrls: ['./execute-command.component.css'] }) export class ExecuteCommandComponent implements OnInit { - @Input() clientData: any = {}; + @Input() clientData: any[] = []; + @Input() buttonType: 'icon' | 'text' = 'icon'; + @Input() buttonText: string = 'Ejecutar Comandos'; + @Input() icon: string = 'terminal'; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; loading: boolean = true; @@ -29,6 +32,8 @@ export class ExecuteCommandComponent implements OnInit { {name: 'Ejecutar script', slug: 'run-script', disabled: true}, ]; + client: any = {}; + constructor( private dialog: MatDialog, private http: HttpClient, @@ -39,20 +44,13 @@ export class ExecuteCommandComponent implements OnInit { } ngOnInit(): void { - this.clientData = this.clientData || {}; - this.loadClient(this.clientData) + this.clientData = this.clientData || []; } - loadClient = (uuid: string) => { - this.http.get(`${this.baseUrl}${uuid}`).subscribe({ - next: data => { - this.clientData = data; - this.loading = false; - }, - error: error => { - console.error('Error al obtener el cliente:', error); - } - }); + ngOnChanges(changes: SimpleChanges): void { + if (changes['clientData']) { + console.log(this.clientData.length) + } } onCommandSelect(action: any): void { @@ -82,7 +80,9 @@ export class ExecuteCommandComponent implements OnInit { } rebootClient(): void { - this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/reboot`, {}).subscribe( + this.http.post(`${this.baseUrl}/clients/server/reboot`, { + clients: this.clientData.map((client: any) => client['@id']) + }).subscribe( response => { this.toastService.success('Cliente actualizado correctamente'); }, @@ -93,13 +93,11 @@ export class ExecuteCommandComponent implements OnInit { } powerOnClient(): void { - const payload = { - client: this.clientData['@id'] - } - - this.http.post(`${this.baseUrl}${this.clientData.repository['@id']}/wol`, payload).subscribe( + this.http.post(`${this.baseUrl}/image-repositories/wol`, { + clients: this.clientData.map((client: any) => client['@id']) + }).subscribe( response => { - this.toastService.success('Cliente actualizado correctamente'); + this.toastService.success('Petición de encendido enviada correctamente'); }, error => { this.toastService.error('Error de conexión con el cliente'); @@ -108,9 +106,11 @@ export class ExecuteCommandComponent implements OnInit { } powerOffClient(): void { - this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/power-off`, {}).subscribe( + this.http.post(`${this.baseUrl}/clients/server/power-off`, { + clients: this.clientData.map((client: any) => client['@id']) + }).subscribe( response => { - this.toastService.success('Cliente actualizado correctamente'); + this.toastService.success('Petición de apagado enviada correctamente'); }, error => { this.toastService.error('Error de conexión con el cliente'); @@ -119,21 +119,24 @@ export class ExecuteCommandComponent implements OnInit { } openPartitionAssistant(): void { - this.router.navigate([`/clients/${this.clientData.uuid}/partition-assistant`]).then(r => { - console.log('navigated', r); + this.router.navigate(['/clients/partition-assistant'], { + state: { clientData: this.clientData }, + }).then(r => { + console.log('Navigated to partition assistant with data:', this.clientData); }); } openCreateImageAssistant(): void { - this.router.navigate([`/clients/${this.clientData.uuid}/create-image`]).then(r => { + this.router.navigate([`/clients/${this.clientData[0].uuid}/create-image`]).then(r => { console.log('navigated', r); }); } openDeployImageAssistant(): void { - this.router.navigate([`/clients/${this.clientData.uuid}/deploy-image`]).then(r => { - console.log('navigated', r); + this.router.navigate(['/clients/deploy-image'], { + state: { clientData: this.clientData }, + }).then(r => { + console.log('Navigated to deploy image with data:', this.clientData); }); } - } diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.ts index 138af09..431c5b6 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.ts @@ -302,7 +302,7 @@ export class ClientMainViewComponent implements OnInit { } openDeployImageAssistant(): void { - this.router.navigate([`/clients/${this.clientData.uuid}/deploy-image`]).then(r => { + this.router.navigate([`/clients/deploy-image`]).then(r => { console.log('navigated', r); }); } diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.html index be7a033..634a3bd 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.html @@ -1,8 +1,13 @@ + +
- -

Crear Imagen desde {{ clientName }}

+
+

+ Crear imagen desde {{ clientName }} +

+
- +
@@ -12,14 +17,6 @@ Nombre canónico - - - Seleccione imagen creada previamente - - -- - {{ image.name }} - -
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.spec.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.spec.ts deleted file mode 100644 index 1cb70ef..0000000 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CreateImageComponent } from './create-image.component'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { ReactiveFormsModule, FormBuilder } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatPaginatorModule } from '@angular/material/paginator'; -import { MatTableModule } from '@angular/material/table'; -import { MatTabsModule } from '@angular/material/tabs'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ActivatedRoute } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; -import { ToastrModule, ToastrService } from 'ngx-toastr'; -import { of } from 'rxjs'; - -describe('CreateImageComponent', () => { - let component: CreateImageComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - CreateImageComponent, - ReactiveFormsModule, - MatDialogModule, - MatFormFieldModule, - MatInputModule, - MatCheckboxModule, - MatButtonModule, - MatTabsModule, - MatTableModule, - MatPaginatorModule, - BrowserAnimationsModule, - ToastrModule.forRoot(), - TranslateModule.forRoot() - ], - providers: [ - FormBuilder, - ToastrService, - provideHttpClient(), - provideHttpClientTesting(), - { - provide: MatDialogRef, - useValue: {} - }, - { - provide: MAT_DIALOG_DATA, - useValue: {} - }, - { - provide: ActivatedRoute, - useValue: { - snapshot: { - paramMap: { - get: (key: string) => 'valorSimulado' - } - }, - params: of({ id: 'valorSimulado' }) - } - } - ] - }) - .compileComponents(); - - fixture = TestBed.createComponent(CreateImageComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts index 28f0f70..a5d1706 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts @@ -2,61 +2,14 @@ import {Component, EventEmitter, Output} from '@angular/core'; import {HttpClient} from "@angular/common/http"; import {ToastrService} from "ngx-toastr"; import {ActivatedRoute, Router} from "@angular/router"; -import {MatButton} from "@angular/material/button"; -import {MatDivider} from "@angular/material/divider"; -import {NgForOf, NgIf} from "@angular/common"; -import {FormsModule, ReactiveFormsModule} from "@angular/forms"; -import { - MatCell, MatCellDef, - MatColumnDef, - MatHeaderCell, - MatHeaderCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, - MatTable, - MatTableDataSource -} from "@angular/material/table"; -import {MatChip} from "@angular/material/chips"; -import {MatCheckbox} from "@angular/material/checkbox"; +import {MatTableDataSource} from "@angular/material/table"; import {SelectionModel} from "@angular/cdk/collections"; -import {MatRadioButton, MatRadioGroup} from "@angular/material/radio"; -import {MatFormField, MatLabel} from "@angular/material/form-field"; -import {MatOption} from "@angular/material/autocomplete"; -import {MatSelect} from "@angular/material/select"; -import {MatInput} from "@angular/material/input"; - @Component({ selector: 'app-create-image', templateUrl: './create-image.component.html', - standalone: true, - imports: [ - MatButton, - MatDivider, - NgForOf, - NgIf, - ReactiveFormsModule, - MatTable, - MatColumnDef, - MatHeaderCell, - MatHeaderCellDef, - MatCell, - MatCellDef, - MatChip, - MatHeaderRow, - MatRow, - MatHeaderRowDef, - MatRowDef, - MatCheckbox, - MatRadioGroup, - MatRadioButton, - MatFormField, - MatLabel, - MatOption, - MatSelect, - MatInput, - FormsModule - ], styleUrl: './create-image.component.css' }) -export class CreateImageComponent { +export class CreateClientImageComponent { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; @Output() dataChange = new EventEmitter(); @@ -65,10 +18,10 @@ export class CreateImageComponent { partitions: any[] = []; images: any[] = []; clientName: string = ''; - selectedImage: string | null = null; selectedPartition: any = null; name: string = ''; client: any = null; + loading: boolean = false; dataSource = new MatTableDataSource(); columns = [ { @@ -111,7 +64,6 @@ export class CreateImageComponent { ngOnInit() { this.clientId = this.route.snapshot.paramMap.get('id'); - this.loadPartitions(); this.loadImages(); } @@ -147,15 +99,12 @@ export class CreateImageComponent { ); } - back() { - this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} }); - } - save(): void { + this.loading = true; + const payload = { client: `/clients/${this.clientId}`, name: this.name, - image: this.selectedImage, partition: this.selectedPartition['@id'], source: 'assistant' }; @@ -165,11 +114,12 @@ export class CreateImageComponent { .subscribe({ next: (response) => { this.toastService.success('Petición de creación de imagen enviada'); + this.loading = false; this.router.navigate(['/images']); }, error: (error) => { - console.error('Error:', error); this.toastService.error(error.error['hydra:description']); + this.loading = false; } } ); diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.css index 6f2e1d9..f952a3d 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.css +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.css @@ -37,6 +37,7 @@ table { width: 100%; padding: 0 5px; box-sizing: border-box; + padding-left: 1em; } .input-group { @@ -70,7 +71,6 @@ table { display: flex; justify-content: space-between; align-items: center; - padding: 10px; } .mat-elevation-z8 { @@ -82,3 +82,47 @@ table { justify-content: end; margin-bottom: 30px; } + +.clients-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 8px; +} + +.client-item { + position: relative; +} + +.client-card { + background: #ffffff; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + overflow: hidden; + position: relative; + padding: 8px; + text-align: center; +} + +.client-details { + margin-top: 4px; +} + +.client-name { + display: block; + font-size: 1.2em; + font-weight: 600; + color: #333; + margin-bottom: 5px; +} + +.client-ip { + display: block; + font-size: 0.9em; + color: #666; +} + +.header-container-title { + flex-grow: 1; + text-align: left; + padding-left: 1em; +} diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html index dd8e4aa..9abcb98 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html @@ -1,12 +1,45 @@ + +
- -

Desplegar imagen en {{ clientName }}

+
+

+ {{ 'deployImage' | translate }} +

+
- +
+
+ + + Clientes + Listado de clientes donde se desplegará la imagen + + +
+
+
+ Client Icon + +
+ {{ client.name }} + {{ client.ip }} + {{ client.mac }} +
+
+
+
+
+
+ + +
@@ -84,12 +117,12 @@ Máximo Clientes - + Tiempo Máximo de Espera - +
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.spec.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.spec.ts index a6a2e2b..f38ab00 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.spec.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.spec.ts @@ -17,6 +17,8 @@ import { TranslateModule } from '@ngx-translate/core'; import { ToastrModule, ToastrService } from 'ngx-toastr'; import { provideRouter } from '@angular/router'; import { MatSelectModule } from '@angular/material/select'; +import {MatExpansionModule} from "@angular/material/expansion"; +import {LoadingComponent} from "../../../../../shared/loading/loading.component"; describe('DeployImageComponent', () => { let component: DeployImageComponent; @@ -24,7 +26,7 @@ describe('DeployImageComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [DeployImageComponent], + declarations: [DeployImageComponent, LoadingComponent], imports: [ ReactiveFormsModule, FormsModule, @@ -32,6 +34,7 @@ describe('DeployImageComponent', () => { MatFormFieldModule, MatInputModule, MatCheckboxModule, + MatExpansionModule, MatButtonModule, MatTableModule, MatDividerModule, diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts index e5dc0c5..30be7b3 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts @@ -1,4 +1,4 @@ -import {Component, EventEmitter, Output} from '@angular/core'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; import {MatTableDataSource} from "@angular/material/table"; import {SelectionModel} from "@angular/cdk/collections"; import {HttpClient} from "@angular/common/http"; @@ -33,11 +33,13 @@ export class DeployImageComponent { p2pTime: Number = 0; name: string = ''; client: any = null; + clientData: any = []; + loading: boolean = false; protected p2pModeOptions = [ - { name: 'Leecher', value: 'p2p-mode-leecher' }, - { name: 'Peer', value: 'p2p-mode-peer' }, - { name: 'Seeder', value: 'p2p-mode-seeder' }, + { name: 'Leecher', value: 'leecher' }, + { name: 'Peer', value: 'peer' }, + { name: 'Seeder', value: 'seeder' }, ]; protected multicastModeOptions = [ { name: 'Half duplex', value: "half"}, @@ -47,7 +49,6 @@ export class DeployImageComponent { allMethods = [ 'uftp', 'udpcast', - 'multicast-direct', 'unicast', 'unicast-direct', 'p2p' @@ -56,7 +57,6 @@ export class DeployImageComponent { updateCacheMethods = [ 'uftp', 'udpcast', - 'multicast', 'unicast', 'p2p' ]; @@ -98,13 +98,12 @@ export class DeployImageComponent { private toastService: ToastrService, private route: ActivatedRoute, private router: Router, - ) {} - - ngOnInit() { - this.clientId = this.route.snapshot.paramMap.get('id'); - this.selectedOption = 'deploy-image'; - this.loadPartitions(); + ) { + const navigation = this.router.getCurrentNavigation(); + this.clientData = navigation?.extras?.state?.['clientData']; + this.clientId = this.clientData?.[0]['@id']; this.loadImages(); + this.loadPartitions() } get deployMethods() { @@ -116,7 +115,7 @@ export class DeployImageComponent { } loadPartitions() { - const url = `${this.baseUrl}/clients/${this.clientId}`; + const url = `${this.baseUrl}${this.clientId}`; this.http.get(url).subscribe( (response: any) => { if (response.partitions) { @@ -151,11 +150,9 @@ export class DeployImageComponent { ); } - back() { - this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} }); - } - save(): void { + this.loading = true; + if (!this.selectedImage) { this.toastService.error('Debe seleccionar una imagen'); return; @@ -171,26 +168,44 @@ export class DeployImageComponent { return; } + this.toastService.info('Preparando petición de despliegue'); + + const payload = { - client: `/clients/${this.clientId}`, + clients: this.clientData.map((client: any) => client['@id']), method: this.selectedMethod, - partition: this.selectedPartition['@id'], + // partition: this.selectedPartition['@id'], + diskNumber: this.selectedPartition.diskNumber, + partitionNumber: this.selectedPartition.partitionNumber, p2pMode: this.p2pMode, p2pTime: this.p2pTime, mcastIp: this.mcastIp, mcastPort: this.mcastPort, mcastMode: this.mcastMode, mcastSpeed: this.mcastSpeed, + maxTime: this.mcastMaxTime, + maxClients: this.mcastMaxClients, }; this.http.post(`${this.baseUrl}${this.selectedImage}/deploy-image`, payload) .subscribe({ next: (response) => { this.toastService.success('Petición de despliegue enviada correctamente'); + this.loading = false; + this.router.navigate(['/commands-logs']); }, error: (error) => { console.error('Error:', error); - this.toastService.error(error.error['hydra:description']); + this.toastService.error(error.error['hydra:description'], 'Se ha detectado un error en el despliegue de imágenes.', { + "closeButton": true, + "newestOnTop": false, + "progressBar": false, + "positionClass": "toast-bottom-right", + "timeOut": 0, + "extendedTimeOut": 0, + "tapToDismiss": false + }); + this.loading = false; } } ); diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css index 385218d..782c974 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css @@ -167,3 +167,58 @@ button.remove-btn:hover { padding: 20px; margin: 10px auto; } + +.clients-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 8px; +} + +.client-item { + position: relative; +} + +.client-card { + background: #ffffff; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + overflow: hidden; + position: relative; + padding: 8px; + text-align: center; +} + +.client-details { + margin-top: 4px; +} + +.client-name { + display: block; + font-size: 1.2em; + font-weight: 600; + color: #333; + margin-bottom: 5px; +} + +.client-ip { + display: block; + font-size: 0.9em; + color: #666; +} + +.header-container-title { + flex-grow: 1; + text-align: left; + padding-left: 1em; +} + +.select-container { + margin-top: 20px; + align-items: center; + width: 100%; + padding: 0 5px; + box-sizing: border-box; + padding-left: 1em; +} + + diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html index 736e158..7c09394 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html @@ -1,12 +1,45 @@ + +
- -

Asistente de particionado

+
+

+ Asistente de particionado +

+
+
+ + + Clientes + Listado de clientes donde se realizará el particionado + + +
+
+
+ Client Icon + +
+ {{ client.name }} + {{ client.ip }} + {{ client.mac }} +
+
+
+
+
+
+ + +
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts index 80ab9af..94c85da 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts @@ -26,7 +26,7 @@ interface Partition { templateUrl: './partition-assistant.component.html', styleUrls: ['./partition-assistant.component.css'] }) -export class PartitionAssistantComponent implements OnInit { +export class PartitionAssistantComponent { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; @Output() dataChange = new EventEmitter(); partitionTypes = PARTITION_TYPES; @@ -39,6 +39,8 @@ export class PartitionAssistantComponent implements OnInit { updateRequests: any[] = []; data: any = {}; disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = []; + clientData: any = []; + loading: boolean = false; private apiUrl: string = this.baseUrl + '/partitions'; @@ -51,11 +53,12 @@ export class PartitionAssistantComponent implements OnInit { private toastService: ToastrService, private route: ActivatedRoute, private router: Router, - ) {} - - ngOnInit() { - this.clientId = this.route.snapshot.paramMap.get('id'); + ) { + const navigation = this.router.getCurrentNavigation(); + this.clientData = navigation?.extras?.state?.['clientData']; + this.clientId = this.clientData[0]['@id']; this.loadPartitions(); + } get selectedDisk():any { @@ -63,7 +66,7 @@ export class PartitionAssistantComponent implements OnInit { } loadPartitions() { - const url = `${this.baseUrl}/clients/${this.clientId}`; + const url = `${this.baseUrl}${this.clientId}`; this.http.get(url).subscribe( (response) => { this.data = response; @@ -250,16 +253,14 @@ export class PartitionAssistantComponent implements OnInit { return modifiedPartitions; } - back() { - this.router.navigate(['clients', this.data.uuid], { state: { clientData: this.data } }); - } - save() { if (!this.selectedDisk) { this.errorMessage = 'Por favor selecciona un disco antes de guardar.'; return; } + this.loading = true; + const totalPartitionSize = this.selectedDisk.partitions.reduce((sum: any, partition: { size: any; }) => sum + partition.size, 0); if (totalPartitionSize > this.selectedDisk.totalDiskSize) { @@ -283,22 +284,26 @@ export class PartitionAssistantComponent implements OnInit { size: partition.size, partitionCode: partition.partitionCode, filesystem: partition.filesystem, - client: `/clients/${this.clientId}`, uuid: partition.uuid, removed: partition.removed || false, format: partition.format || false, })); if (newPartitions.length > 0) { - const bulkPayload = { partitions: newPartitions }; + const bulkPayload = { + partitions: newPartitions, + clients: this.clientData.map((client: any) => client['@id']), + }; this.http.post(this.apiUrl, bulkPayload).subscribe( (response) => { this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.'); + this.loading = false; this.router.navigate(['/commands-logs']); }, (error) => { console.error('Error al crear las particiones:', error); + this.loading = false; this.toastService.error('Error al crear las particiones.'); } ); diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/restore-image/restore-image.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/restore-image/restore-image.component.html deleted file mode 100644 index 2bd354f..0000000 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/restore-image/restore-image.component.html +++ /dev/null @@ -1,39 +0,0 @@ -

{{ 'diskImageAssistantTitle' | translate }}

-
-
- -
- -
- - - - - - - - - - - - - - -
{{ 'partitionColumn' | translate }}{{ 'isoImageColumn' | translate }}{{ 'ogliveColumn' | translate }}
{{ partition.partitionNumber }} - - - -
- - -
- -
diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index 1120910..ae08699 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -3,8 +3,7 @@ display: flex; justify-content: space-between; align-items: center; - padding: 20px; - background-color: #f5f5f5; + padding: 10px 10px; border-bottom: 1px solid #ddd; } @@ -33,58 +32,6 @@ button[mat-raised-button] { font-size: 16px; } -mat-card { - background-color: #ffffff; - border-radius: 10px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - transition: transform 0.2s, box-shadow 0.2s; - overflow: hidden; - align-items: center; -} - -mat-card:hover { - transform: translateY(-4px); - box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15); -} - -.unidad-card { - cursor: pointer; - padding: 16px; - font-size: 14px; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.unidad-card.selected-item { - border: 2px solid #1976d2; -} - -mat-card-title { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: flex; - align-items: center; - font-size: 16px; - font-weight: bold; - color: #333; - margin: 0; -} - -mat-card-title mat-icon { - font-size: 20px; - margin-right: 8px; - color: #1976d2; -} - -mat-card-actions { - display: flex; - justify-content: flex-end; - gap: 10px; - margin-top: 16px; -} - .actions mat-icon { color: #757575; cursor: pointer; @@ -186,7 +133,7 @@ button[mat-raised-button] { mat-tree { background-color: #f9f9f9; - padding: 10px; + padding: 0px 10px 10px 10px; } mat-tree mat-tree-node { @@ -298,18 +245,14 @@ mat-tree mat-tree-node.disabled:hover { .filters-container { display: flex; - flex-wrap: wrap; - gap: 16px; - margin-bottom: 16px; + justify-content: start; + gap: 1rem; + margin: 2rem 0px 0.7rem 10px; } .filters-container mat-form-field { flex: 1 1 100%; - max-width: 300px; -} - -.filter-container { - margin-bottom: 16px; + max-width: 250px; } .chip-busy { @@ -394,24 +337,17 @@ mat-tree mat-tree-node.disabled:hover { .tree-container { width: 25%; - padding: 16px; overflow-x: hidden; overflow-y: auto; } .clients-container { width: 75%; - padding: 16px; + padding: 0px 16px 16px 16px; box-sizing: border-box; overflow-y: auto; } -.clients-container h3 { - margin-bottom: 15px; - font-size: 1.5em; - color: #333; -} - .client-item { display: flex; justify-content: center; @@ -516,23 +452,10 @@ button[mat-raised-button] { flex-shrink: 0; } -.filters-container { - display: flex; - flex-wrap: wrap; - gap: 16px; - margin: 16px 0; - padding: 0 16px; -} - .mat-elevation-z8 { box-shadow: 0px 0px 0px rgba(0,0,0,0.2); } -.filters-container mat-form-field { - flex: 1 1 300px; - max-width: 300px; -} - .client-info { display: flex; flex-direction: column; @@ -586,7 +509,7 @@ button[mat-raised-button] { .clients-title-name { font-size: x-large; display: block; - padding: 1rem 1rem 1rem 15px; + padding: 1rem 1rem 1rem 13px; } .no-clients-info { @@ -594,4 +517,5 @@ button[mat-raised-button] { align-items: center; gap: 10px; margin-top: 1.5rem; + margin-left: 16px; } \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index acb8172..c0e817c 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -15,8 +15,8 @@ - - + + + + {{ 'searchClient' | translate }} + + + + + + - +
- + @@ -83,7 +100,7 @@ more_vert - + {{ @@ -125,20 +142,20 @@ + - @@ -193,7 +215,11 @@ - +
@@ -202,9 +228,27 @@
+ + + + - -
+ + + + + + {{ 'status' | translate }} + Client Icon {{ 'name' | translate }} +
{{ client.name }}
{{ client.ip }}
@@ -268,7 +313,11 @@ - + - @@ -288,7 +337,7 @@
- +
@@ -299,8 +348,8 @@
- error_outline {{ 'noClients' | translate }} + error_outline
diff --git a/ogWebconsole/src/app/components/groups/groups.component.spec.ts b/ogWebconsole/src/app/components/groups/groups.component.spec.ts index 60f8c01..3da4b0e 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.spec.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.spec.ts @@ -24,6 +24,8 @@ import { TranslateModule } from '@ngx-translate/core'; import { JoyrideModule } from 'ngx-joyride'; import { MatMenuModule } from '@angular/material/menu'; import { MatTreeModule } from '@angular/material/tree'; +import { TreeNode } from './model/model'; +import {ExecuteCommandComponent} from "../commands/main-commands/execute-command/execute-command.component"; describe('GroupsComponent', () => { let component: GroupsComponent; @@ -31,7 +33,7 @@ describe('GroupsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [GroupsComponent], + declarations: [GroupsComponent, ExecuteCommandComponent], imports: [ HttpClientTestingModule, ToastrModule.forRoot(), @@ -80,21 +82,43 @@ describe('GroupsComponent', () => { expect(component.search).toHaveBeenCalled(); }); - it('should call getFilters on ngOnInit', () => { - spyOn(component, 'getFilters'); - component.ngOnInit(); - expect(component.getFilters).toHaveBeenCalled(); - }); - it('should call search method', () => { spyOn(component, 'search'); component.search(); expect(component.search).toHaveBeenCalled(); }); - it('should call getFilters method', () => { - spyOn(component, 'getFilters'); - component.getFilters(); - expect(component.getFilters).toHaveBeenCalled(); + it('should clear selection', () => { + spyOn(component, 'clearSelection'); + component.clearSelection(); + expect(component.clearSelection).toHaveBeenCalled(); + }); + + it('should toggle view', () => { + component.toggleView('card'); + expect(component.currentView).toBe('card'); + component.toggleView('list'); + expect(component.currentView).toBe('list'); + }); + + it('should filter tree', () => { + const searchTerm = 'test'; + spyOn(component, 'filterTree'); + component.filterTree(searchTerm); + expect(component.filterTree).toHaveBeenCalledWith(searchTerm); + }); + + it('should add multiple clients', () => { + spyOn(component, 'addMultipleClients'); + const event = new MouseEvent('click'); + component.addMultipleClients(event); + expect(component.addMultipleClients).toHaveBeenCalledWith(event); + }); + + it('should expand path to node', () => { + const node: TreeNode = { id: '1', name: 'Node 1', type: 'type', children: [] }; + spyOn(component, 'expandPathToNode'); + component.expandPathToNode(node); + expect(component.expandPathToNode).toHaveBeenCalledWith(node); }); }); diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 10ad8ae..a6e7486 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -3,7 +3,6 @@ import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { MatBottomSheet } from '@angular/material/bottom-sheet'; -import { MatTabChangeEvent } from '@angular/material/tabs'; import { ToastrService } from 'ngx-toastr'; import { JoyrideService } from 'ngx-joyride'; import { FlatTreeControl } from '@angular/cdk/tree'; @@ -16,7 +15,6 @@ import { CreateClientComponent } from './shared/clients/create-client/create-cli import { EditOrganizationalUnitComponent } from './shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component'; import { EditClientComponent } from './shared/clients/edit-client/edit-client.component'; import { ShowOrganizationalUnitComponent } from './shared/organizational-units/show-organizational-unit/show-organizational-unit.component'; -import { TreeViewComponent } from './shared/tree-view/tree-view.component'; import { LegendComponent } from './shared/legend/legend.component'; import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component'; import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal'; @@ -24,6 +22,7 @@ import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatPaginator } from '@angular/material/paginator'; import {CreateMultipleClientComponent} from "./shared/clients/create-multiple-client/create-multiple-client.component"; +import {SelectionModel} from "@angular/cdk/collections"; enum NodeType { OrganizationalUnit = 'organizational-unit', @@ -53,17 +52,17 @@ export class GroupsComponent implements OnInit, OnDestroy { commands: Command[] = []; commandsLoading = false; selectedClients = new MatTableDataSource([]); + selection = new SelectionModel(true, []); cols = 4; - selectedClientsOriginal: Client[] = []; currentView: 'card' | 'list' = 'list'; - isTreeViewActive = false; savedFilterNames: [string, string][] = []; selectedTreeFilter = ''; syncStatus = false; syncingClientId: string | null = null; private originalTreeData: TreeNode[] = []; + arrayClients: any[] = []; - displayedColumns: string[] = ['status','sync', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions']; + displayedColumns: string[] = ['select', 'status','sync', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions']; private _sort!: MatSort; private _paginator!: MatPaginator; @@ -112,9 +111,8 @@ export class GroupsComponent implements OnInit, OnDestroy { ngOnInit(): void { this.search(); - this.getFilters(); this.updateGridCols(); - this.loadOrganizationalUnits(); + this.refreshData(); window.addEventListener('resize', this.updateGridCols); this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => { @@ -144,40 +142,6 @@ export class GroupsComponent implements OnInit, OnDestroy { '@id': node['@id'], }); - private loadOrganizationalUnits(): void { - this.loading = true; - this.isLoadingClients = true; - this.dataService.getOrganizationalUnits().subscribe( - (data) => { - this.organizationalUnits = data; - this.loading = false; - - if (this.organizationalUnits.length > 0) { - const treeData = this.organizationalUnits.map((unidad) => this.convertToTreeData(unidad)); - this.treeDataSource.data = treeData.flat(); - - this.isTreeViewActive = true; - - const firstNode = this.treeDataSource.data[0]; - if (firstNode) { - this.selectedNode = firstNode; - this.fetchClientsForNode(firstNode); - } - } else { - this.toastr.info('No existen unidades organizativas'); - this.isTreeViewActive = false; - this.isLoadingClients = false; - return; - } - }, - (error) => { - console.error('Error fetching organizational units', error); - this.loading = false; - } - ); - } - - toggleView(view: 'card' | 'list'): void { this.currentView = view; } @@ -191,10 +155,22 @@ export class GroupsComponent implements OnInit, OnDestroy { this.selectedUnidad = null; this.selectedDetail = null; this.selectedClients.data = []; - this.isTreeViewActive = false; this.selectedNode = null; } + // Función para obtener los filtros guardados actualmente deshabilitada + // getFilters(): void { + // this.subscriptions.add( + // this.dataService.getFilters().subscribe( + // (data) => { + // this.savedFilterNames = data.map((filter: { name: string; uuid: string; }) => [filter.name, filter.uuid]); + // }, + // (error) => { + // console.error('Error fetching filters:', error); + // } + // ) + // ); + // } getFilters(): void { this.subscriptions.add( this.dataService.getFilters().subscribe( @@ -208,21 +184,6 @@ export class GroupsComponent implements OnInit, OnDestroy { ); } - loadSelectedFilter(savedFilter: [string, string]): void { - this.subscriptions.add( - this.dataService.getFilter(savedFilter[1]).subscribe( - (response) => { - if (response) { - console.log('Filter:', response.filters); - } - }, - (error) => { - console.error('Error:', error); - } - ) - ); - } - search(): void { this.loading = true; this.subscriptions.add( @@ -239,49 +200,112 @@ export class GroupsComponent implements OnInit, OnDestroy { ); } - private async loadChildrenAndClients(id: string): Promise { - try { - const childrenData = await this.dataService.getChildren(id).toPromise(); - - const processHierarchy = (nodes: UnidadOrganizativa[]): UnidadOrganizativa[] => { - return nodes.map((node) => ({ - ...node, - children: node.children ? processHierarchy(node.children) : [], - })); - }; - - return { - ...this.selectedUnidad!, - children: childrenData ? processHierarchy(childrenData) : [], - }; - } catch (error) { - console.error('Error loading children:', error); - return this.selectedUnidad!; - } - } - - - private convertToTreeData(data: UnidadOrganizativa): TreeNode[] { + private convertToTreeData(data: UnidadOrganizativa): TreeNode { const processNode = (node: UnidadOrganizativa): TreeNode => ({ id: node.id, + uuid: node.uuid, name: node.name, type: node.type, '@id': node['@id'], children: node.children?.map(processNode) || [], hasClients: (node.clients?.length ?? 0) > 0, }); - return [processNode(data)]; + return processNode(data); } + private refreshData(selectedNodeIdOrUuid?: string): void { + this.loading = true; + this.isLoadingClients = !!selectedNodeIdOrUuid; + + this.dataService.getOrganizationalUnits().subscribe({ + next: (data) => { + this.originalTreeData = data.map((unidad) => this.convertToTreeData(unidad)); + this.treeDataSource.data = [...this.originalTreeData]; + + if (selectedNodeIdOrUuid) { + this.selectedNode = this.findNodeByIdOrUuid(this.treeDataSource.data, selectedNodeIdOrUuid); + + if (this.selectedNode) { + this.treeControl.collapseAll(); + this.expandPathToNode(this.selectedNode); + this.fetchClientsForNode(this.selectedNode); + } + } else { + this.treeControl.collapseAll(); + if (this.treeDataSource.data.length > 0) { + this.selectedNode = this.treeDataSource.data[0]; + this.fetchClientsForNode(this.selectedNode); + } else { + this.selectedNode = null; + this.selectedClients.data = []; + } + } + + this.loading = false; + this.isLoadingClients = false; + }, + error: (error) => { + console.error('Error fetching organizational units', error); + this.toastr.error('Ocurrió un error al cargar las unidades organizativas'); + this.loading = false; + this.isLoadingClients = false; + }, + }); + } + + expandPathToNode(node: TreeNode): void { + const path: TreeNode[] = []; + let currentNode: TreeNode | null = node; + + while (currentNode) { + path.unshift(currentNode); + currentNode = currentNode.id ? this.findParentNode(this.treeDataSource.data, currentNode.id) : null; + } + + path.forEach((pathNode) => { + const flatNode = this.treeControl.dataNodes?.find((n) => n.id === pathNode.id); + if (flatNode) { + this.treeControl.expand(flatNode); + } + }); + } + + private findParentNode(treeData: TreeNode[], childId: string): TreeNode | null { + for (const node of treeData) { + if (node.children?.some((child) => child.id === childId)) { + return node; + } + + if (node.children && node.children.length > 0) { + const parent = this.findParentNode(node.children, childId); + if (parent) { + return parent; + } + } + } + return null; + } + + private findNodeByIdOrUuid(treeData: TreeNode[], identifier: string): TreeNode | null { + const search = (nodes: TreeNode[]): TreeNode | null => { + for (const node of nodes) { + if (node.id === identifier || node.uuid === identifier) return node; + if (node.children && node.children.length > 0) { + const found = search(node.children); + if (found) return found; + } + } + return null; + }; + return search(treeData); + } onNodeClick(node: TreeNode): void { - console.log('Node clicked:', node); this.selectedNode = node; this.fetchClientsForNode(node); } private fetchClientsForNode(node: TreeNode): void { - console.log('Node:', node); this.isLoadingClients = true; this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({ next: (response) => { @@ -294,110 +318,82 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } - getNodeIcon(node: TreeNode): string { - switch (node.type) { - case NodeType.OrganizationalUnit: - return 'apartment'; - case NodeType.ClassroomsGroup: - return 'doors'; - case NodeType.Classroom: - return 'school'; - case NodeType.ClientsGroup: - return 'lan'; - case NodeType.Client: - return 'computer'; - default: - return 'group'; - } - } - addOU(event: MouseEvent, parent: TreeNode | null = null): void { event.stopPropagation(); const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px', }); - dialogRef.afterClosed().subscribe(() => { - this.refreshOrganizationalUnits(); + dialogRef.afterClosed().subscribe((newUnit) => { + if (newUnit?.uuid) { + console.log('Unidad organizativa creada:', newUnit); + this.refreshData(newUnit.uuid); + } }); } addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void { event.stopPropagation(); + const targetNode = organizationalUnit || this.selectedNode; const dialogRef = this.dialog.open(CreateClientComponent, { - data: { organizationalUnit }, + data: { organizationalUnit: targetNode }, width: '900px', }); - dialogRef.afterClosed().subscribe(() => { - this.refreshOrganizationalUnits(); - if (organizationalUnit && organizationalUnit['@id']) { - this.refreshClientsForNode(organizationalUnit); + + dialogRef.afterClosed().subscribe((result) => { + if (result?.client && result?.organizationalUnit) { + const organizationalUnitUrl = result.organizationalUnit; + const uuid = organizationalUnitUrl.split('/')[2]; + const parentNode = this.findNodeByIdOrUuid(this.treeDataSource.data, uuid); + + if (parentNode) { + this.refreshData(parentNode.uuid); + } } }); } addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void { event.stopPropagation(); + const targetNode = organizationalUnit || this.selectedNode; + const dialogRef = this.dialog.open(CreateMultipleClientComponent, { - data: { organizationalUnit }, + data: { organizationalUnit: targetNode }, width: '900px', }); - dialogRef.afterClosed().subscribe(() => { - this.refreshOrganizationalUnits(); - if (organizationalUnit && organizationalUnit['@id']) { - this.refreshClientsForNode(organizationalUnit); + dialogRef.afterClosed().subscribe((result) => { + if (result?.success) { + const organizationalUnitUrl = result.organizationalUnit; + const uuid = organizationalUnitUrl.split('/')[2]; + const parentNode = this.findNodeByIdOrUuid(this.treeDataSource.data, uuid); + + if (parentNode) { + console.log('Nodo padre encontrado para actualización:', parentNode); + this.refreshData(parentNode.uuid); + } else { + console.error('No se encontró el nodo padre después de la creación masiva.'); + } } }); } - private refreshOrganizationalUnits(): void { - const expandedNodeIds = this.treeControl.dataNodes - ? this.treeControl.dataNodes - .filter(node => this.treeControl.isExpanded(node)) - .map(node => this.extractUuid(node['@id'])) - : []; - - this.subscriptions.add( - this.dataService.getOrganizationalUnits().subscribe( - (data) => { - this.organizationalUnits = data; - if (this.selectedUnidad) { - this.loadChildrenAndClients(this.selectedUnidad?.id || '').then((updatedData) => { - this.selectedUnidad = updatedData; - const treeData = this.convertToTreeData(updatedData); - this.originalTreeData = treeData[0]?.children || []; - this.treeDataSource.data = [...this.originalTreeData]; - - setTimeout(() => { - this.treeControl.dataNodes.forEach(node => { - const nodeId = this.extractUuid(node['@id']); - if (nodeId && expandedNodeIds.includes(nodeId)) { - this.treeControl.expand(node); - } - }); - }); - }); - } - }, - (error) => console.error('Error fetching organizational units', error) - ) - ); - } - - onEditNode(event: MouseEvent, node: TreeNode | null): void { event.stopPropagation(); const uuid = node ? this.extractUuid(node['@id']) : null; if (!uuid) return; - if (node && node.type !== NodeType.Client) { - this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); - } else { - this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); - } + const dialogRef = node?.type !== NodeType.Client + ? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) + : this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); + + dialogRef.afterClosed().subscribe(() => { + if (node) { + this.refreshData(node.id); + } + }); } - onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void { + onDeleteClick(event: MouseEvent, node: TreeNode | null): void { event.stopPropagation(); const uuid = node ? this.extractUuid(node['@id']) : null; if (!uuid) return; @@ -410,44 +406,50 @@ export class GroupsComponent implements OnInit, OnDestroy { dialogRef.afterClosed().subscribe((result) => { if (result === true) { - this.deleteEntity(uuid, node.type, node); + this.deleteEntityorClient(uuid, node?.type); } }); } - private deleteEntity(uuid: string, type: string, node: TreeNode): void { - this.subscriptions.add( - this.dataService.deleteElement(uuid, type).subscribe( - () => { - this.refreshOrganizationalUnits(); - if (type === NodeType.Client) { - this.refreshClientsForNode(node); - } - this.toastr.success('Entidad eliminada exitosamente'); - }, - (error) => { - console.error('Error deleting entity:', error); - this.toastr.error('Error al eliminar la entidad', error.message); - } - ) - ); - } + private deleteEntityorClient(uuid: string, type: string): void { + if (!this.selectedNode) return; - private refreshClientsForNode(node: TreeNode): void { - if (!node['@id']) { - this.selectedClients.data = []; - return; - } - this.fetchClientsForNode(node); + const parentNode = this.selectedNode?.id + ? this.findParentNode(this.treeDataSource.data, this.selectedNode.id) + : null; + + this.dataService.deleteElement(uuid, type).subscribe({ + next: () => { + const entityType = type === NodeType.Client ? 'Cliente' : 'Entidad'; + const verb = type === NodeType.Client ? 'eliminado' : 'eliminada'; + + this.toastr.success(`${entityType} ${verb} exitosamente`); + + if (type === NodeType.Client) { + this.refreshData(this.selectedNode?.id); + } else if (parentNode) { + this.refreshData(parentNode.id); + } else { + this.refreshData(); + } + }, + error: (error) => { + console.error('Error deleting entity:', error); + const entityType = type === NodeType.Client ? 'cliente' : 'entidad'; + this.toastr.error(`Error al eliminar el ${entityType}`); + }, + }); } onEditClick(event: MouseEvent, type: string, uuid: string): void { event.stopPropagation(); - if (type !== NodeType.Client) { - this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); - } else { - this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); - } + const dialogRef = type !== NodeType.Client + ? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) + : this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); + + dialogRef.afterClosed().subscribe(() => { + this.refreshData(this.selectedNode?.id); + }); } onRoomMap(room: TreeNode | null): void { @@ -467,22 +469,6 @@ export class GroupsComponent implements OnInit, OnDestroy { ); } - fetchCommands(): void { - this.commandsLoading = true; - this.subscriptions.add( - this.http.get<{ 'hydra:member': Command[] }>(`${this.baseUrl}/commands?page=1&itemsPerPage=30`).subscribe( - (response) => { - this.commands = response['hydra:member']; - this.commandsLoading = false; - }, - (error) => { - console.error('Error fetching commands:', error); - this.commandsLoading = false; - } - ) - ); - } - executeCommand(command: Command, selectedNode: TreeNode | null): void { if (!selectedNode) { @@ -509,13 +495,6 @@ export class GroupsComponent implements OnInit, OnDestroy { } } - onTreeClick(event: MouseEvent, data: TreeNode): void { - event.stopPropagation(); - if (data.type !== NodeType.Client) { - this.dialog.open(TreeViewComponent, { data: { data }, width: '800px' }); - } - } - openBottomSheet(): void { this.bottomSheet.open(LegendComponent); } @@ -531,31 +510,54 @@ export class GroupsComponent implements OnInit, OnDestroy { hasChild = (_: number, node: FlatNode): boolean => node.expandable; isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable; - filterTree(searchTerm: string, filterType: string): void { - const filterNodes = (nodes: TreeNode[]): TreeNode[] => { - const filteredNodes: TreeNode[] = []; - for (const node of nodes) { - const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase()); - const matchesType = filterType ? node.type.toLowerCase() === filterType.toLowerCase() : true; - const filteredChildren = node.children ? filterNodes(node.children) : []; + filterTree(searchTerm: string): void { + const expandPaths: TreeNode[][] = []; - if ((matchesName && matchesType) || filteredChildren.length > 0) { - filteredNodes.push({ ...node, children: filteredChildren }); - } - } - return filteredNodes; + const filterNodes = (nodes: TreeNode[], parentPath: TreeNode[] = []): TreeNode[] => { + return nodes + .map((node) => { + const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase()); + const filteredChildren = node.children ? filterNodes(node.children, [...parentPath, node]) : []; + + if (matchesName) { + expandPaths.push([...parentPath, node]); + return { + ...node, + children: node.children, + } as TreeNode; + } else if (filteredChildren.length > 0) { + return { + ...node, + children: filteredChildren, + } as TreeNode; + } + return null; + }) + .filter((node): node is TreeNode => node !== null); }; - const filteredData = filterNodes(this.originalTreeData); - this.treeDataSource.data = filteredData; + if (!searchTerm) { + this.treeDataSource.data = [...this.originalTreeData]; + this.treeControl.collapseAll(); + } else { + this.treeDataSource.data = filterNodes(this.originalTreeData); + expandPaths.forEach((path) => this.expandPath(path)); + } } - + private expandPath(path: TreeNode[]): void { + path.forEach((pathNode) => { + const flatNode = this.treeControl.dataNodes?.find((n) => n.id === pathNode.id); + if (flatNode) { + this.treeControl.expand(flatNode); + } + }); + } onTreeFilterInput(event: Event): void { const input = event.target as HTMLInputElement; - const searchTerm = input?.value || ''; - this.filterTree(searchTerm, this.selectedTreeFilter); + const searchTerm = input?.value.trim() || ''; + this.filterTree(searchTerm); } onClientFilterInput(event: Event): void { @@ -569,7 +571,6 @@ export class GroupsComponent implements OnInit, OnDestroy { this.selectedClients.filter = this.searchTerm; } - public setSelectedNode(node: TreeNode): void { this.selectedNode = node; } @@ -586,19 +587,68 @@ export class GroupsComponent implements OnInit, OnDestroy { this.toastr.success('Cliente actualizado correctamente'); this.syncStatus = false; this.syncingClientId = null; - this.search() + this.refreshData() }, () => { this.toastr.error('Error de conexión con el cliente'); this.syncStatus = false; this.syncingClientId = null; - this.search() + this.refreshData() } ) ); } + isAllSelected() { + const numSelected = this.selection.selected.length; + const numRows = this.selectedClients.data.length; + return numSelected === numRows; + } + + toggleAllRows() { + if (this.isAllSelected()) { + this.selection.clear(); + this.arrayClients = [] + return; + } + + this.selection.select(...this.selectedClients.data); + this.arrayClients = [...this.selection.selected]; + } + + toggleRow(row: any) { + this.selection.toggle(row); + this.updateSelectedClients(); + } + + updateSelectedClients() { + this.arrayClients = [...this.selection.selected]; + } + + getClientPath(client: Client): string { + const path: string[] = []; + let currentNode: TreeNode | null = this.findNodeByIdOrUuid(this.treeDataSource.data, client.organizationalUnit.uuid); + + while (currentNode) { + path.unshift(currentNode.name); + currentNode = currentNode.id ? this.findParentNode(this.treeDataSource.data, currentNode.id) : null; + } + + return path.join(' / '); + } + private extractUuid(idPath: string | undefined): string | null { return idPath ? idPath.split('/').pop() || null : null; } + + clearTreeSearch(inputElement: HTMLInputElement): void { + inputElement.value = ''; + this.filterTree(''); + } + + clearClientSearch(inputElement: HTMLInputElement): void { + inputElement.value = ''; + this.filterClients(''); + } + } diff --git a/ogWebconsole/src/app/components/groups/model/model.ts b/ogWebconsole/src/app/components/groups/model/model.ts index 0bada34..aaa5b3e 100644 --- a/ogWebconsole/src/app/components/groups/model/model.ts +++ b/ogWebconsole/src/app/components/groups/model/model.ts @@ -61,6 +61,7 @@ export interface ClientCollection { export interface TreeNode { id?: string + uuid?: string; name: string; type: string; '@id'?: string; diff --git a/ogWebconsole/src/app/components/groups/services/data.service.ts b/ogWebconsole/src/app/components/groups/services/data.service.ts index 997c149..34b0bc3 100644 --- a/ogWebconsole/src/app/components/groups/services/data.service.ts +++ b/ogWebconsole/src/app/components/groups/services/data.service.ts @@ -231,5 +231,15 @@ export class DataService { ); } + getOrganizationalUnitPath(unit: UnidadOrganizativa, units: UnidadOrganizativa[]): string { + const path: string[] = []; + let currentUnit: UnidadOrganizativa | undefined = unit; + while (currentUnit) { + path.unshift(currentUnit.name); + currentUnit = units.find(u => u['@id'] === currentUnit?.parent?.['@id']); + } + + return path.join(' / '); + } } diff --git a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html index 84e406d..3ef6251 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html +++ b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html @@ -6,9 +6,12 @@ Padre - + + {{ getSelectedParentName() }} + +
{{ unit.name }}
-
{{ unit.path }}
+
{{ unit.path }}
diff --git a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts index 138f0ad..514893b 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts @@ -15,6 +15,7 @@ export class CreateClientComponent implements OnInit { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; clientForm!: FormGroup; parentUnits: any[] = []; + parentUnitsWithPaths: { id: string, name: string, path: string }[] = []; hardwareProfiles: any[] = []; ogLives: any[] = []; menus: any[] = []; @@ -80,6 +81,11 @@ export class CreateClientComponent implements OnInit { this.http.get(url).subscribe( response => { this.parentUnits = response['hydra:member']; + this.parentUnitsWithPaths = this.parentUnits.map(unit => ({ + id: unit['@id'], + name: unit.name, + path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits) + })); this.loading = false; }, error => { @@ -89,6 +95,11 @@ export class CreateClientComponent implements OnInit { ); } + getSelectedParentName(): string | undefined { + const parentId = this.clientForm.get('organizationalUnit')?.value; + return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name; + } + loadHardwareProfiles(): void { this.dataService.getHardwareProfiles().subscribe( (data: any[]) => { @@ -157,15 +168,21 @@ export class CreateClientComponent implements OnInit { onSubmit(): void { if (this.clientForm.valid) { const formData = this.clientForm.value; + this.http.post(`${this.baseUrl}/clients`, formData).subscribe( - response => { + (response) => { this.toastService.success('Cliente creado exitosamente', 'Éxito'); - this.dialogRef.close(response); + this.dialogRef.close({ + client: response, + organizationalUnit: formData.organizationalUnit, + }); }, - error => { - this.toastService.error('Error al crear el cliente', 'Error'); + (error) => { + this.toastService.error(error.error['hydra:description'], 'Error al crear el cliente'); } ); + } else { + this.toastService.error('Formulario inválido. Por favor, revise los campos obligatorios.', 'Error'); } } diff --git a/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.html index fd678aa..1b34d9c 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.html +++ b/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.html @@ -6,9 +6,13 @@
{{ 'organizationalUnitLabel' | translate }} - - + + + {{ getSelectedParentName() }} + +
{{ unit.name }}
+
{{ unit.path }}
diff --git a/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.ts b/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.ts index e07eaeb..a2fe39e 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/clients/create-multiple-client/create-multiple-client.component.ts @@ -3,6 +3,8 @@ import {MatDialogRef} from "@angular/material/dialog"; import {HttpClient} from "@angular/common/http"; import {MatSnackBar} from "@angular/material/snack-bar"; import {ToastrService} from "ngx-toastr"; +import {MAT_DIALOG_DATA} from "@angular/material/dialog"; +import { DataService } from '../../../services/data.service'; @Component({ selector: 'app-create-multiple-client', @@ -12,21 +14,29 @@ import {ToastrService} from "ngx-toastr"; export class CreateMultipleClientComponent implements OnInit{ baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; parentUnits: any[] = []; + parentUnitsWithPaths: { id: string, name: string, path: string }[] = []; uploadedClients: any[] = []; loading: boolean = false; displayedColumns: string[] = ['name', 'ip', 'mac']; showTextarea: boolean = true; organizationalUnit: any; + regex: RegExp = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([a-zA-Z0-9]{2}(:[a-zA-Z0-9]{2}){5});\s+fixed-address\s+((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?));\s+\}/g; constructor( @Optional() private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) private data: any, private http: HttpClient, private snackBar: MatSnackBar, - private toastService: ToastrService + private toastService: ToastrService, + private dataService: DataService ) {} ngOnInit(): void { this.loadParentUnits(); + + if (this.data?.organizationalUnit) { + this.organizationalUnit = this.data.organizationalUnit['@id']; + } } loadParentUnits(): void { @@ -36,6 +46,11 @@ export class CreateMultipleClientComponent implements OnInit{ this.http.get(url).subscribe( response => { this.parentUnits = response['hydra:member']; + this.parentUnitsWithPaths = this.parentUnits.map(unit => ({ + id: unit['@id'], + name: unit.name, + path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits) + })); this.loading = false; }, error => { @@ -45,8 +60,12 @@ export class CreateMultipleClientComponent implements OnInit{ ); } + getSelectedParentName(): string | undefined { + const parentId = this.organizationalUnit; + return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name; + } + setOrganizationalUnit(organizationalUnit: any): void { - console.log('Organizational unit selected:', organizationalUnit.value); this.organizationalUnit = organizationalUnit.value; } @@ -57,15 +76,14 @@ export class CreateMultipleClientComponent implements OnInit{ reader.onload = (e: any) => { const textData = e.target.result; - const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g; let match; const clients = []; - while ((match = regex.exec(textData)) !== null) { + while ((match = this.regex.exec(textData)) !== null) { clients.push({ name: match[1], mac: match[2], - ip: match[3] + ip: match[4] }); } @@ -84,15 +102,14 @@ export class CreateMultipleClientComponent implements OnInit{ } onTextarea(text: string): void { - const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g; let match; const clients = []; - while ((match = regex.exec(text)) !== null) { + while ((match = this.regex.exec(text)) !== null) { clients.push({ name: match[1], mac: match[2], - ip: match[3] + ip: match[4] }); } @@ -108,6 +125,9 @@ export class CreateMultipleClientComponent implements OnInit{ onSubmit(): void { if (this.uploadedClients.length > 0) { + let successCount = 0; + let errorMessages: string[] = []; + this.uploadedClients.forEach(client => { const formData = { organizationalUnit: this.organizationalUnit, @@ -118,20 +138,37 @@ export class CreateMultipleClientComponent implements OnInit{ this.http.post(`${this.baseUrl}/clients`, formData).subscribe( response => { - this.toastService.success(`Cliente ${client.name} creado exitosamente`, 'Éxito'); + successCount++; + if (successCount + errorMessages.length === this.uploadedClients.length) { + this.showFinalToast(successCount, errorMessages); + } }, error => { - this.toastService.error(error.error['hydra:description'], `Error al crear el cliente ${client.name}`); + errorMessages.push(`Error al crear el cliente ${client.name}: ${error.error['hydra:description']}`); + if (successCount + errorMessages.length === this.uploadedClients.length) { + this.showFinalToast(successCount, errorMessages); + } } ); }); - this.uploadedClients = []; - this.dialogRef.close(); } else { this.toastService.error('No hay clientes cargados para añadir', 'Error'); } } + showFinalToast(successCount: number, errorMessages: string[]): void { + if (successCount > 0) { + this.toastService.success(`${successCount} clientes creados exitosamente`, 'Éxito'); + } + if (errorMessages.length > 0) { + errorMessages.forEach(message => this.toastService.error(message, 'Error')); + } + this.dialogRef.close({ + success: successCount > 0, + organizationalUnit: this.organizationalUnit, + }); + } + onNoClick(): void { this.dialogRef.close(); } diff --git a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.html index 663e8e0..7fb4fcc 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.html +++ b/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.html @@ -6,8 +6,12 @@ {{ 'organizationalUnitLabel' | translate }} - - {{ unit.name }} + + {{ getSelectedParentName() }} + + +
{{ unit.name }}
+
{{ unit.path }}
diff --git a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.ts b/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.ts index 1b05c2c..e53dd47 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/clients/edit-client/edit-client.component.ts @@ -15,6 +15,7 @@ export class EditClientComponent { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; clientForm!: FormGroup; parentUnits: any[] = []; + parentUnitsWithPaths: { id: string, name: string, path: string }[] = []; hardwareProfiles: any[] = []; repositories: any[] = []; ogLives: any[] = []; @@ -68,19 +69,32 @@ export class EditClientComponent { }); } - loadParentUnits() { + loadParentUnits(): void { + this.loading = true; const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`; this.http.get(url).subscribe( response => { this.parentUnits = response['hydra:member']; + this.parentUnitsWithPaths = this.parentUnits.map(unit => ({ + id: unit['@id'], + name: unit.name, + path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits) + })); + this.loading = false; }, error => { console.error('Error fetching parent units:', error); + this.loading = false; } ); } + getSelectedParentName(): string | undefined { + const parentId = this.clientForm.get('organizationalUnit')?.value; + return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name; + } + loadHardwareProfiles(): void { this.dataService.getHardwareProfiles().subscribe( (data: any[]) => { diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.css b/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.css index f8e9c6c..ffd86db 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.css +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.css @@ -39,4 +39,4 @@ button { mat-slide-toggle{ margin-left: 10px; -} \ No newline at end of file +} diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.html b/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.html index fda236e..675c34c 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.html +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.html @@ -1,182 +1,160 @@

{{ 'addOrgUnitTitle' | translate }}

-
- - - - - {{ 'generalStepLabel' | translate }} - - {{ 'typeLabel' | translate }} - - - {{ typeTranslations[type] }} - - - - - {{ 'nameLabel' | translate }} - - - - {{ 'createOrgUnitparentLabel' | translate }} - - {{ 'noParentOption' | translate }} - {{ unit.name }} - - - - {{ 'descriptionLabel' | translate }} - - -
- -
- -
+ +
+ + {{ 'typeLabel' | translate }} + + + {{ typeTranslations[type] }} + + + + + {{ 'nameLabel' | translate }} + + + + {{ 'createOrgUnitparentLabel' | translate }} + + + {{ getSelectedParentName() }} + + +
{{ unit.name }}
+
{{ unit.path }}
+
+
+
+ + {{ 'descriptionLabel' | translate }} + + +
- - -
- {{ 'classroomInfoStepLabel' | translate }} - - {{ 'locationLabel' | translate }} - - - {{ 'projectorToggle' | translate }} - {{ 'boardToggle' | translate }} - - {{ 'capacityLabel' | translate }} - - - - {{ 'associatedCalendarLabel' | translate }} - - - {{ calendar.name }} - - - + + + + {{ 'locationLabel' | translate }} + + + {{ 'projectorToggle' | translate }} + {{ 'boardToggle' | translate }} + + {{ 'capacityLabel' | translate }} + + + + {{ 'associatedCalendarLabel' | translate }} + + + {{ calendar.name }} + + + +
-
- - -
- -
+ +
+ + {{ 'commentsLabel' | translate }} + + +
- - -
- {{ 'additionalInfoStepLabel' | translate }} - - {{ 'commentsLabel' | translate }} - - -
- - -
-
-
+ +
+ + {{ 'ogLiveLabel' | translate }} + + + {{ oglive.filename }} + + + + + {{ 'repositoryLabel' | translate }} + + + {{ repository.name }} + + + + + {{ 'nextServerLabel' | translate }} + + + + {{ 'bootFileNameLabel' | translate }} + + - - - - {{ 'networkSettingsStepLabel' | translate }} - - {{ 'ogLiveLabel' | translate }} - - - {{ oglive.filename }} - - - - - {{ 'repositoryLabel' | translate }} - - - {{ repository.name }} - - - - - {{ 'nextServerLabel' | translate }} - - - - {{ 'bootFileNameLabel' | translate }} - - - - - {{ 'proxyUrlLabel' | translate }} - - - - {{ 'dnsIpLabel' | translate }} - - - - {{ 'netmaskLabel' | translate }} - - - - {{ 'routerLabel' | translate }} - - - - {{ 'ntpIpLabel' | translate }} - - - - {{ 'p2pModeLabel' | translate }} - - - {{ option.name }} - - - - - {{ 'p2pTimeLabel' | translate }} - - - - {{ 'mcastIpLabel' | translate }} - - - - {{ 'mcastSpeedLabel' | translate }} - - - - {{ 'mcastPortLabel' | translate }} - - - - {{ 'mcastModeLabel' | translate }} - - - {{ option.name }} - - - - - {{ 'menuUrlLabel' | translate }} - - - - {{ 'hardwareProfileLabel' | translate }} - - {{ unit.description }} - - {{ 'urlFormatError' | translate }} - - -
-
+ + {{ 'proxyUrlLabel' | translate }} + + + + {{ 'dnsIpLabel' | translate }} + + + + {{ 'netmaskLabel' | translate }} + + + + {{ 'routerLabel' | translate }} + + + + {{ 'ntpIpLabel' | translate }} + + + + {{ 'p2pModeLabel' | translate }} + + + {{ option.name }} + + + + + {{ 'p2pTimeLabel' | translate }} + + + + {{ 'mcastIpLabel' | translate }} + + + + {{ 'mcastSpeedLabel' | translate }} + + + + {{ 'mcastPortLabel' | translate }} + + + + {{ 'mcastModeLabel' | translate }} + + + {{ option.name }} + + + + + {{ 'menuUrlLabel' | translate }} + + + + {{ 'hardwareProfileLabel' | translate }} + + {{ unit.description }} + + {{ 'urlFormatError' | translate }} + +
- +
diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.ts b/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.ts index 5790568..f3aae66 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component.ts @@ -25,9 +25,9 @@ export class CreateOrganizationalUnitComponent implements OnInit { 'clients-group': 'Grupo de clientes' }; protected p2pModeOptions = [ - { name: 'Leecher', value: 'p2p-mode-leecher' }, - { name: 'Peer', value: 'p2p-mode-peer' }, - { name: 'Seeder', value: 'p2p-mode-seeder' }, + { name: 'Leecher', value: 'leecher' }, + { name: 'Peer', value: 'peer' }, + { name: 'Seeder', value: 'seeder' }, ]; protected multicastModeOptions = [ {"name": 'Half duplex', "value": "half"}, @@ -39,9 +39,9 @@ export class CreateOrganizationalUnitComponent implements OnInit { ogLives: any[] = []; repositories: any[] = []; selectedCalendarUuid: string | null = null; + parentUnitsWithPaths: { id: string, name: string, path: string }[] = []; - - @Output() unitAdded = new EventEmitter(); + @Output() unitAdded = new EventEmitter<{ uuid: string; name: string }>(); constructor( private _formBuilder: FormBuilder, @@ -104,11 +104,23 @@ export class CreateOrganizationalUnitComponent implements OnInit { loadParentUnits() { const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`; this.http.get(url).subscribe( - response => this.parentUnits = response['hydra:member'], + response => { + this.parentUnits = response['hydra:member']; + this.parentUnitsWithPaths = this.parentUnits.map(unit => ({ + id: unit['@id'], + name: unit.name, + path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits) + })); + }, error => console.error('Error fetching parent units:', error) ); } + getSelectedParentName(): string | undefined { + const parentId = this.generalFormGroup.get('parent')?.value; + return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name; + } + loadHardwareProfiles(): void { this.dataService.getHardwareProfiles().subscribe( (data: any[]) => this.hardwareProfiles = data, @@ -118,7 +130,9 @@ export class CreateOrganizationalUnitComponent implements OnInit { loadOgLives() { this.dataService.getOgLives().subscribe( - (data: any[]) => this.ogLives = data, + (data: any[]) => { + this.ogLives = data + }, error => console.error('Error fetching ogLives', error) ); } @@ -160,10 +174,10 @@ export class CreateOrganizationalUnitComponent implements OnInit { this.http.post(postUrl, formData, { headers }).subscribe( response => { - this.unitAdded.emit(); - this.dialogRef.close(); - this.toastService.success('Cliente creado exitosamente', 'Éxito'); - this.openSnackBar(false, 'Cliente creado exitosamente'); + this.unitAdded.emit(response); + this.dialogRef.close(response); + this.toastService.success('Unidad creada exitosamente', 'Éxito'); + this.openSnackBar(false, 'Unidad creada exitosamente'); }, error => { console.error('Error al realizar POST:', error); @@ -218,9 +232,9 @@ export class CreateOrganizationalUnitComponent implements OnInit { openSnackBar(isError: boolean, message: string) { if (isError) { - this.toastService.error('Error al crear el cliente: ' + message, 'Error'); + this.toastService.error('Error al crear la unidad: ' + message, 'Error'); } else { - this.toastService.success('Cliente creado exitosamente', 'Éxito'); + this.toastService.success('Unidad creada exitosamente', 'Éxito'); } } } diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.css b/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.css index 40124bd..76f9983 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.css +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.css @@ -5,35 +5,34 @@ h1 { color: #3f51b5; margin-bottom: 20px; } - + .network-form { display: flex; flex-direction: column; gap: 15px; } - + .form-field { width: 100%; margin-top: 10px; } - + .mat-dialog-content { padding: 20px; } - + .mat-dialog-actions { display: flex; justify-content: flex-end; padding: 10px 20px; } - + button { text-transform: none; font-size: 16px; font-weight: 500; } - + .mat-slide-toggle { margin-top: 20px; } - \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.html b/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.html index e1c72e1..c4bdb8a 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.html +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.html @@ -1,170 +1,148 @@

{{ 'editOrgUnitTitle' | translate }}

- - - -
- {{ 'generalStepLabel' | translate }} - - {{ 'typeLabel' | translate }} - - {{ type }} - - - - {{ 'nameLabel' | translate }} - - - - {{ 'editOrgUnitParentLabel' | translate }} - - {{ unit.name }} - - - - {{ 'descriptionLabel' | translate }} - - -
- -
-
-
+ +
+ + {{ 'typeLabel' | translate }} + + + {{ typeTranslations[type] }} + + + + + {{ 'nameLabel' | translate }} + + + + {{ 'editOrgUnitParentLabel' | translate }} + + + {{ getSelectedParentName() }} + + +
{{ unit.name }}
+
{{ unit.path }}
+
+
+
+ + {{ 'descriptionLabel' | translate }} + + +
- - -
- {{ 'classroomInfoStepLabel' | translate }} - - {{ 'locationLabel' | translate }} - - - {{ 'projectorToggle' | translate }} - {{ 'boardToggle' | translate }} - - {{ 'capacityLabel' | translate }} - - + + + + {{ 'locationLabel' | translate }} + + + {{ 'projectorToggle' | translate }} + {{ 'boardToggle' | translate }} + + {{ 'capacityLabel' | translate }} + + + + {{ 'associatedCalendarLabel' | translate }} + + + {{ calendar.name }} + + + +
- - {{ 'associatedCalendarLabel' | translate }} - - - {{ calendar.name }} - - - + +
+ + {{ 'commentsLabel' | translate }} + + +
-
- - -
- -
- - - -
- {{ 'additionalInfoStepLabel' | translate }} - - {{ 'commentsLabel' | translate }} - - -
- - -
-
-
- - - -
- {{ 'networkSettingsStepLabel' | translate }} - - {{ 'ogLiveLabel' | translate }} - - - {{ oglive.filename }} - - - - - {{ 'repositoryLabel' | translate }} - - - {{ repository.name }} - - - - - {{ 'proxyUrlLabel' | translate }} - - - - {{ 'dnsIpLabel' | translate }} - - - - {{ 'netmaskLabel' | translate }} - - - - {{ 'routerLabel' | translate }} - - - - {{ 'ntpIpLabel' | translate }} - - - - {{ 'p2pModeLabel' | translate }} - - {{ option.name }} - - - - {{ 'p2pTimeLabel' | translate }} - - - - {{ 'mcastIpLabel' | translate }} - - - - {{ 'mcastSpeedLabel' | translate }} - - - - {{ 'mcastPortLabel' | translate }} - - - - {{ 'mcastModeLabel' | translate }} - - {{ option.name }} - - - - {{ 'menuUrlLabel' | translate }} - - - - {{ 'hardwareProfileLabel' | translate }} - - {{ unit.description }} - - {{ 'urlFormatError' | translate }} - - {{ 'validationToggle' | translate }} -
- -
-
-
-
+ +
+ + {{ 'ogLiveLabel' | translate }} + + + {{ oglive.filename }} + + + + + {{ 'repositoryLabel' | translate }} + + + {{ repository.name }} + + + + + {{ 'proxyUrlLabel' | translate }} + + + + {{ 'dnsIpLabel' | translate }} + + + + {{ 'netmaskLabel' | translate }} + + + + {{ 'routerLabel' | translate }} + + + + {{ 'ntpIpLabel' | translate }} + + + + {{ 'p2pModeLabel' | translate }} + + {{ option.name }} + + + + {{ 'p2pTimeLabel' | translate }} + + + + {{ 'mcastIpLabel' | translate }} + + + + {{ 'mcastSpeedLabel' | translate }} + + + + {{ 'mcastPortLabel' | translate }} + + + + {{ 'mcastModeLabel' | translate }} + + {{ option.name }} + + + + {{ 'menuUrlLabel' | translate }} + + + + {{ 'hardwareProfileLabel' | translate }} + + {{ unit.description }} + + {{ 'urlFormatError' | translate }} + + {{ 'validationToggle' | translate }} +
- +
diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.ts b/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.ts index a1a3188..251e2b1 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component.ts @@ -19,16 +19,23 @@ export class EditOrganizationalUnitComponent implements OnInit { networkSettingsFormGroup: FormGroup; classroomInfoFormGroup: FormGroup; types: string[] = ['organizational-unit', 'classrooms-group', 'classroom', 'clients-group']; + typeTranslations: { [key: string]: string } = { + 'organizational-unit': 'Centro', + 'classrooms-group': 'Grupo de aulas', + 'classroom': 'Aula', + 'clients-group': 'Grupo de clientes' + }; parentUnits: any[] = []; hardwareProfiles: any[] = []; isEditMode: boolean; currentCalendar: any = []; ogLives: any[] = []; repositories: any[] = []; + parentUnitsWithPaths: { id: string, name: string, path: string }[] = []; protected p2pModeOptions = [ - {"name": 'Leecher', "value": "p2p-mode-leecher"}, - {"name": 'Peer', "value": "p2p-mode-peer"}, - {"name": 'Seeder', "value": "p2p-mode-seeder"}, + {"name": 'Leecher', "value": "leecher"}, + {"name": 'Peer', "value": "peer"}, + {"name": 'Seeder', "value": "seeder"}, ]; protected multicastModeOptions = [ {"name": 'Half duplex', "value": "half"}, @@ -103,18 +110,25 @@ export class EditOrganizationalUnitComponent implements OnInit { } loadParentUnits() { - const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`; - + const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`; this.http.get(url).subscribe( response => { this.parentUnits = response['hydra:member']; + this.parentUnitsWithPaths = this.parentUnits.map(unit => ({ + id: unit['@id'], + name: unit.name, + path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits) + })); }, - error => { - console.error('Error fetching parent units:', error); - } + error => console.error('Error fetching parent units:', error) ); } + getSelectedParentName(): string | undefined { + const parentId = this.generalFormGroup.get('parent')?.value; + return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name; + } + loadHardwareProfiles(): void { this.dataService.getHardwareProfiles().subscribe( (data: any[]) => { @@ -266,7 +280,7 @@ export class EditOrganizationalUnitComponent implements OnInit { }, error => { console.error('Error al realizar POST:', error); - this.toastService.error('Error al editar:', error); + this.toastService.error('Error al editar:', error.error['hydra:description']); } ); } diff --git a/ogWebconsole/src/app/components/groups/shared/tree-view/tree-view.component.css b/ogWebconsole/src/app/components/groups/shared/tree-view/tree-view.component.css deleted file mode 100644 index 5e7e53a..0000000 --- a/ogWebconsole/src/app/components/groups/shared/tree-view/tree-view.component.css +++ /dev/null @@ -1,40 +0,0 @@ -mat-content { - padding: 20px; -} - -.item-content { - display: flex; - width: 100%; - padding: 10px; -} - -.item-content mat-icon { - margin-right: 10px; -} - -.tree-invisible { - display: none; -} - -.tree ul, -.tree li { - margin-top: 0; - margin-bottom: 0; - list-style-type: none; -} - -/* - * This padding sets alignment of the nested nodes. - */ -.tree .mat-nested-tree-node div[role=group] { - padding-left: 40px; -} - -/* - * Padding for leaf nodes. - * Leaf nodes need to have padding so as to align with other non-leaf nodes - * under the same parent. - */ -.tree div[role=group] > .mat-tree-node { - padding-left: 40px; -} diff --git a/ogWebconsole/src/app/components/groups/shared/tree-view/tree-view.component.html b/ogWebconsole/src/app/components/groups/shared/tree-view/tree-view.component.html deleted file mode 100644 index 608d135..0000000 --- a/ogWebconsole/src/app/components/groups/shared/tree-view/tree-view.component.html +++ /dev/null @@ -1,54 +0,0 @@ -

{{ 'viewTreeTitle' | translate }}

- - - - - - apartment - meeting_room - school - computer - lan - help_outline - - {{ node.name }} - - - - -
- -
- - apartment - meeting_room - school - computer - lan - help_outline - - {{ node.name }} -
-
- -
- - - - computer - {{ client.name }} - {{ client.ip }} | {{ client.mac }} - - -
-
-
-
- - - - diff --git a/ogWebconsole/src/app/components/groups/shared/tree-view/tree-view.component.ts b/ogWebconsole/src/app/components/groups/shared/tree-view/tree-view.component.ts deleted file mode 100644 index 4f6d76e..0000000 --- a/ogWebconsole/src/app/components/groups/shared/tree-view/tree-view.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {Component, Inject, OnInit} from '@angular/core'; -import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; -import {NestedTreeControl} from "@angular/cdk/tree"; -import {MatTreeNestedDataSource} from "@angular/material/tree"; - -interface OrganizationalUnit { - id: number; - name: string; - type: string; - clients?: Client[]; - children?: OrganizationalUnit[]; -} - -interface Client { - id: number; - name: string; - ip: string; - mac: string; - serialNumber: string; -} - -@Component({ - selector: 'app-tree-view', - templateUrl: './tree-view.component.html', - styleUrl: './tree-view.component.css' -}) -export class TreeViewComponent implements OnInit { - treeControl = new NestedTreeControl(node => node.children); - dataSource = new MatTreeNestedDataSource(); - - constructor( - private dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any - ) { - } - ngOnInit() { - this.dataSource.data = [this.mapData(this.data.data)]; - } - - hasChild = (_: number, node: OrganizationalUnit) => (!!node.children && node.children.length > 0 || !!node.clients && node.clients.length > 0); - - private mapData(data: any): OrganizationalUnit { - const mapClients = (clients: any[]): Client[] => { - console.log(clients) - return clients.map(client => ({ - id: client.id, - name: client.name, - ip: client.ip, - mac: client.mac, - serialNumber: client.serialNumber, - })); - }; - - const mapChildren = (children: any[]): OrganizationalUnit[] => { - return children.map(child => this.mapData(child)); - }; - - return { - id: data.id, - name: data.name, - type: data.type, - clients: data.clients ? mapClients(data.clients) : [], - children: data.children ? mapChildren(data.children) : [] - }; - } - - close(): void { - this.dialogRef.close(); - } -} 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 6f48c33..409bd7f 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 @@ -42,6 +42,13 @@ {{ 'remotePcLabel' | translate }} + + {{ 'globalImageLabel' | translate }} + +
diff --git a/ogWebconsole/src/app/components/images/create-image/create-image.component.ts b/ogWebconsole/src/app/components/images/create-image/create-image.component.ts index ae6b57c..5043740 100644 --- a/ogWebconsole/src/app/components/images/create-image/create-image.component.ts +++ b/ogWebconsole/src/app/components/images/create-image/create-image.component.ts @@ -30,6 +30,7 @@ export class CreateImageComponent implements OnInit { description: [''], comments: [''], remotePc: [false], + isGlobal: [false], softwareProfile: [''], imageRepository: ['', Validators.required], }); @@ -51,6 +52,7 @@ export class CreateImageComponent implements OnInit { description: [response.description], comments: [response.comments], remotePc: [response.remotePc], + isGlobal: [response.isGlobal], softwareProfile: [response.softwareProfile ? response.softwareProfile['@id'] : null, Validators.required], imageRepository: [response.imageRepository ? response.imageRepository['@id'] : null, Validators.required], }); @@ -90,16 +92,12 @@ export class CreateImageComponent implements OnInit { } saveImage(): void { - if (this.imageForm.invalid) { - this.toastService.error('Por favor, rellena los campos obligatorios'); - return; - } - const payload: any = { name: this.imageForm.value.name, description: this.imageForm.value.description, comments: this.imageForm.value.comments, remotePc: this.imageForm.value.remotePc, + isGlobal: this.imageForm.value.isGlobal, imageRepository: this.imageForm.value.imageRepository, ...(this.imageForm.value.softwareProfile ? { softwareProfile: this.imageForm.value.softwareProfile } : {}), }; diff --git a/ogWebconsole/src/app/components/images/export-image/export-image.component.css b/ogWebconsole/src/app/components/images/export-image/export-image.component.css new file mode 100644 index 0000000..2239479 --- /dev/null +++ b/ogWebconsole/src/app/components/images/export-image/export-image.component.css @@ -0,0 +1,22 @@ +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100px; +} + +mat-form-field { + width: 100%; +} + +mat-dialog-actions { + display: flex; + justify-content: flex-end; +} + +.checkbox-group { + display: flex; + flex-direction: column; + gap: 10px; +} diff --git a/ogWebconsole/src/app/components/images/export-image/export-image.component.html b/ogWebconsole/src/app/components/images/export-image/export-image.component.html new file mode 100644 index 0000000..40f46ec --- /dev/null +++ b/ogWebconsole/src/app/components/images/export-image/export-image.component.html @@ -0,0 +1,15 @@ +

Exportar imagen {{data.image?.name}}

+ + + + Seleccione repositorio destino + + {{ repository.name }} + + + + + + + + diff --git a/ogWebconsole/src/app/components/images/export-image/export-image.component.spec.ts b/ogWebconsole/src/app/components/images/export-image/export-image.component.spec.ts new file mode 100644 index 0000000..e45ee65 --- /dev/null +++ b/ogWebconsole/src/app/components/images/export-image/export-image.component.spec.ts @@ -0,0 +1,58 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExportImageComponent } from './export-image.component'; +import {FormBuilder, ReactiveFormsModule} from "@angular/forms"; +import {ToastrModule, ToastrService} from "ngx-toastr"; +import {provideHttpClient} from "@angular/common/http"; +import {provideHttpClientTesting} from "@angular/common/http/testing"; +import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {MatInputModule} from "@angular/material/input"; +import {MatButtonModule} from "@angular/material/button"; +import {MatSelectModule} from "@angular/material/select"; +import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; +import {TranslateModule} from "@ngx-translate/core"; + +describe('ExportImageComponent', () => { + let component: ExportImageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ExportImageComponent], + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatSelectModule, + BrowserAnimationsModule, + ToastrModule.forRoot(), + TranslateModule.forRoot() + ], + providers: [ + FormBuilder, + ToastrService, + provideHttpClient(), + provideHttpClientTesting(), + { + provide: MatDialogRef, + useValue: {} + }, + { + provide: MAT_DIALOG_DATA, + useValue: {} + }] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ExportImageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ogWebconsole/src/app/components/images/export-image/export-image.component.ts b/ogWebconsole/src/app/components/images/export-image/export-image.component.ts new file mode 100644 index 0000000..9e81e34 --- /dev/null +++ b/ogWebconsole/src/app/components/images/export-image/export-image.component.ts @@ -0,0 +1,62 @@ +import {Component, Inject, OnInit} from '@angular/core'; +import {HttpClient} from "@angular/common/http"; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {ToastrService} from "ngx-toastr"; +import {Router} from "@angular/router"; + +@Component({ + selector: 'app-export-image', + templateUrl: './export-image.component.html', + styleUrl: './export-image.component.css' +}) +export class ExportImageComponent implements OnInit { + baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; + loading: boolean = true; + repositories: any[] = []; + selectedRepository: string = ''; + + constructor( + private http: HttpClient, + public dialogRef: MatDialogRef, + private toastService: ToastrService, + private router: Router, + @Inject(MAT_DIALOG_DATA) public data: { image: any } + ) { + + } + + ngOnInit(): void { + this.loading = true; + this.loadRepositories(); + } + + loadRepositories() { + this.http.get(`${this.baseUrl}/image-repositories?id[ne]=1&page=1&itemsPerPage=50`).subscribe( + response => { + this.repositories = response['hydra:member']; + this.loading = false; + }, + error => console.error('Error fetching organizational units:', error) + ); + } + + save() { + this.http.post(`${this.baseUrl}${this.selectedRepository}/export-image`, { + images: [this.data.image['@id']] + }).subscribe({ + next: (response) => { + this.toastService.success('Imagen exportada correctamente'); + this.dialogRef.close(); + this.router.navigate(['/commands-logs']); + }, + error: error => { + console.error('Error al exportar imagen:', error); + this.toastService.error('Error al exportar imagen'); + } + }); + } + + close() { + this.dialogRef.close(); + } +} diff --git a/ogWebconsole/src/app/components/images/images.component.css b/ogWebconsole/src/app/components/images/images.component.css index 50f0f3a..af5ea91 100644 --- a/ogWebconsole/src/app/components/images/images.component.css +++ b/ogWebconsole/src/app/components/images/images.component.css @@ -91,3 +91,14 @@ table { color: white; } +.chip-transferring { + background-color: #f5a623 !important; + color: white; +} + +.header-container-title { + flex-grow: 1; + text-align: left; + margin-left: 1em; +} + diff --git a/ogWebconsole/src/app/components/images/images.component.html b/ogWebconsole/src/app/components/images/images.component.html index 235fdb9..7162256 100644 --- a/ogWebconsole/src/app/components/images/images.component.html +++ b/ogWebconsole/src/app/components/images/images.component.html @@ -3,7 +3,11 @@ -

{{ 'imagesTitle' | translate }}

+
+

+ {{ 'imagesTitle' | translate }} +

+
+ - + diff --git a/ogWebconsole/src/app/components/images/images.component.ts b/ogWebconsole/src/app/components/images/images.component.ts index ca36c01..fbe94b9 100644 --- a/ogWebconsole/src/app/components/images/images.component.ts +++ b/ogWebconsole/src/app/components/images/images.component.ts @@ -1,16 +1,15 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, Input, OnInit} from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { HttpClient } from '@angular/common/http'; import { MatTableDataSource } from '@angular/material/table'; import { ToastrService } from 'ngx-toastr'; import { DatePipe } from '@angular/common'; import { CreateImageComponent } from './create-image/create-image.component'; -import {CreateCommandComponent} from "../commands/main-commands/create-command/create-command.component"; import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delete-modal.component"; import {ServerInfoDialogComponent} from "../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component"; import {Observable} from "rxjs"; -import {InfoImageComponent} from "../ogboot/pxe-images/info-image/info-image/info-image.component"; import { JoyrideService } from 'ngx-joyride'; +import {ExportImageComponent} from "./export-image/export-image.component"; @Component({ selector: 'app-images', @@ -48,6 +47,11 @@ export class ImagesComponent implements OnInit { header: 'Remote Pc', cell: (image: any) => `${image.remotePc}` }, + { + columnDef: 'isGlobal', + header: 'Imagen Global', + cell: (image: any) => `${image.isGlobal}` + }, { columnDef: 'status', header: 'Estado', @@ -67,16 +71,34 @@ export class ImagesComponent implements OnInit { displayedColumns = [...this.columns.map(column => column.columnDef), 'actions']; private apiUrl = `${this.baseUrl}/images`; + @Input() repositoryUuid: any + private repositoryId: any; constructor( public dialog: MatDialog, private http: HttpClient, private toastService: ToastrService, - private joyrideService: JoyrideService + private joyrideService: JoyrideService, ) {} ngOnInit(): void { - this.search(); + if (this.repositoryUuid) { + this.loadRepository() + } else { + this.search(); + } + } + + loadRepository(): void { + this.http.get(`${this.baseUrl}/image-repositories/${this.repositoryUuid}`, {}).subscribe( + data => { + this.repositoryId = data.id; + this.search(); + }, + error => { + console.error('Error fetching image repositories', error); + } + ) } getStatusLabel(status: string): string { @@ -110,7 +132,7 @@ export class ImagesComponent implements OnInit { search(): void { this.loading = true; - this.http.get(`${this.apiUrl}?page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe( + this.http.get(`${this.apiUrl}?page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}&repository.id=${this.repositoryId}`, { params: this.filters }).subscribe( data => { this.dataSource.data = data['hydra:member']; this.length = data['hydra:totalItems']; @@ -205,6 +227,17 @@ export class ImagesComponent implements OnInit { } }); break; + case 'delete-permanent': + this.http.post(`${this.baseUrl}/images/server/${image.uuid}/delete-permanent`, {}).subscribe({ + next: () => { + this.toastService.success('Petición de eliminación de la papelera temporal enviada'); + this.search() + }, + error: (error) => { + this.toastService.error(error.error['hydra:description']); + } + }); + break; case 'recover': this.http.post(`${this.baseUrl}/images/server/${image.uuid}/recover`, {}).subscribe({ next: () => { @@ -216,6 +249,14 @@ export class ImagesComponent implements OnInit { } }); break; + case 'export': + this.dialog.open(ExportImageComponent, { + width: '600px', + data: { + image: image + } + }); + break; default: console.error('Acción no soportada:', action); break; diff --git a/ogWebconsole/src/app/components/repositories/import-image/import-image.component.css b/ogWebconsole/src/app/components/repositories/import-image/import-image.component.css new file mode 100644 index 0000000..6e0686f --- /dev/null +++ b/ogWebconsole/src/app/components/repositories/import-image/import-image.component.css @@ -0,0 +1,39 @@ +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100px; +} + +mat-form-field { + width: 100%; +} + +mat-dialog-actions { + display: flex; + justify-content: flex-end; +} + +.checkbox-group { + display: flex; + flex-direction: column; + gap: 10px; +} + +.selected-list ul { + list-style: none; + padding: 0; +} + +.selected-item { + display: flex; + justify-content: space-between; /* Alinea texto a la izquierda y botón a la derecha */ + align-items: center; /* Centra verticalmente */ + padding: 8px; + border-bottom: 1px solid #ccc; +} + +.selected-item button { + margin-left: 10px; +} diff --git a/ogWebconsole/src/app/components/repositories/import-image/import-image.component.html b/ogWebconsole/src/app/components/repositories/import-image/import-image.component.html new file mode 100644 index 0000000..943cc38 --- /dev/null +++ b/ogWebconsole/src/app/components/repositories/import-image/import-image.component.html @@ -0,0 +1,28 @@ +

Importar imagenes a {{data.repository?.name}}

+ + + + Seleccione imagenes a importar + + {{ image.name }} + + + +
+

Imágenes seleccionadas:

+
    +
  • + {{ getImageName(imageId) }} + +
  • +
+
+ +
+ + + + + diff --git a/ogWebconsole/src/app/components/repositories/import-image/import-image.component.spec.ts b/ogWebconsole/src/app/components/repositories/import-image/import-image.component.spec.ts new file mode 100644 index 0000000..03aa01d --- /dev/null +++ b/ogWebconsole/src/app/components/repositories/import-image/import-image.component.spec.ts @@ -0,0 +1,59 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ImportImageComponent } from './import-image.component'; +import {FormBuilder, ReactiveFormsModule} from "@angular/forms"; +import {ToastrModule, ToastrService} from "ngx-toastr"; +import {DataService} from "../../calendar/data.service"; +import {provideHttpClient} from "@angular/common/http"; +import {provideHttpClientTesting} from "@angular/common/http/testing"; +import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; +import {TranslateModule} from "@ngx-translate/core"; +import {MatButtonModule} from "@angular/material/button"; +import {MatInputModule} from "@angular/material/input"; +import {MatSelectModule} from "@angular/material/select"; + +describe('ImportImageComponent', () => { + let component: ImportImageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ImportImageComponent], + imports: [ + ReactiveFormsModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatSelectModule, + BrowserAnimationsModule, + ToastrModule.forRoot(), + TranslateModule.forRoot() + ], + providers: [ + FormBuilder, + ToastrService, + provideHttpClient(), + provideHttpClientTesting(), + { + provide: MatDialogRef, + useValue: {} + }, + { + provide: MAT_DIALOG_DATA, + useValue: {} + }] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ImportImageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ogWebconsole/src/app/components/repositories/import-image/import-image.component.ts b/ogWebconsole/src/app/components/repositories/import-image/import-image.component.ts new file mode 100644 index 0000000..8db695a --- /dev/null +++ b/ogWebconsole/src/app/components/repositories/import-image/import-image.component.ts @@ -0,0 +1,70 @@ +import {Component, Inject, OnInit} from '@angular/core'; +import {HttpClient} from "@angular/common/http"; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; +import {ToastrService} from "ngx-toastr"; +import {Router} from "@angular/router"; + +@Component({ + selector: 'app-import-image', + templateUrl: './import-image.component.html', + styleUrl: './import-image.component.css' +}) +export class ImportImageComponent implements OnInit{ + baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; + loading: boolean = true; + images: any[] = []; + selectedClients: any[] = []; + + constructor( + private http: HttpClient, + public dialogRef: MatDialogRef, + private toastService: ToastrService, + private router: Router, + @Inject(MAT_DIALOG_DATA) public data: { repository: any } + ) { + + } + + ngOnInit(): void { + this.loading = true; + this.loadImages(); + } + + loadImages() { + this.http.get(`${this.baseUrl}/images?page=1&itemsPerPage=50`).subscribe( + response => { + this.images = response['hydra:member']; + this.loading = false; + }, + error => console.error('Error fetching organizational units:', error) + ); + } + + getImageName(imageId: string): string { + const image = this.images.find(img => img['@id'] === imageId); + return image ? image.name : 'Desconocido'; + } + + removeImage(imageId: string) { + this.selectedClients = this.selectedClients.filter(id => id !== imageId); + } + + save() { + this.http.post(`${this.baseUrl}${this.data.repository['@id']}/import-image`, { + images: this.selectedClients + }).subscribe({ + next: (response) => { + this.toastService.success('Peticion de importacion de imagen enviada correctamente'); + this.dialogRef.close(); + this.router.navigate(['/commands-logs']); + }, + error: error => { + this.toastService.error('Error al importar imagenes'); + } + }); + } + + close() { + this.dialogRef.close(); + } +} diff --git a/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.css b/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.css index 66ff364..326ce6a 100644 --- a/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.css +++ b/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.css @@ -135,6 +135,10 @@ min-height: 400px; } +.main-container { + margin-top: 15px; +} + .mat-tab-body-wrapper { min-height: inherit; } @@ -210,13 +214,6 @@ p { align-items: center; } -.status-led { - width: 10px; - height: 10px; - border-radius: 50%; - display: inline-block; - margin-right: 10px; -} .status-led.active { background-color: green; @@ -304,4 +301,85 @@ table { } +.dashboard { + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.grid-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; +} + +.row { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 20px; +} + +.top-row { + display: flex; + justify-content: center; + gap: 20px; +} + +.top-row .card { + flex: 1; +} + +.card { + background: white; + padding: 15px; + border-radius: 10px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + flex: 1; + min-width: 300px; +} + +.status-led { + width: 10px; + height: 10px; + display: inline-block; + border-radius: 50%; + margin-right: 5px; +} + +.active { + background-color: green; +} + +.inactive { + background-color: red; +} + +.cpu-usage-bar { + background: lightgray; + width: 100%; + height: 20px; + border-radius: 5px; + overflow: hidden; +} + +.cpu-bar { + height: 100%; + background: green; +} + +.cpu-bar.high { + background: red; +} + +@media (max-width: 900px) { + .top-row { + flex-direction: column; + } + + .top-row .card { + max-width: 100%; + } +} diff --git a/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.html b/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.html index 5dea2dd..ae23d9d 100644 --- a/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.html +++ b/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.html @@ -1,51 +1,91 @@ - +
-

OgRepository server Status

-
-
-

Uso de disco

+

OgRepository Server Status

+ +
+
+

Uso de Disco

+ [labels]="showLabels" > -
+

Total: {{ diskUsage.total }}

Ocupado: {{ diskUsage.used }}

Disponible: {{ diskUsage.available }}

-

Ocupado ( % ): {{ diskUsage.percentage }}

+

Ocupado (%): {{ diskUsage.percentage }}

-
+
+

Uso de RAM

+ + +
+

Total: {{ ramUsage.total }}

+

Ocupado: {{ ramUsage.used }}

+

Disponible: {{ ramUsage.available }}

+

Ocupado (%): {{ ramUsage.percentage }}

+
+
+
+ +
+
+

Uso de CPU

+
+
+
+

Usado: {{ cpuUsage.percentage }}

+
+ +

Servicios

  • - + {{ service.name }}: - Activo - Detenido - No accesible - {{ service.status }} - + Activo + Detenido + No accesible + {{ service.status }} + +
  • +
+
+ +
+

Procesos

+
    +
  • + + {{ process.name }}: {{ process.status }}
+ +
@@ -83,59 +123,6 @@ -
-

Imágenes

-
- - Buscar nombre de imagen - - search - Pulsar 'enter' para buscar - -
- - - - - - - - - - - - - -
{{ column.header }} - - - {{ image[column.columnDef] ? 'check_circle' : 'cancel' }} - - - - {{ column.cell(image) }} - - Acciones - - - - - - - -
-
- - -
-
+
diff --git a/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.ts b/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.ts index f209816..3b77099 100644 --- a/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.ts +++ b/ogWebconsole/src/app/components/repositories/main-repository-view/main-repository-view.component.ts @@ -1,4 +1,4 @@ -import {Component, Inject} from '@angular/core'; +import {Component, Inject, OnInit} from '@angular/core'; import {FormBuilder, FormGroup, Validators} from "@angular/forms"; import {HttpClient} from "@angular/common/http"; import {ToastrService} from "ngx-toastr"; @@ -17,7 +17,7 @@ import {MatDialog} from "@angular/material/dialog"; templateUrl: './main-repository-view.component.html', styleUrl: './main-repository-view.component.css' }) -export class MainRepositoryViewComponent { +export class MainRepositoryViewComponent implements OnInit { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; repositoryForm: FormGroup; repositoryId: string | null = null; @@ -25,16 +25,20 @@ export class MainRepositoryViewComponent { loading: boolean = true; diskUsage: any = {}; servicesStatus: any = {}; + processesStatus: any = {}; diskUsageChartData: any[] = []; + ramUsageChartData: any[] = []; + ramUsage: any = {}; + cpuUsage: any = {}; alertMessage: string | null = null; length: number = 0; itemsPerPage: number = 10; page: number = 0; view: [number, number] = [800, 500]; gradient: boolean = true; - showLegend: boolean = true; showLabels: boolean = true; isDoughnut: boolean = true; + status: boolean = false; repositoryData: any = {}; colorScheme: any = { domain: ['#FF6384', '#3f51b5'] @@ -115,7 +119,6 @@ export class MainRepositoryViewComponent { comments: [response.comments], }); this.loading = false; - // Llamar searchImages() solo cuando la data de repository esté cargada this.searchImages(); }, (error) => { @@ -157,28 +160,33 @@ export class MainRepositoryViewComponent { loadStatus(): void { this.http.get(`${this.baseUrl}/image-repositories/server/${this.repositoryId}/status`).subscribe(data => { - const diskData = data.output.disk; - const servicesData = data.output.services; + if (!data.success) { + console.error('Error: No se pudo obtener los datos del servidor'); + this.status = false; + return; + } - this.diskUsage = { - total: diskData.total, - used: diskData.used, - available: diskData.available, - percentage: diskData.used_percentage - }; + this.status = true; + const { disk, services, ram, cpu, processes } = data.output; + + this.diskUsage = { ...disk }; this.diskUsageChartData = [ - { - name: 'Usado', - value: parseFloat(diskData.used) - }, - { - name: 'Disponible', - value: parseFloat(diskData.available) - } + { name: 'Usado', value: parseFloat(disk.used.replace('GB', '')) }, + { name: 'Disponible', value: parseFloat(disk.available.replace('GB', '')) } ]; - this.servicesStatus = servicesData; + this.ramUsage = { ...ram }; + this.ramUsageChartData = [ + { name: 'Usado', value: parseFloat(ram.used.replace('GB', '')) }, + { name: 'Disponible', value: parseFloat(ram.available.replace('GB', '')) } + ]; + + this.cpuUsage = { percentage: cpu.used_percentage }; + + this.servicesStatus = Object.entries(services).map(([name, status]) => ({ name, status })); + + this.processesStatus = Object.entries(processes).map(([name, status]) => ({ name, status })); }, error => { console.error('Error fetching status', error); @@ -186,10 +194,17 @@ export class MainRepositoryViewComponent { } getServices(): { name: string, status: string }[] { - return Object.keys(this.servicesStatus).map(key => ({ - name: key, - status: this.servicesStatus[key] - })); + if (!this.status) { + return []; + } + return this.servicesStatus ? this.servicesStatus : []; + } + + getProcesses(): { name: string, status: string }[] { + if (!this.status) { + return []; + } + return this.processesStatus ? this.processesStatus : []; } searchImages(): void { @@ -207,64 +222,6 @@ export class MainRepositoryViewComponent { ); } - editImage(event: MouseEvent, image: any): void { - event.stopPropagation(); - this.dialog.open(CreateImageComponent, { - width: '800px', - data: image['@id'] - }).afterClosed().subscribe(() => this.searchImages()); - } - - deleteImage(image: any): void { - this.dialog.open(DeleteModalComponent, { - width: '300px', - data: { name: image.name }, - }).afterClosed().subscribe((result) => { - if (result) { - this.http.delete(`${this.apiUrl}/server/${image.uuid}/delete`).subscribe({ - next: () => { - this.toastService.success('Imagen eliminada con éxito'); - this.searchImages(); - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - console.error('Error al eliminar la imagen:', error); - } - }); - } - }); - } - - loadImageAlert(image: any): Observable { - return this.http.get(`${this.apiUrl}/server/${image.uuid}/get`, {}); - } - - showImageInfo(event: MouseEvent, image:any) { - event.stopPropagation(); - this.loadImageAlert(image).subscribe( - response => { - this.alertMessage = response.output; - - this.dialog.open(ServerInfoDialogComponent, { - width: '600px', - data: { - message: this.alertMessage - } - }); - }, - error => { - this.toastService.error(error.error['hydra:description']); - } - ); - } - - onPageChange(event: any): void { - this.page = event.pageIndex; - this.itemsPerPage = event.pageSize; - this.length = event.length; - this.searchImages(); - } - loadAlert(): Observable { return this.http.get(`${this.baseUrl}/image-repositories/server/get-collection`); } @@ -280,29 +237,6 @@ export class MainRepositoryViewComponent { }); } - - toggleAction(image: any, action:string): void { - switch (action) { - case 'get-aux': - this.http.post(`${this.baseUrl}/images/server/${image.uuid}/create-aux-files`, {}).subscribe({ - next: () => { - this.toastService.success('Petición de creación de archivos auxiliares enviada'); - this.searchImages() - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - } - }); - break; - case 'delete': - this.deleteImage(image); - break; - default: - console.error('Acción no soportada:', action); - break; - } - } - openImageInfoDialog() { this.loadAlert().subscribe( response => { diff --git a/ogWebconsole/src/app/components/repositories/repositories.component.css b/ogWebconsole/src/app/components/repositories/repositories.component.css index 16117cb..8207143 100644 --- a/ogWebconsole/src/app/components/repositories/repositories.component.css +++ b/ogWebconsole/src/app/components/repositories/repositories.component.css @@ -100,3 +100,10 @@ table { margin: 8px 8px 8px 0; } +.header-container-title { + flex-grow: 1; + text-align: left; + margin-left: 1em; +} + + diff --git a/ogWebconsole/src/app/components/repositories/repositories.component.html b/ogWebconsole/src/app/components/repositories/repositories.component.html index debcfd6..204a119 100644 --- a/ogWebconsole/src/app/components/repositories/repositories.component.html +++ b/ogWebconsole/src/app/components/repositories/repositories.component.html @@ -2,7 +2,11 @@ -

Administrar repositorios

+
+

+ {{ 'repositoryTitle' | translate }} +

+
@@ -11,14 +15,20 @@
- Buscar nombre de imagen + Buscar nombre de repositorio search Pulsar 'enter' para buscar + + Buscar IP de repositorio + + search + Pulsar 'enter' para buscar +
- +
- diff --git a/ogWebconsole/src/app/components/repositories/repositories.component.ts b/ogWebconsole/src/app/components/repositories/repositories.component.ts index 20229db..e497435 100644 --- a/ogWebconsole/src/app/components/repositories/repositories.component.ts +++ b/ogWebconsole/src/app/components/repositories/repositories.component.ts @@ -8,6 +8,7 @@ import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delet import { JoyrideService } from 'ngx-joyride'; import {CreateRepositoryComponent} from "./create-repository/create-repository.component"; import { Router } from '@angular/router'; +import {ImportImageComponent} from "./import-image/import-image.component"; @Component({ selector: 'app-repositories', @@ -91,6 +92,16 @@ export class RepositoriesComponent { this.router.navigate(['repository', repository.uuid]); } + importImage(event: MouseEvent, repository: any): void { + event.stopPropagation(); + this.dialog.open(ImportImageComponent, { + width: '600px', + data: { repository } + }).afterClosed().subscribe(() => { + this.search(); + }); + } + deleteRepository(event: MouseEvent,command: any): void { event.stopPropagation(); this.dialog.open(DeleteModalComponent, { diff --git a/ogWebconsole/src/app/layout/main-layout/main-layout.component.css b/ogWebconsole/src/app/layout/main-layout/main-layout.component.css index 79922ec..c60e4f2 100644 --- a/ogWebconsole/src/app/layout/main-layout/main-layout.component.css +++ b/ogWebconsole/src/app/layout/main-layout/main-layout.component.css @@ -4,15 +4,16 @@ .container { width: 100vw; - height: calc(100vh - 10vh); + height: calc(100vh - 10vh); } .sidebar { - width: 250px; + width: 250px; + z-index: auto !important; } .content { - margin: 10px; - padding: 10px; - box-sizing: border-box; -} \ No newline at end of file + margin: 0px 10px 10px 10px; + padding: 0px 10px 10px 10px; + box-sizing: border-box; +} diff --git a/ogWebconsole/src/app/shared/loading/loading.component.css b/ogWebconsole/src/app/shared/loading/loading.component.css new file mode 100644 index 0000000..04b222d --- /dev/null +++ b/ogWebconsole/src/app/shared/loading/loading.component.css @@ -0,0 +1,12 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 99999; +} diff --git a/ogWebconsole/src/app/shared/loading/loading.component.html b/ogWebconsole/src/app/shared/loading/loading.component.html new file mode 100644 index 0000000..2ecad58 --- /dev/null +++ b/ogWebconsole/src/app/shared/loading/loading.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/ogWebconsole/src/app/shared/loading/loading.component.spec.ts b/ogWebconsole/src/app/shared/loading/loading.component.spec.ts new file mode 100644 index 0000000..0356eca --- /dev/null +++ b/ogWebconsole/src/app/shared/loading/loading.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoadingComponent } from './loading.component'; + +describe('LoadingComponent', () => { + let component: LoadingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LoadingComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LoadingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ogWebconsole/src/app/shared/loading/loading.component.ts b/ogWebconsole/src/app/shared/loading/loading.component.ts new file mode 100644 index 0000000..49bd8da --- /dev/null +++ b/ogWebconsole/src/app/shared/loading/loading.component.ts @@ -0,0 +1,10 @@ +import {Component, Input} from '@angular/core'; + +@Component({ + selector: 'app-loading', + templateUrl: './loading.component.html', + styleUrl: './loading.component.css' +}) +export class LoadingComponent { + @Input() isLoading: boolean = false; +} diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index 32bffd7..2ee9b67 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -156,6 +156,8 @@ "newOrganizationalUnitTooltip": "Open modal to create organizational units of any type (Center, Classroom, Classroom Group, or Client Group)", "newOrganizationalUnitButton": "New Organizational Unit", "newClientButton": "New Client", + "newSingleClientButton": "Add single client", + "newMultipleClientButton": "Add numerous clients", "keyStepText": "The legend will show you the types of organizational units and their corresponding icons", "legendButton": "Legend", "unitStepText": "This is the section where 'Center' type organizational units will be displayed", @@ -215,7 +217,8 @@ "hardwareProfileLabel": "Hardware Profile", "urlFormatError": "Invalid URL format.", "validationToggle": "Validation", - "submitButton": "Add", + "addOUSubmitButton": "Add", + "editOUSubmitButton": "Edit", "addOrgUnitTitle": "Add Organizational Unit", "createOrgUnitparentLabel": "Parent organizational unit", "noParentOption": "--", @@ -266,11 +269,13 @@ "diskUsedLabel": "Used", "diskTotalLabel": "Total", "diskImageAssistantTitle": "Disk image assistant", + "deployImage": "Deploy image", "partitionColumn": "Partition", "isoImageColumn": "ISO Image", "ogliveColumn": "OgLive", "selectImageOption": "Select image", "selectOgLiveOption": "Select OgLive", + "repositoryTitle": "Admin Repository", "saveAssociationsButton": "Save Associations", "partitionAssistantTitle": "Partition assistant", "diskSizeLabel": "Size", @@ -278,6 +283,8 @@ "partitionSizeColumn": "Size (MB)", "usageColumn": "Usage (%)", "formatColumn": "Format", + "remotePcLabel": "Remote PC", + "globalImageLabel": "Global image", "ntfsOption": "NTFS", "linuxOption": "LINUX", "cacheOption": "CACHE", @@ -428,7 +435,7 @@ "addClientMenu": "Add client", "filters": "Filters", "searchClient": "Search client", - "searchTree": "Search in tree", + "searchTree": "Search organizational unit", "filterByType": "Filter by type", "all": "All", "classroomsGroup": "Classroom groups", diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index 016e8c1..16355f8 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -157,6 +157,8 @@ "newOrganizationalUnitTooltip": "Abrir modal para crear unidades organizativas de cualquier tipo (Centro, Aula, Grupo de aulas o Grupo de clientes)", "newOrganizationalUnitButton": "Nueva Unidad Organizativa", "newClientButton": "Nuevo Cliente", + "newSingleClientButton": "Añadir cliente unitario", + "newMultipleClientButton": "Añadir clientes masivamente", "keyStepText": "La leyenda te mostrará los tipos de unidades organizativas y sus iconos correspondientes", "legendButton": "Leyenda", "unitStepText": "Esta es la sección donde se mostrarán las unidades organizativas de tipo 'Centro'", @@ -215,7 +217,8 @@ "hardwareProfileLabel": "Perfil de Hardware", "urlFormatError": "Formato de URL inválido.", "validationToggle": "Validación", - "submitButton": "Añadir", + "addOUSubmitButton": "Añadir", + "editOUSubmitButton": "Editar", "addOrgUnitTitle": "Añadir Unidad Organizativa", "createOrgUnitparentLabel": "Unidad organizativa padre", "noParentOption": "--", @@ -271,6 +274,7 @@ "isoImageColumn": "Imagen ISO", "ogliveColumn": "OgLive", "selectImageOption": "Seleccionar imagen", + "deployImage": "Desplegar imagen", "selectOgLiveOption": "Seleccionar OgLive", "saveAssociationsButton": "Guardar Asociaciones", "partitionAssistantTitle": "Asistente de particionado", @@ -296,6 +300,9 @@ "internalUnits": "Unidades internas", "noResultsMessage": "No hay resultados para mostrar.", "imagesTitle": "Administrar imágenes", + "repositoryTitle": "Administrar repositorios", + "remotePcLabel": "Remote PC", + "globalImageLabel": "Imagen Global", "addImageButton": "Añadir imagen", "searchNameDescription": "Busca imágenes por nombre para encontrar rápidamente una imagen específica.", "searchDefaultDescription": "Filtra las imágenes para mostrar solo las imágenes por defecto o no por defecto.", @@ -430,7 +437,7 @@ "addClientMenu": "Añadir cliente", "filters": "Filtros", "searchClient": "Buscar cliente", - "searchTree": "Buscar en árbol", + "searchTree": "Buscar unidad organizativa", "filterByType": "Filtrar por tipo", "all": "Todos", "classroomsGroup": "Grupos de aulas",
{{ column.header }} @@ -30,9 +40,10 @@ Acciones - - + + +