develop #12
|
@ -31,7 +31,7 @@ import {
|
||||||
} from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component";
|
} from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component";
|
||||||
import {RepositoriesComponent} from "./components/repositories/repositories.component";
|
import {RepositoriesComponent} from "./components/repositories/repositories.component";
|
||||||
import {
|
import {
|
||||||
CreateImageComponent
|
CreateClientImageComponent
|
||||||
} from "./components/groups/components/client-main-view/create-image/create-image.component";
|
} from "./components/groups/components/client-main-view/create-image/create-image.component";
|
||||||
import {
|
import {
|
||||||
DeployImageComponent
|
DeployImageComponent
|
||||||
|
@ -63,10 +63,10 @@ const routes: Routes = [
|
||||||
{ path: 'commands-task', component: CommandsTaskComponent },
|
{ path: 'commands-task', component: CommandsTaskComponent },
|
||||||
{ path: 'commands-logs', component: TaskLogsComponent },
|
{ path: 'commands-logs', component: TaskLogsComponent },
|
||||||
{ path: 'calendars', component: CalendarComponent },
|
{ path: 'calendars', component: CalendarComponent },
|
||||||
|
{ path: 'clients/deploy-image', component: DeployImageComponent },
|
||||||
|
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent },
|
||||||
{ path: 'clients/:id', component: ClientMainViewComponent },
|
{ path: 'clients/:id', component: ClientMainViewComponent },
|
||||||
{ path: 'clients/:id/partition-assistant', component: PartitionAssistantComponent },
|
{ path: 'clients/:id/create-image', component: CreateClientImageComponent },
|
||||||
{ path: 'clients/:id/create-image', component: CreateImageComponent },
|
|
||||||
{ path: 'clients/:id/deploy-image', component: DeployImageComponent },
|
|
||||||
{ path: 'images', component: ImagesComponent },
|
{ path: 'images', component: ImagesComponent },
|
||||||
{ path: 'repositories', component: RepositoriesComponent },
|
{ path: 'repositories', component: RepositoriesComponent },
|
||||||
{ path: 'repository/:id', component: MainRepositoryViewComponent },
|
{ path: 'repository/:id', component: MainRepositoryViewComponent },
|
||||||
|
|
|
@ -52,7 +52,6 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||||
import { ToastrModule } from 'ngx-toastr';
|
import { ToastrModule } from 'ngx-toastr';
|
||||||
import { ShowOrganizationalUnitComponent } from './components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component';
|
import { ShowOrganizationalUnitComponent } from './components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component';
|
||||||
import { MatGridList, MatGridTile } from "@angular/material/grid-list";
|
import { MatGridList, MatGridTile } from "@angular/material/grid-list";
|
||||||
import { TreeViewComponent } from './components/groups/shared/tree-view/tree-view.component';
|
|
||||||
import {
|
import {
|
||||||
MatNestedTreeNode,
|
MatNestedTreeNode,
|
||||||
MatTree,
|
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 { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||||
import { ImagesComponent } from './components/images/images.component';
|
import { ImagesComponent } from './components/images/images.component';
|
||||||
import { CreateImageComponent } from './components/images/create-image/create-image.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 { PartitionAssistantComponent } from './components/groups/components/client-main-view/partition-assistant/partition-assistant.component';
|
||||||
import { SoftwareComponent } from './components/software/software.component';
|
import { SoftwareComponent } from './components/software/software.component';
|
||||||
import { CreateSoftwareComponent } from './components/software/create-software/create-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 { MenusComponent } from './components/menus/menus.component';
|
||||||
import { CreateMenuComponent } from './components/menus/create-menu/create-menu.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 { 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) {
|
export function HttpLoaderFactory(http: HttpClient) {
|
||||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||||
}
|
}
|
||||||
|
@ -152,7 +155,6 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||||
ClassroomViewComponent,
|
ClassroomViewComponent,
|
||||||
ClientViewComponent,
|
ClientViewComponent,
|
||||||
ShowOrganizationalUnitComponent,
|
ShowOrganizationalUnitComponent,
|
||||||
TreeViewComponent,
|
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
ClassroomViewDialogComponent,
|
ClassroomViewDialogComponent,
|
||||||
SaveFiltersDialogComponent,
|
SaveFiltersDialogComponent,
|
||||||
|
@ -174,6 +176,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||||
CreateCommandComponent,
|
CreateCommandComponent,
|
||||||
CalendarComponent,
|
CalendarComponent,
|
||||||
CreateCalendarComponent,
|
CreateCalendarComponent,
|
||||||
|
CreateClientImageComponent,
|
||||||
CreateCalendarRuleComponent,
|
CreateCalendarRuleComponent,
|
||||||
CommandsGroupsComponent,
|
CommandsGroupsComponent,
|
||||||
CommandsTaskComponent,
|
CommandsTaskComponent,
|
||||||
|
@ -205,7 +208,10 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||||
EnvVarsComponent,
|
EnvVarsComponent,
|
||||||
MenusComponent,
|
MenusComponent,
|
||||||
CreateMenuComponent,
|
CreateMenuComponent,
|
||||||
CreateMultipleClientComponent
|
CreateMultipleClientComponent,
|
||||||
|
ExportImageComponent,
|
||||||
|
ImportImageComponent,
|
||||||
|
LoadingComponent,
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
imports: [BrowserModule,
|
imports: [BrowserModule,
|
||||||
|
|
|
@ -90,3 +90,10 @@ table {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-container-title {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||||
<mat-icon>help</mat-icon>
|
<mat-icon>help</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div class="header-container-title">
|
||||||
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' | translate }}</h2>
|
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' | translate }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="images-button-row">
|
<div class="images-button-row">
|
||||||
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="resetFiltersStep" text="{{ 'resetFiltersStepText' | translate }}">
|
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="resetFiltersStep" text="{{ 'resetFiltersStepText' | translate }}">
|
||||||
{{ 'resetFilters' | translate }}
|
{{ 'resetFilters' | translate }}
|
||||||
|
|
|
@ -1,10 +1,20 @@
|
||||||
<button mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu">
|
<ng-container [ngSwitch]="buttonType">
|
||||||
<mat-icon>terminal</mat-icon>
|
<button *ngSwitchCase="'icon'" mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu">
|
||||||
</button>
|
<mat-icon>{{ icon }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
<mat-menu #commandMenu="matMenu">
|
<button mat-flat-button [disabled]="clientData.length === 0" *ngSwitchCase="'text'" mat-button color="primary" [matMenuTriggerFor]="commandMenu">
|
||||||
<button mat-menu-item [disabled]="command.disabled" *ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)">
|
{{ buttonText }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<mat-menu #commandMenu="matMenu" >
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="command.disabled || (command.slug === 'create-image' && clientData.length > 1)"
|
||||||
|
*ngFor="let command of arrayCommands"
|
||||||
|
(click)="onCommandSelect(command.slug)"
|
||||||
|
>
|
||||||
{{ command.name }}
|
{{ command.name }}
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|
||||||
|
|
|
@ -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 {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
|
@ -11,7 +11,10 @@ import {ToastrService} from "ngx-toastr";
|
||||||
styleUrls: ['./execute-command.component.css']
|
styleUrls: ['./execute-command.component.css']
|
||||||
})
|
})
|
||||||
export class ExecuteCommandComponent implements OnInit {
|
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;
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
loading: boolean = true;
|
loading: boolean = true;
|
||||||
|
|
||||||
|
@ -29,6 +32,8 @@ export class ExecuteCommandComponent implements OnInit {
|
||||||
{name: 'Ejecutar script', slug: 'run-script', disabled: true},
|
{name: 'Ejecutar script', slug: 'run-script', disabled: true},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
client: any = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
|
@ -39,20 +44,13 @@ export class ExecuteCommandComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.clientData = this.clientData || {};
|
this.clientData = this.clientData || [];
|
||||||
this.loadClient(this.clientData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadClient = (uuid: string) => {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
this.http.get<any>(`${this.baseUrl}${uuid}`).subscribe({
|
if (changes['clientData']) {
|
||||||
next: data => {
|
console.log(this.clientData.length)
|
||||||
this.clientData = data;
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
error: error => {
|
|
||||||
console.error('Error al obtener el cliente:', error);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCommandSelect(action: any): void {
|
onCommandSelect(action: any): void {
|
||||||
|
@ -82,7 +80,9 @@ export class ExecuteCommandComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
rebootClient(): void {
|
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 => {
|
response => {
|
||||||
this.toastService.success('Cliente actualizado correctamente');
|
this.toastService.success('Cliente actualizado correctamente');
|
||||||
},
|
},
|
||||||
|
@ -93,13 +93,11 @@ export class ExecuteCommandComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
powerOnClient(): void {
|
powerOnClient(): void {
|
||||||
const payload = {
|
this.http.post(`${this.baseUrl}/image-repositories/wol`, {
|
||||||
client: this.clientData['@id']
|
clients: this.clientData.map((client: any) => client['@id'])
|
||||||
}
|
}).subscribe(
|
||||||
|
|
||||||
this.http.post(`${this.baseUrl}${this.clientData.repository['@id']}/wol`, payload).subscribe(
|
|
||||||
response => {
|
response => {
|
||||||
this.toastService.success('Cliente actualizado correctamente');
|
this.toastService.success('Petición de encendido enviada correctamente');
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
this.toastService.error('Error de conexión con el cliente');
|
this.toastService.error('Error de conexión con el cliente');
|
||||||
|
@ -108,9 +106,11 @@ export class ExecuteCommandComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
powerOffClient(): void {
|
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 => {
|
response => {
|
||||||
this.toastService.success('Cliente actualizado correctamente');
|
this.toastService.success('Petición de apagado enviada correctamente');
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
this.toastService.error('Error de conexión con el cliente');
|
this.toastService.error('Error de conexión con el cliente');
|
||||||
|
@ -119,21 +119,24 @@ export class ExecuteCommandComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
openPartitionAssistant(): void {
|
openPartitionAssistant(): void {
|
||||||
this.router.navigate([`/clients/${this.clientData.uuid}/partition-assistant`]).then(r => {
|
this.router.navigate(['/clients/partition-assistant'], {
|
||||||
console.log('navigated', r);
|
state: { clientData: this.clientData },
|
||||||
|
}).then(r => {
|
||||||
|
console.log('Navigated to partition assistant with data:', this.clientData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
openCreateImageAssistant(): void {
|
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);
|
console.log('navigated', r);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
openDeployImageAssistant(): void {
|
openDeployImageAssistant(): void {
|
||||||
this.router.navigate([`/clients/${this.clientData.uuid}/deploy-image`]).then(r => {
|
this.router.navigate(['/clients/deploy-image'], {
|
||||||
console.log('navigated', r);
|
state: { clientData: this.clientData },
|
||||||
|
}).then(r => {
|
||||||
|
console.log('Navigated to deploy image with data:', this.clientData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -302,7 +302,7 @@ export class ClientMainViewComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
openDeployImageAssistant(): void {
|
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);
|
console.log('navigated', r);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
<app-loading [isLoading]="loading"></app-loading>
|
||||||
|
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
<div class="header-container-title">
|
||||||
<h2 class="title" i18n="@@subnetsTitle">Crear Imagen desde {{ clientName }}</h2>
|
<h2 >
|
||||||
|
Crear imagen desde {{ clientName }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
<div class="subnets-button-row">
|
<div class="subnets-button-row">
|
||||||
<button mat-flat-button color="primary" (click)="save()">Guardar y ejecutar</button>
|
<button mat-flat-button color="primary" (click)="save()">Ejecutar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
@ -12,14 +17,6 @@
|
||||||
<mat-label>Nombre canónico</mat-label>
|
<mat-label>Nombre canónico</mat-label>
|
||||||
<input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
|
<input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
|
||||||
<mat-label>Seleccione imagen creada previamente</mat-label>
|
|
||||||
<mat-select [(ngModel)]="selectedImage">
|
|
||||||
<mat-option>--</mat-option>
|
|
||||||
<mat-option *ngFor="let image of images" [value]="image['@id']">{{ image.name }}</mat-option>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||||
|
|
|
@ -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<CreateImageComponent>;
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -2,61 +2,14 @@ import {Component, EventEmitter, Output} from '@angular/core';
|
||||||
import {HttpClient} from "@angular/common/http";
|
import {HttpClient} from "@angular/common/http";
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
import {ActivatedRoute, Router} from "@angular/router";
|
import {ActivatedRoute, Router} from "@angular/router";
|
||||||
import {MatButton} from "@angular/material/button";
|
import {MatTableDataSource} from "@angular/material/table";
|
||||||
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 {SelectionModel} from "@angular/cdk/collections";
|
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({
|
@Component({
|
||||||
selector: 'app-create-image',
|
selector: 'app-create-image',
|
||||||
templateUrl: './create-image.component.html',
|
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'
|
styleUrl: './create-image.component.css'
|
||||||
})
|
})
|
||||||
export class CreateImageComponent {
|
export class CreateClientImageComponent {
|
||||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
@Output() dataChange = new EventEmitter<any>();
|
@Output() dataChange = new EventEmitter<any>();
|
||||||
|
|
||||||
|
@ -65,10 +18,10 @@ export class CreateImageComponent {
|
||||||
partitions: any[] = [];
|
partitions: any[] = [];
|
||||||
images: any[] = [];
|
images: any[] = [];
|
||||||
clientName: string = '';
|
clientName: string = '';
|
||||||
selectedImage: string | null = null;
|
|
||||||
selectedPartition: any = null;
|
selectedPartition: any = null;
|
||||||
name: string = '';
|
name: string = '';
|
||||||
client: any = null;
|
client: any = null;
|
||||||
|
loading: boolean = false;
|
||||||
dataSource = new MatTableDataSource<any>();
|
dataSource = new MatTableDataSource<any>();
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
|
@ -111,7 +64,6 @@ export class CreateImageComponent {
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.clientId = this.route.snapshot.paramMap.get('id');
|
this.clientId = this.route.snapshot.paramMap.get('id');
|
||||||
|
|
||||||
this.loadPartitions();
|
this.loadPartitions();
|
||||||
this.loadImages();
|
this.loadImages();
|
||||||
}
|
}
|
||||||
|
@ -147,15 +99,12 @@ export class CreateImageComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
back() {
|
|
||||||
this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
|
|
||||||
}
|
|
||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
client: `/clients/${this.clientId}`,
|
client: `/clients/${this.clientId}`,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
image: this.selectedImage,
|
|
||||||
partition: this.selectedPartition['@id'],
|
partition: this.selectedPartition['@id'],
|
||||||
source: 'assistant'
|
source: 'assistant'
|
||||||
};
|
};
|
||||||
|
@ -165,11 +114,12 @@ export class CreateImageComponent {
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.toastService.success('Petición de creación de imagen enviada');
|
this.toastService.success('Petición de creación de imagen enviada');
|
||||||
|
this.loading = false;
|
||||||
this.router.navigate(['/images']);
|
this.router.navigate(['/images']);
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Error:', error);
|
|
||||||
this.toastService.error(error.error['hydra:description']);
|
this.toastService.error(error.error['hydra:description']);
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -37,6 +37,7 @@ table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
padding-left: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group {
|
.input-group {
|
||||||
|
@ -70,7 +71,6 @@ table {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-elevation-z8 {
|
.mat-elevation-z8 {
|
||||||
|
@ -82,3 +82,47 @@ table {
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
margin-bottom: 30px;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,45 @@
|
||||||
|
<app-loading [isLoading]="loading"></app-loading>
|
||||||
|
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
<div class="header-container-title">
|
||||||
<h2 class="title" i18n="@@subnetsTitle">Desplegar imagen en {{ clientName }}</h2>
|
<h2>
|
||||||
|
{{ 'deployImage' | translate }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
<div class="subnets-button-row">
|
<div class="subnets-button-row">
|
||||||
<button mat-flat-button color="primary" (click)="save()">Guardar</button>
|
<button mat-flat-button color="primary" (click)="save()">Ejecutar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
<div class="select-container">
|
||||||
|
<mat-expansion-panel hideToggle>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title> Clientes </mat-panel-title>
|
||||||
|
<mat-panel-description> Listado de clientes donde se desplegará la imagen </mat-panel-description>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
|
||||||
|
<div class="clients-grid" >
|
||||||
|
<div *ngFor="let client of clientData" class="client-item">
|
||||||
|
<div class="client-card">
|
||||||
|
<img
|
||||||
|
[src]="'assets/images/ordenador_' + client.status + '.png'"
|
||||||
|
alt="Client Icon"
|
||||||
|
class="client-image" />
|
||||||
|
|
||||||
|
<div class="client-details">
|
||||||
|
<span class="client-name">{{ client.name }}</span>
|
||||||
|
<span class="client-ip">{{ client.ip }}</span>
|
||||||
|
<span class="client-ip">{{ client.mac }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-divider style="margin-top: 20px;"></mat-divider>
|
||||||
|
|
||||||
<div class="select-container">
|
<div class="select-container">
|
||||||
<div class="option-container">
|
<div class="option-container">
|
||||||
<mat-radio-group [(ngModel)]="selectedOption" name="selectedOption" aria-label="Selecciona una opcion">
|
<mat-radio-group [(ngModel)]="selectedOption" name="selectedOption" aria-label="Selecciona una opcion">
|
||||||
|
@ -84,12 +117,12 @@
|
||||||
|
|
||||||
<mat-form-field appearance="fill" class="input-field">
|
<mat-form-field appearance="fill" class="input-field">
|
||||||
<mat-label>Máximo Clientes</mat-label>
|
<mat-label>Máximo Clientes</mat-label>
|
||||||
<input matInput [(ngModel)]="mcastMaxClients" name="mcastMaxClients">
|
<input matInput [(ngModel)]="mcastMaxClients" name="mcastMaxClients" type="number">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="fill" class="input-field">
|
<mat-form-field appearance="fill" class="input-field">
|
||||||
<mat-label>Tiempo Máximo de Espera</mat-label>
|
<mat-label>Tiempo Máximo de Espera</mat-label>
|
||||||
<input matInput [(ngModel)]="mcastMaxTime" name="mcastMaxTime">
|
<input matInput [(ngModel)]="mcastMaxTime" name="mcastMaxTime" type="number">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import {MatExpansionModule} from "@angular/material/expansion";
|
||||||
|
import {LoadingComponent} from "../../../../../shared/loading/loading.component";
|
||||||
|
|
||||||
describe('DeployImageComponent', () => {
|
describe('DeployImageComponent', () => {
|
||||||
let component: DeployImageComponent;
|
let component: DeployImageComponent;
|
||||||
|
@ -24,7 +26,7 @@ describe('DeployImageComponent', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [DeployImageComponent],
|
declarations: [DeployImageComponent, LoadingComponent],
|
||||||
imports: [
|
imports: [
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
@ -32,6 +34,7 @@ describe('DeployImageComponent', () => {
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
|
MatExpansionModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatTableModule,
|
MatTableModule,
|
||||||
MatDividerModule,
|
MatDividerModule,
|
||||||
|
|
|
@ -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 {MatTableDataSource} from "@angular/material/table";
|
||||||
import {SelectionModel} from "@angular/cdk/collections";
|
import {SelectionModel} from "@angular/cdk/collections";
|
||||||
import {HttpClient} from "@angular/common/http";
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
@ -33,11 +33,13 @@ export class DeployImageComponent {
|
||||||
p2pTime: Number = 0;
|
p2pTime: Number = 0;
|
||||||
name: string = '';
|
name: string = '';
|
||||||
client: any = null;
|
client: any = null;
|
||||||
|
clientData: any = [];
|
||||||
|
loading: boolean = false;
|
||||||
|
|
||||||
protected p2pModeOptions = [
|
protected p2pModeOptions = [
|
||||||
{ name: 'Leecher', value: 'p2p-mode-leecher' },
|
{ name: 'Leecher', value: 'leecher' },
|
||||||
{ name: 'Peer', value: 'p2p-mode-peer' },
|
{ name: 'Peer', value: 'peer' },
|
||||||
{ name: 'Seeder', value: 'p2p-mode-seeder' },
|
{ name: 'Seeder', value: 'seeder' },
|
||||||
];
|
];
|
||||||
protected multicastModeOptions = [
|
protected multicastModeOptions = [
|
||||||
{ name: 'Half duplex', value: "half"},
|
{ name: 'Half duplex', value: "half"},
|
||||||
|
@ -47,7 +49,6 @@ export class DeployImageComponent {
|
||||||
allMethods = [
|
allMethods = [
|
||||||
'uftp',
|
'uftp',
|
||||||
'udpcast',
|
'udpcast',
|
||||||
'multicast-direct',
|
|
||||||
'unicast',
|
'unicast',
|
||||||
'unicast-direct',
|
'unicast-direct',
|
||||||
'p2p'
|
'p2p'
|
||||||
|
@ -56,7 +57,6 @@ export class DeployImageComponent {
|
||||||
updateCacheMethods = [
|
updateCacheMethods = [
|
||||||
'uftp',
|
'uftp',
|
||||||
'udpcast',
|
'udpcast',
|
||||||
'multicast',
|
|
||||||
'unicast',
|
'unicast',
|
||||||
'p2p'
|
'p2p'
|
||||||
];
|
];
|
||||||
|
@ -98,13 +98,12 @@ export class DeployImageComponent {
|
||||||
private toastService: ToastrService,
|
private toastService: ToastrService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
) {}
|
) {
|
||||||
|
const navigation = this.router.getCurrentNavigation();
|
||||||
ngOnInit() {
|
this.clientData = navigation?.extras?.state?.['clientData'];
|
||||||
this.clientId = this.route.snapshot.paramMap.get('id');
|
this.clientId = this.clientData?.[0]['@id'];
|
||||||
this.selectedOption = 'deploy-image';
|
|
||||||
this.loadPartitions();
|
|
||||||
this.loadImages();
|
this.loadImages();
|
||||||
|
this.loadPartitions()
|
||||||
}
|
}
|
||||||
|
|
||||||
get deployMethods() {
|
get deployMethods() {
|
||||||
|
@ -116,7 +115,7 @@ export class DeployImageComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPartitions() {
|
loadPartitions() {
|
||||||
const url = `${this.baseUrl}/clients/${this.clientId}`;
|
const url = `${this.baseUrl}${this.clientId}`;
|
||||||
this.http.get(url).subscribe(
|
this.http.get(url).subscribe(
|
||||||
(response: any) => {
|
(response: any) => {
|
||||||
if (response.partitions) {
|
if (response.partitions) {
|
||||||
|
@ -151,11 +150,9 @@ export class DeployImageComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
back() {
|
|
||||||
this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
|
|
||||||
}
|
|
||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
if (!this.selectedImage) {
|
if (!this.selectedImage) {
|
||||||
this.toastService.error('Debe seleccionar una imagen');
|
this.toastService.error('Debe seleccionar una imagen');
|
||||||
return;
|
return;
|
||||||
|
@ -171,26 +168,44 @@ export class DeployImageComponent {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.toastService.info('Preparando petición de despliegue');
|
||||||
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
client: `/clients/${this.clientId}`,
|
clients: this.clientData.map((client: any) => client['@id']),
|
||||||
method: this.selectedMethod,
|
method: this.selectedMethod,
|
||||||
partition: this.selectedPartition['@id'],
|
// partition: this.selectedPartition['@id'],
|
||||||
|
diskNumber: this.selectedPartition.diskNumber,
|
||||||
|
partitionNumber: this.selectedPartition.partitionNumber,
|
||||||
p2pMode: this.p2pMode,
|
p2pMode: this.p2pMode,
|
||||||
p2pTime: this.p2pTime,
|
p2pTime: this.p2pTime,
|
||||||
mcastIp: this.mcastIp,
|
mcastIp: this.mcastIp,
|
||||||
mcastPort: this.mcastPort,
|
mcastPort: this.mcastPort,
|
||||||
mcastMode: this.mcastMode,
|
mcastMode: this.mcastMode,
|
||||||
mcastSpeed: this.mcastSpeed,
|
mcastSpeed: this.mcastSpeed,
|
||||||
|
maxTime: this.mcastMaxTime,
|
||||||
|
maxClients: this.mcastMaxClients,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.http.post(`${this.baseUrl}${this.selectedImage}/deploy-image`, payload)
|
this.http.post(`${this.baseUrl}${this.selectedImage}/deploy-image`, payload)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.toastService.success('Petición de despliegue enviada correctamente');
|
this.toastService.success('Petición de despliegue enviada correctamente');
|
||||||
|
this.loading = false;
|
||||||
|
this.router.navigate(['/commands-logs']);
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -167,3 +167,58 @@ button.remove-btn:hover {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin: 10px auto;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,45 @@
|
||||||
|
<app-loading [isLoading]="loading"></app-loading>
|
||||||
|
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
<div class="header-container-title">
|
||||||
<h2 class="title" i18n="@@subnetsTitle">Asistente de particionado</h2>
|
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
||||||
|
Asistente de particionado
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
<div class="subnets-button-row">
|
<div class="subnets-button-row">
|
||||||
<button mat-flat-button color="primary" [disabled]="data.status === 'busy'" (click)="save()">Ejecutar</button>
|
<button mat-flat-button color="primary" [disabled]="data.status === 'busy'" (click)="save()">Ejecutar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
|
<div class="select-container">
|
||||||
|
<mat-expansion-panel hideToggle>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title> Clientes </mat-panel-title>
|
||||||
|
<mat-panel-description> Listado de clientes donde se realizará el particionado </mat-panel-description>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
|
||||||
|
<div class="clients-grid" >
|
||||||
|
<div *ngFor="let client of clientData" class="client-item">
|
||||||
|
<div class="client-card">
|
||||||
|
<img
|
||||||
|
[src]="'assets/images/ordenador_' + client.status + '.png'"
|
||||||
|
alt="Client Icon"
|
||||||
|
class="client-image" />
|
||||||
|
|
||||||
|
<div class="client-details">
|
||||||
|
<span class="client-name">{{ client.name }}</span>
|
||||||
|
<span class="client-ip">{{ client.ip }}</span>
|
||||||
|
<span class="client-ip">{{ client.mac }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mat-divider style="margin-top: 20px;"></mat-divider>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div class="disk-select">
|
<div class="disk-select">
|
||||||
<mat-form-field appearance="fill">
|
<mat-form-field appearance="fill">
|
||||||
|
|
|
@ -26,7 +26,7 @@ interface Partition {
|
||||||
templateUrl: './partition-assistant.component.html',
|
templateUrl: './partition-assistant.component.html',
|
||||||
styleUrls: ['./partition-assistant.component.css']
|
styleUrls: ['./partition-assistant.component.css']
|
||||||
})
|
})
|
||||||
export class PartitionAssistantComponent implements OnInit {
|
export class PartitionAssistantComponent {
|
||||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
@Output() dataChange = new EventEmitter<any>();
|
@Output() dataChange = new EventEmitter<any>();
|
||||||
partitionTypes = PARTITION_TYPES;
|
partitionTypes = PARTITION_TYPES;
|
||||||
|
@ -39,6 +39,8 @@ export class PartitionAssistantComponent implements OnInit {
|
||||||
updateRequests: any[] = [];
|
updateRequests: any[] = [];
|
||||||
data: any = {};
|
data: any = {};
|
||||||
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = [];
|
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = [];
|
||||||
|
clientData: any = [];
|
||||||
|
loading: boolean = false;
|
||||||
|
|
||||||
private apiUrl: string = this.baseUrl + '/partitions';
|
private apiUrl: string = this.baseUrl + '/partitions';
|
||||||
|
|
||||||
|
@ -51,11 +53,12 @@ export class PartitionAssistantComponent implements OnInit {
|
||||||
private toastService: ToastrService,
|
private toastService: ToastrService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
) {}
|
) {
|
||||||
|
const navigation = this.router.getCurrentNavigation();
|
||||||
ngOnInit() {
|
this.clientData = navigation?.extras?.state?.['clientData'];
|
||||||
this.clientId = this.route.snapshot.paramMap.get('id');
|
this.clientId = this.clientData[0]['@id'];
|
||||||
this.loadPartitions();
|
this.loadPartitions();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectedDisk():any {
|
get selectedDisk():any {
|
||||||
|
@ -63,7 +66,7 @@ export class PartitionAssistantComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPartitions() {
|
loadPartitions() {
|
||||||
const url = `${this.baseUrl}/clients/${this.clientId}`;
|
const url = `${this.baseUrl}${this.clientId}`;
|
||||||
this.http.get(url).subscribe(
|
this.http.get(url).subscribe(
|
||||||
(response) => {
|
(response) => {
|
||||||
this.data = response;
|
this.data = response;
|
||||||
|
@ -250,16 +253,14 @@ export class PartitionAssistantComponent implements OnInit {
|
||||||
return modifiedPartitions;
|
return modifiedPartitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
back() {
|
|
||||||
this.router.navigate(['clients', this.data.uuid], { state: { clientData: this.data } });
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
if (!this.selectedDisk) {
|
if (!this.selectedDisk) {
|
||||||
this.errorMessage = 'Por favor selecciona un disco antes de guardar.';
|
this.errorMessage = 'Por favor selecciona un disco antes de guardar.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
const totalPartitionSize = this.selectedDisk.partitions.reduce((sum: any, partition: { size: any; }) => sum + partition.size, 0);
|
const totalPartitionSize = this.selectedDisk.partitions.reduce((sum: any, partition: { size: any; }) => sum + partition.size, 0);
|
||||||
|
|
||||||
if (totalPartitionSize > this.selectedDisk.totalDiskSize) {
|
if (totalPartitionSize > this.selectedDisk.totalDiskSize) {
|
||||||
|
@ -283,22 +284,26 @@ export class PartitionAssistantComponent implements OnInit {
|
||||||
size: partition.size,
|
size: partition.size,
|
||||||
partitionCode: partition.partitionCode,
|
partitionCode: partition.partitionCode,
|
||||||
filesystem: partition.filesystem,
|
filesystem: partition.filesystem,
|
||||||
client: `/clients/${this.clientId}`,
|
|
||||||
uuid: partition.uuid,
|
uuid: partition.uuid,
|
||||||
removed: partition.removed || false,
|
removed: partition.removed || false,
|
||||||
format: partition.format || false,
|
format: partition.format || false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (newPartitions.length > 0) {
|
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(
|
this.http.post(this.apiUrl, bulkPayload).subscribe(
|
||||||
(response) => {
|
(response) => {
|
||||||
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
|
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
|
||||||
|
this.loading = false;
|
||||||
this.router.navigate(['/commands-logs']);
|
this.router.navigate(['/commands-logs']);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('Error al crear las particiones:', error);
|
console.error('Error al crear las particiones:', error);
|
||||||
|
this.loading = false;
|
||||||
this.toastService.error('Error al crear las particiones.');
|
this.toastService.error('Error al crear las particiones.');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
<h2>{{ 'diskImageAssistantTitle' | translate }}</h2>
|
|
||||||
<div *ngFor="let disk of disks" class="partition-assistant">
|
|
||||||
<div class="header">
|
|
||||||
<label>{{ 'diskLabel' | translate }} {{ disk.diskNumber }}</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="partition-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ 'partitionColumn' | translate }}</th>
|
|
||||||
<th>{{ 'isoImageColumn' | translate }}</th>
|
|
||||||
<th>{{ 'ogliveColumn' | translate }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let partition of disk.partitions">
|
|
||||||
<td>{{ partition.partitionNumber }}</td>
|
|
||||||
<td>
|
|
||||||
<select [(ngModel)]="partition.associatedImageId" (change)="onImageSelected(partition, $event)" name="associatedImage-{{partition.partitionNumber}}">
|
|
||||||
<option value="">{{ 'selectImageOption' | translate }}</option>
|
|
||||||
<option *ngFor="let image of availableImages" [value]="image['@id']">
|
|
||||||
{{ image.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<select (change)="onOgLiveSelected(partition, $event)">
|
|
||||||
<option value="">{{ 'selectOgLiveOption' | translate }}</option>
|
|
||||||
<option *ngFor="let ogLive of availableOgLives" [value]="ogLive">{{ ogLive }}</option>
|
|
||||||
</select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<button mat-flat-button color="primary" (click)="saveAssociations()">{{ 'saveAssociationsButton' | translate }}</button>
|
|
||||||
</div>
|
|
|
@ -3,8 +3,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 10px 10px;
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,58 +32,6 @@ button[mat-raised-button] {
|
||||||
font-size: 16px;
|
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 {
|
.actions mat-icon {
|
||||||
color: #757575;
|
color: #757575;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -186,7 +133,7 @@ button[mat-raised-button] {
|
||||||
|
|
||||||
mat-tree {
|
mat-tree {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
padding: 10px;
|
padding: 0px 10px 10px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-tree mat-tree-node {
|
mat-tree mat-tree-node {
|
||||||
|
@ -298,18 +245,14 @@ mat-tree mat-tree-node.disabled:hover {
|
||||||
|
|
||||||
.filters-container {
|
.filters-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
justify-content: start;
|
||||||
gap: 16px;
|
gap: 1rem;
|
||||||
margin-bottom: 16px;
|
margin: 2rem 0px 0.7rem 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters-container mat-form-field {
|
.filters-container mat-form-field {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
max-width: 300px;
|
max-width: 250px;
|
||||||
}
|
|
||||||
|
|
||||||
.filter-container {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip-busy {
|
.chip-busy {
|
||||||
|
@ -394,24 +337,17 @@ mat-tree mat-tree-node.disabled:hover {
|
||||||
|
|
||||||
.tree-container {
|
.tree-container {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
padding: 16px;
|
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clients-container {
|
.clients-container {
|
||||||
width: 75%;
|
width: 75%;
|
||||||
padding: 16px;
|
padding: 0px 16px 16px 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clients-container h3 {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 1.5em;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-item {
|
.client-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -516,23 +452,10 @@ button[mat-raised-button] {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
margin: 16px 0;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-elevation-z8 {
|
.mat-elevation-z8 {
|
||||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
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 {
|
.client-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -586,7 +509,7 @@ button[mat-raised-button] {
|
||||||
.clients-title-name {
|
.clients-title-name {
|
||||||
font-size: x-large;
|
font-size: x-large;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 1rem 1rem 1rem 15px;
|
padding: 1rem 1rem 1rem 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-clients-info {
|
.no-clients-info {
|
||||||
|
@ -594,4 +517,5 @@ button[mat-raised-button] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
|
margin-left: 16px;
|
||||||
}
|
}
|
|
@ -15,8 +15,8 @@
|
||||||
</button>
|
</button>
|
||||||
<button mat-flat-button color="primary" [matMenuTriggerFor]="menuClients">{{ 'newClientButton' | translate }}</button>
|
<button mat-flat-button color="primary" [matMenuTriggerFor]="menuClients">{{ 'newClientButton' | translate }}</button>
|
||||||
<mat-menu #menuClients="matMenu">
|
<mat-menu #menuClients="matMenu">
|
||||||
<button mat-menu-item (click)="addClient($event)" >Añadir cliente unitario</button>
|
<button mat-menu-item (click)="addClient($event)">{{ 'newSingleClientButton' | translate }}</button>
|
||||||
<button mat-menu-item (click)="addMultipleClients($event)">Añadir clientes masivamente</button>
|
<button mat-menu-item (click)="addMultipleClients($event)">{{ 'newMultipleClientButton' | translate }}</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
|
|
||||||
<button mat-flat-button (click)="openBottomSheet()" joyrideStep="keyStep" text="{{ 'keyStepText' | translate }}"
|
<button mat-flat-button (click)="openBottomSheet()" joyrideStep="keyStep" text="{{ 'keyStepText' | translate }}"
|
||||||
|
@ -27,26 +27,43 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters Panel -->
|
<!-- Filters Panel -->
|
||||||
<mat-expansion-panel *ngIf="isTreeViewActive" class="filters-panel" joyrideStep="filtersPanelStep" text="{{ 'filtersPanelStepText' | translate }}">
|
<div class="filters-panel" joyrideStep="filtersPanelStep" text="{{ 'filtersPanelStepText' | translate }}">
|
||||||
<mat-expansion-panel-header>
|
|
||||||
<mat-panel-title>{{ 'filters' | translate }}</mat-panel-title>
|
|
||||||
</mat-expansion-panel-header>
|
|
||||||
<div class="filters-container">
|
<div class="filters-container">
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>{{ 'searchClient' | translate }}</mat-label>
|
<mat-label>{{ 'searchTree' | translate }}</mat-label>
|
||||||
<input matInput (input)="onClientFilterInput($event)" placeholder="Buscar nombre, IP, estado o MAC">
|
<input matInput #treeSearchInput (input)="onTreeFilterInput($event)" placeholder="Centro, aula, grupos ..." />
|
||||||
|
<button
|
||||||
|
*ngIf="treeSearchInput.value"
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
aria-label="Clear tree search"
|
||||||
|
(click)="clearTreeSearch(treeSearchInput)"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
|
<mat-label>{{ 'searchClient' | translate }}</mat-label>
|
||||||
|
<input matInput #clientSearchInput (input)="onClientFilterInput($event)" placeholder="Nombre, IP, estado o MAC" />
|
||||||
|
<button
|
||||||
|
*ngIf="clientSearchInput.value"
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
aria-label="Clear client search"
|
||||||
|
(click)="clearClientSearch(clientSearchInput)"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<!-- Funcionalidad actualmente deshabilitada-->
|
||||||
|
<!-- <mat-form-field appearance="outline">
|
||||||
<mat-select (selectionChange)="loadSelectedFilter($event.value)" placeholder="Cargar filtro" disabled>
|
<mat-select (selectionChange)="loadSelectedFilter($event.value)" placeholder="Cargar filtro" disabled>
|
||||||
<mat-option *ngFor="let savedFilter of savedFilterNames" [value]="savedFilter">
|
<mat-option *ngFor="let savedFilter of savedFilterNames" [value]="savedFilter">
|
||||||
{{ savedFilter[0] }}
|
{{ savedFilter[0] }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline">
|
|
||||||
<mat-label>{{ 'searchTree' | translate }}</mat-label>
|
|
||||||
<input matInput (input)="onTreeFilterInput($event)" placeholder="Buscar nombre o tipo" disabled>
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>{{ 'filterByType' | translate }}</mat-label>
|
<mat-label>{{ 'filterByType' | translate }}</mat-label>
|
||||||
<mat-select [(value)]="selectedTreeFilter" (selectionChange)="filterTree(searchTerm, $event.value)" disabled>
|
<mat-select [(value)]="selectedTreeFilter" (selectionChange)="filterTree(searchTerm, $event.value)" disabled>
|
||||||
|
@ -55,16 +72,16 @@
|
||||||
<mat-option value="classroom">{{ 'classrooms' | translate }}</mat-option>
|
<mat-option value="classroom">{{ 'classrooms' | translate }}</mat-option>
|
||||||
<mat-option value="group">{{ 'computerGroups' | translate }}</mat-option>
|
<mat-option value="group">{{ 'computerGroups' | translate }}</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field> -->
|
||||||
</div>
|
</div>
|
||||||
</mat-expansion-panel>
|
</div>
|
||||||
|
|
||||||
<!-- Unit details view-->
|
<!-- Unit details view-->
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<!-- Tree view -->
|
<!-- Tree view -->
|
||||||
<div class="tree-container">
|
<div class="tree-container">
|
||||||
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
||||||
<mat-tree-node [ngClass]="{'selected-node': node === selectedNode}" *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick(node)">
|
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}" *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick(node)">
|
||||||
<button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable" [ngClass]="{'disabled-toggle': !node.expandable}">
|
<button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable" [ngClass]="{'disabled-toggle': !node.expandable}">
|
||||||
<mat-icon>{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}</mat-icon>
|
<mat-icon>{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
@ -83,7 +100,7 @@
|
||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-tree-node>
|
</mat-tree-node>
|
||||||
<mat-tree-node [ngClass]="{'selected-node': node === selectedNode}" *matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding (click)="onNodeClick(node)">
|
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}" *matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding (click)="onNodeClick(node)">
|
||||||
<button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle"></button>
|
<button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle"></button>
|
||||||
<mat-icon style="color: green;">
|
<mat-icon style="color: green;">
|
||||||
{{
|
{{
|
||||||
|
@ -125,20 +142,20 @@
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="addClient($event, selectedNode)">
|
<button mat-menu-item (click)="addClient($event, selectedNode)">
|
||||||
<mat-icon>add</mat-icon>
|
<mat-icon>add</mat-icon>
|
||||||
<span>{{ 'addClientMenu' | translate }}</span>
|
<span>{{ 'newSingleClientButton' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
<button mat-menu-item (click)="addMultipleClients($event, selectedNode)">
|
||||||
|
<mat-icon>playlist_add</mat-icon>
|
||||||
|
<span>{{ 'newMultipleClientButton' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="addOU($event, selectedNode)">
|
<button mat-menu-item (click)="addOU($event, selectedNode)">
|
||||||
<mat-icon>playlist_add</mat-icon>
|
<mat-icon>account_tree</mat-icon>
|
||||||
<span>{{ 'addOrganizationalUnit' | translate }}</span>
|
<span>{{ 'addOrganizationalUnit' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="onEditNode($event, selectedNode)">
|
<button mat-menu-item (click)="onEditNode($event, selectedNode)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
<span>{{ 'edit' | translate }}</span>
|
<span>{{ 'edit' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="selectedNode && onTreeClick($event, selectedNode)">
|
|
||||||
<mat-icon matTooltip="{{ 'viewTreeTooltip' | translate }}" matTooltipHideDelay="0">account_tree</mat-icon>
|
|
||||||
<span>{{ 'viewTreeMenu' | translate }}</span>
|
|
||||||
</button>
|
|
||||||
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)">
|
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span>{{ 'delete' | translate }}</span>
|
<span>{{ 'delete' | translate }}</span>
|
||||||
|
@ -152,6 +169,11 @@
|
||||||
<strong>{{ selectedNode?.name }}</strong>
|
<strong>{{ selectedNode?.name }}</strong>
|
||||||
</span>
|
</span>
|
||||||
<div class="view-type-container">
|
<div class="view-type-container">
|
||||||
|
<app-execute-command
|
||||||
|
[clientData]="arrayClients"
|
||||||
|
[buttonType]="'text'"
|
||||||
|
[buttonText]="'Ejecutar comandos'"
|
||||||
|
></app-execute-command>
|
||||||
<button mat-button color="primary" (click)="toggleView('card')" [disabled]="currentView === 'card'">
|
<button mat-button color="primary" (click)="toggleView('card')" [disabled]="currentView === 'card'">
|
||||||
<mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
|
<mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -193,7 +215,11 @@
|
||||||
<button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)">
|
<button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)">
|
||||||
<mat-icon>visibility</mat-icon>
|
<mat-icon>visibility</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<app-execute-command [clientData]="client['@id']"></app-execute-command>
|
<app-execute-command
|
||||||
|
[clientData]="[client]"
|
||||||
|
[buttonType]="'icon'"
|
||||||
|
[icon]="'terminal'"
|
||||||
|
></app-execute-command>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -202,9 +228,27 @@
|
||||||
<!-- List view -->
|
<!-- List view -->
|
||||||
<div class="clients-table" *ngIf="currentView === 'list'">
|
<div class="clients-table" *ngIf="currentView === 'list'">
|
||||||
<table mat-table matSort [dataSource]="selectedClients" class="mat-elevation-z8">
|
<table mat-table matSort [dataSource]="selectedClients" class="mat-elevation-z8">
|
||||||
|
<ng-container matColumnDef="select">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>
|
||||||
|
<mat-checkbox (change)="$event ? toggleAllRows() : null"
|
||||||
|
[checked]="selection.hasValue() && isAllSelected()"
|
||||||
|
[indeterminate]="selection.hasValue() && !isAllSelected()"
|
||||||
|
>
|
||||||
|
</mat-checkbox>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let row">
|
||||||
|
<mat-checkbox (click)="$event.stopPropagation()"
|
||||||
|
(change)="toggleRow(row)"
|
||||||
|
[checked]="selection.isSelected(row)"
|
||||||
|
[disabled]="row.status === 'busy'"
|
||||||
|
>
|
||||||
|
</mat-checkbox>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
<ng-container matColumnDef="status">
|
<ng-container matColumnDef="status">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
|
||||||
<td mat-cell *matCellDef="let client">
|
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
|
||||||
|
matTooltipPosition="left" matTooltipShowDelay="500">
|
||||||
<img
|
<img
|
||||||
[src]="'assets/images/ordenador_' + client.status + '.png'"
|
[src]="'assets/images/ordenador_' + client.status + '.png'"
|
||||||
alt="Client Icon"
|
alt="Client Icon"
|
||||||
|
@ -231,7 +275,8 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="name">
|
<ng-container matColumnDef="name">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
|
||||||
<td mat-cell *matCellDef="let client">
|
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
|
||||||
|
matTooltipPosition="left" matTooltipShowDelay="500">
|
||||||
<div class="client-info">
|
<div class="client-info">
|
||||||
<div class="client-name">{{ client.name }}</div>
|
<div class="client-name">{{ client.name }}</div>
|
||||||
<div class="client-ip">{{ client.ip }}</div>
|
<div class="client-ip">{{ client.ip }}</div>
|
||||||
|
@ -268,7 +313,11 @@
|
||||||
<button mat-icon-button [matMenuTriggerFor]="clientMenu">
|
<button mat-icon-button [matMenuTriggerFor]="clientMenu">
|
||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<app-execute-command [clientData]="client['@id']"></app-execute-command>
|
<app-execute-command
|
||||||
|
[clientData]="[client]"
|
||||||
|
[buttonType]="'icon'"
|
||||||
|
[icon]="'terminal'"
|
||||||
|
></app-execute-command>
|
||||||
<mat-menu #clientMenu="matMenu">
|
<mat-menu #clientMenu="matMenu">
|
||||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
|
@ -278,7 +327,7 @@
|
||||||
<mat-icon>visibility</mat-icon>
|
<mat-icon>visibility</mat-icon>
|
||||||
<span>{{ 'viewDetails' | translate }}</span>
|
<span>{{ 'viewDetails' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="onDeleteClick($event, client, selectedNode)">
|
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span>{{ 'delete' | translate }}</span>
|
<span>{{ 'delete' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -288,7 +337,7 @@
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||||
</table>
|
</table>
|
||||||
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5, 10, 20]" showFirstLastButtons></mat-paginator>
|
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5, 10, 20, 50]" showFirstLastButtons></mat-paginator>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -299,8 +348,8 @@
|
||||||
<mat-spinner></mat-spinner>
|
<mat-spinner></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!isLoadingClients" class="no-clients-info">
|
<div *ngIf="!isLoadingClients" class="no-clients-info">
|
||||||
<mat-icon>error_outline</mat-icon>
|
|
||||||
<span>{{ 'noClients' | translate }}</span>
|
<span>{{ 'noClients' | translate }}</span>
|
||||||
|
<mat-icon>error_outline</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { JoyrideModule } from 'ngx-joyride';
|
import { JoyrideModule } from 'ngx-joyride';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatTreeModule } from '@angular/material/tree';
|
import { MatTreeModule } from '@angular/material/tree';
|
||||||
|
import { TreeNode } from './model/model';
|
||||||
|
import {ExecuteCommandComponent} from "../commands/main-commands/execute-command/execute-command.component";
|
||||||
|
|
||||||
describe('GroupsComponent', () => {
|
describe('GroupsComponent', () => {
|
||||||
let component: GroupsComponent;
|
let component: GroupsComponent;
|
||||||
|
@ -31,7 +33,7 @@ describe('GroupsComponent', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [GroupsComponent],
|
declarations: [GroupsComponent, ExecuteCommandComponent],
|
||||||
imports: [
|
imports: [
|
||||||
HttpClientTestingModule,
|
HttpClientTestingModule,
|
||||||
ToastrModule.forRoot(),
|
ToastrModule.forRoot(),
|
||||||
|
@ -80,21 +82,43 @@ describe('GroupsComponent', () => {
|
||||||
expect(component.search).toHaveBeenCalled();
|
expect(component.search).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getFilters on ngOnInit', () => {
|
|
||||||
spyOn(component, 'getFilters');
|
|
||||||
component.ngOnInit();
|
|
||||||
expect(component.getFilters).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call search method', () => {
|
it('should call search method', () => {
|
||||||
spyOn(component, 'search');
|
spyOn(component, 'search');
|
||||||
component.search();
|
component.search();
|
||||||
expect(component.search).toHaveBeenCalled();
|
expect(component.search).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getFilters method', () => {
|
it('should clear selection', () => {
|
||||||
spyOn(component, 'getFilters');
|
spyOn(component, 'clearSelection');
|
||||||
component.getFilters();
|
component.clearSelection();
|
||||||
expect(component.getFilters).toHaveBeenCalled();
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { HttpClient } from '@angular/common/http';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatBottomSheet } from '@angular/material/bottom-sheet';
|
import { MatBottomSheet } from '@angular/material/bottom-sheet';
|
||||||
import { MatTabChangeEvent } from '@angular/material/tabs';
|
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { JoyrideService } from 'ngx-joyride';
|
import { JoyrideService } from 'ngx-joyride';
|
||||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
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 { EditOrganizationalUnitComponent } from './shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component';
|
||||||
import { EditClientComponent } from './shared/clients/edit-client/edit-client.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 { 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 { LegendComponent } from './shared/legend/legend.component';
|
||||||
import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component';
|
import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||||
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
|
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 { MatTableDataSource } from '@angular/material/table';
|
||||||
import { MatPaginator } from '@angular/material/paginator';
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
import {CreateMultipleClientComponent} from "./shared/clients/create-multiple-client/create-multiple-client.component";
|
import {CreateMultipleClientComponent} from "./shared/clients/create-multiple-client/create-multiple-client.component";
|
||||||
|
import {SelectionModel} from "@angular/cdk/collections";
|
||||||
|
|
||||||
enum NodeType {
|
enum NodeType {
|
||||||
OrganizationalUnit = 'organizational-unit',
|
OrganizationalUnit = 'organizational-unit',
|
||||||
|
@ -53,17 +52,17 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
commands: Command[] = [];
|
commands: Command[] = [];
|
||||||
commandsLoading = false;
|
commandsLoading = false;
|
||||||
selectedClients = new MatTableDataSource<Client>([]);
|
selectedClients = new MatTableDataSource<Client>([]);
|
||||||
|
selection = new SelectionModel<any>(true, []);
|
||||||
cols = 4;
|
cols = 4;
|
||||||
selectedClientsOriginal: Client[] = [];
|
|
||||||
currentView: 'card' | 'list' = 'list';
|
currentView: 'card' | 'list' = 'list';
|
||||||
isTreeViewActive = false;
|
|
||||||
savedFilterNames: [string, string][] = [];
|
savedFilterNames: [string, string][] = [];
|
||||||
selectedTreeFilter = '';
|
selectedTreeFilter = '';
|
||||||
syncStatus = false;
|
syncStatus = false;
|
||||||
syncingClientId: string | null = null;
|
syncingClientId: string | null = null;
|
||||||
private originalTreeData: TreeNode[] = [];
|
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 _sort!: MatSort;
|
||||||
private _paginator!: MatPaginator;
|
private _paginator!: MatPaginator;
|
||||||
|
@ -112,9 +111,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.search();
|
this.search();
|
||||||
this.getFilters();
|
|
||||||
this.updateGridCols();
|
this.updateGridCols();
|
||||||
this.loadOrganizationalUnits();
|
this.refreshData();
|
||||||
window.addEventListener('resize', this.updateGridCols);
|
window.addEventListener('resize', this.updateGridCols);
|
||||||
|
|
||||||
this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => {
|
this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => {
|
||||||
|
@ -144,40 +142,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
'@id': node['@id'],
|
'@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 {
|
toggleView(view: 'card' | 'list'): void {
|
||||||
this.currentView = view;
|
this.currentView = view;
|
||||||
}
|
}
|
||||||
|
@ -191,10 +155,22 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
this.selectedUnidad = null;
|
this.selectedUnidad = null;
|
||||||
this.selectedDetail = null;
|
this.selectedDetail = null;
|
||||||
this.selectedClients.data = [];
|
this.selectedClients.data = [];
|
||||||
this.isTreeViewActive = false;
|
|
||||||
this.selectedNode = null;
|
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 {
|
getFilters(): void {
|
||||||
this.subscriptions.add(
|
this.subscriptions.add(
|
||||||
this.dataService.getFilters().subscribe(
|
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 {
|
search(): void {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.subscriptions.add(
|
this.subscriptions.add(
|
||||||
|
@ -239,49 +200,112 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadChildrenAndClients(id: string): Promise<UnidadOrganizativa> {
|
private convertToTreeData(data: UnidadOrganizativa): TreeNode {
|
||||||
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[] {
|
|
||||||
const processNode = (node: UnidadOrganizativa): TreeNode => ({
|
const processNode = (node: UnidadOrganizativa): TreeNode => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
|
uuid: node.uuid,
|
||||||
name: node.name,
|
name: node.name,
|
||||||
type: node.type,
|
type: node.type,
|
||||||
'@id': node['@id'],
|
'@id': node['@id'],
|
||||||
children: node.children?.map(processNode) || [],
|
children: node.children?.map(processNode) || [],
|
||||||
hasClients: (node.clients?.length ?? 0) > 0,
|
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 {
|
onNodeClick(node: TreeNode): void {
|
||||||
console.log('Node clicked:', node);
|
|
||||||
this.selectedNode = node;
|
this.selectedNode = node;
|
||||||
this.fetchClientsForNode(node);
|
this.fetchClientsForNode(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchClientsForNode(node: TreeNode): void {
|
private fetchClientsForNode(node: TreeNode): void {
|
||||||
console.log('Node:', node);
|
|
||||||
this.isLoadingClients = true;
|
this.isLoadingClients = true;
|
||||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({
|
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({
|
||||||
next: (response) => {
|
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 {
|
addOU(event: MouseEvent, parent: TreeNode | null = null): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, {
|
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, {
|
||||||
data: { parent },
|
data: { parent },
|
||||||
width: '900px',
|
width: '900px',
|
||||||
});
|
});
|
||||||
dialogRef.afterClosed().subscribe(() => {
|
dialogRef.afterClosed().subscribe((newUnit) => {
|
||||||
this.refreshOrganizationalUnits();
|
if (newUnit?.uuid) {
|
||||||
|
console.log('Unidad organizativa creada:', newUnit);
|
||||||
|
this.refreshData(newUnit.uuid);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
const targetNode = organizationalUnit || this.selectedNode;
|
||||||
const dialogRef = this.dialog.open(CreateClientComponent, {
|
const dialogRef = this.dialog.open(CreateClientComponent, {
|
||||||
data: { organizationalUnit },
|
data: { organizationalUnit: targetNode },
|
||||||
width: '900px',
|
width: '900px',
|
||||||
});
|
});
|
||||||
dialogRef.afterClosed().subscribe(() => {
|
|
||||||
this.refreshOrganizationalUnits();
|
dialogRef.afterClosed().subscribe((result) => {
|
||||||
if (organizationalUnit && organizationalUnit['@id']) {
|
if (result?.client && result?.organizationalUnit) {
|
||||||
this.refreshClientsForNode(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 {
|
addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
const targetNode = organizationalUnit || this.selectedNode;
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(CreateMultipleClientComponent, {
|
const dialogRef = this.dialog.open(CreateMultipleClientComponent, {
|
||||||
data: { organizationalUnit },
|
data: { organizationalUnit: targetNode },
|
||||||
width: '900px',
|
width: '900px',
|
||||||
});
|
});
|
||||||
dialogRef.afterClosed().subscribe(() => {
|
dialogRef.afterClosed().subscribe((result) => {
|
||||||
this.refreshOrganizationalUnits();
|
if (result?.success) {
|
||||||
if (organizationalUnit && organizationalUnit['@id']) {
|
const organizationalUnitUrl = result.organizationalUnit;
|
||||||
this.refreshClientsForNode(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 {
|
onEditNode(event: MouseEvent, node: TreeNode | null): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const uuid = node ? this.extractUuid(node['@id']) : null;
|
const uuid = node ? this.extractUuid(node['@id']) : null;
|
||||||
if (!uuid) return;
|
if (!uuid) return;
|
||||||
|
|
||||||
if (node && node.type !== NodeType.Client) {
|
const dialogRef = node?.type !== NodeType.Client
|
||||||
this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' });
|
? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
|
||||||
} else {
|
: this.dialog.open(EditClientComponent, { 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();
|
event.stopPropagation();
|
||||||
const uuid = node ? this.extractUuid(node['@id']) : null;
|
const uuid = node ? this.extractUuid(node['@id']) : null;
|
||||||
if (!uuid) return;
|
if (!uuid) return;
|
||||||
|
@ -410,44 +406,50 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((result) => {
|
dialogRef.afterClosed().subscribe((result) => {
|
||||||
if (result === true) {
|
if (result === true) {
|
||||||
this.deleteEntity(uuid, node.type, node);
|
this.deleteEntityorClient(uuid, node?.type);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteEntity(uuid: string, type: string, node: TreeNode): void {
|
private deleteEntityorClient(uuid: string, type: string): void {
|
||||||
this.subscriptions.add(
|
if (!this.selectedNode) return;
|
||||||
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 refreshClientsForNode(node: TreeNode): void {
|
const parentNode = this.selectedNode?.id
|
||||||
if (!node['@id']) {
|
? this.findParentNode(this.treeDataSource.data, this.selectedNode.id)
|
||||||
this.selectedClients.data = [];
|
: null;
|
||||||
return;
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
this.fetchClientsForNode(node);
|
},
|
||||||
|
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 {
|
onEditClick(event: MouseEvent, type: string, uuid: string): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (type !== NodeType.Client) {
|
const dialogRef = type !== NodeType.Client
|
||||||
this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' });
|
? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
|
||||||
} else {
|
: this.dialog.open(EditClientComponent, { 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 {
|
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 {
|
executeCommand(command: Command, selectedNode: TreeNode | null): void {
|
||||||
|
|
||||||
if (!selectedNode) {
|
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 {
|
openBottomSheet(): void {
|
||||||
this.bottomSheet.open(LegendComponent);
|
this.bottomSheet.open(LegendComponent);
|
||||||
}
|
}
|
||||||
|
@ -531,31 +510,54 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
hasChild = (_: number, node: FlatNode): boolean => node.expandable;
|
hasChild = (_: number, node: FlatNode): boolean => node.expandable;
|
||||||
isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable;
|
isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable;
|
||||||
|
|
||||||
filterTree(searchTerm: string, filterType: string): void {
|
filterTree(searchTerm: string): void {
|
||||||
const filterNodes = (nodes: TreeNode[]): TreeNode[] => {
|
const expandPaths: 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) : [];
|
|
||||||
|
|
||||||
if ((matchesName && matchesType) || filteredChildren.length > 0) {
|
const filterNodes = (nodes: TreeNode[], parentPath: TreeNode[] = []): TreeNode[] => {
|
||||||
filteredNodes.push({ ...node, children: filteredChildren });
|
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;
|
||||||
return filteredNodes;
|
})
|
||||||
|
.filter((node): node is TreeNode => node !== null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredData = filterNodes(this.originalTreeData);
|
if (!searchTerm) {
|
||||||
this.treeDataSource.data = filteredData;
|
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 {
|
onTreeFilterInput(event: Event): void {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
const searchTerm = input?.value || '';
|
const searchTerm = input?.value.trim() || '';
|
||||||
this.filterTree(searchTerm, this.selectedTreeFilter);
|
this.filterTree(searchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClientFilterInput(event: Event): void {
|
onClientFilterInput(event: Event): void {
|
||||||
|
@ -569,7 +571,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
this.selectedClients.filter = this.searchTerm;
|
this.selectedClients.filter = this.searchTerm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public setSelectedNode(node: TreeNode): void {
|
public setSelectedNode(node: TreeNode): void {
|
||||||
this.selectedNode = node;
|
this.selectedNode = node;
|
||||||
}
|
}
|
||||||
|
@ -586,19 +587,68 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
this.toastr.success('Cliente actualizado correctamente');
|
this.toastr.success('Cliente actualizado correctamente');
|
||||||
this.syncStatus = false;
|
this.syncStatus = false;
|
||||||
this.syncingClientId = null;
|
this.syncingClientId = null;
|
||||||
this.search()
|
this.refreshData()
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.toastr.error('Error de conexión con el cliente');
|
this.toastr.error('Error de conexión con el cliente');
|
||||||
this.syncStatus = false;
|
this.syncStatus = false;
|
||||||
this.syncingClientId = null;
|
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 {
|
private extractUuid(idPath: string | undefined): string | null {
|
||||||
return idPath ? idPath.split('/').pop() || null : null;
|
return idPath ? idPath.split('/').pop() || null : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearTreeSearch(inputElement: HTMLInputElement): void {
|
||||||
|
inputElement.value = '';
|
||||||
|
this.filterTree('');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearClientSearch(inputElement: HTMLInputElement): void {
|
||||||
|
inputElement.value = '';
|
||||||
|
this.filterClients('');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ export interface ClientCollection {
|
||||||
|
|
||||||
export interface TreeNode {
|
export interface TreeNode {
|
||||||
id?: string
|
id?: string
|
||||||
|
uuid?: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
'@id'?: string;
|
'@id'?: string;
|
||||||
|
|
|
@ -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(' / ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,12 @@
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label i18n="@@organizational-unit-label">Padre</mat-label>
|
<mat-label i18n="@@organizational-unit-label">Padre</mat-label>
|
||||||
<mat-select formControlName="organizationalUnit">
|
<mat-select formControlName="organizationalUnit">
|
||||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
|
<mat-select-trigger>
|
||||||
|
{{ getSelectedParentName() }}
|
||||||
|
</mat-select-trigger>
|
||||||
|
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
|
||||||
<div class="unit-name">{{ unit.name }}</div>
|
<div class="unit-name">{{ unit.name }}</div>
|
||||||
<div class="unit-path">{{ unit.path }}</div>
|
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
|
@ -15,6 +15,7 @@ export class CreateClientComponent implements OnInit {
|
||||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
clientForm!: FormGroup;
|
clientForm!: FormGroup;
|
||||||
parentUnits: any[] = [];
|
parentUnits: any[] = [];
|
||||||
|
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||||
hardwareProfiles: any[] = [];
|
hardwareProfiles: any[] = [];
|
||||||
ogLives: any[] = [];
|
ogLives: any[] = [];
|
||||||
menus: any[] = [];
|
menus: any[] = [];
|
||||||
|
@ -80,6 +81,11 @@ export class CreateClientComponent implements OnInit {
|
||||||
this.http.get<any>(url).subscribe(
|
this.http.get<any>(url).subscribe(
|
||||||
response => {
|
response => {
|
||||||
this.parentUnits = response['hydra:member'];
|
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;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
error => {
|
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 {
|
loadHardwareProfiles(): void {
|
||||||
this.dataService.getHardwareProfiles().subscribe(
|
this.dataService.getHardwareProfiles().subscribe(
|
||||||
(data: any[]) => {
|
(data: any[]) => {
|
||||||
|
@ -157,15 +168,21 @@ export class CreateClientComponent implements OnInit {
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
if (this.clientForm.valid) {
|
if (this.clientForm.valid) {
|
||||||
const formData = this.clientForm.value;
|
const formData = this.clientForm.value;
|
||||||
|
|
||||||
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
||||||
response => {
|
(response) => {
|
||||||
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
||||||
this.dialogRef.close(response);
|
this.dialogRef.close({
|
||||||
|
client: response,
|
||||||
|
organizationalUnit: formData.organizationalUnit,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
error => {
|
(error) => {
|
||||||
this.toastService.error('Error al crear el cliente', '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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,13 @@
|
||||||
<form class="client-form grid-form" *ngIf="!loading">
|
<form class="client-form grid-form" *ngIf="!loading">
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||||
<mat-select (selectionChange)="setOrganizationalUnit($event)">
|
<mat-select (selectionChange)="setOrganizationalUnit($event)" [value]="organizationalUnit">
|
||||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
|
<mat-select-trigger>
|
||||||
|
{{ getSelectedParentName() }}
|
||||||
|
</mat-select-trigger>
|
||||||
|
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
|
||||||
<div class="unit-name">{{ unit.name }}</div>
|
<div class="unit-name">{{ unit.name }}</div>
|
||||||
|
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
|
@ -3,6 +3,8 @@ import {MatDialogRef} from "@angular/material/dialog";
|
||||||
import {HttpClient} from "@angular/common/http";
|
import {HttpClient} from "@angular/common/http";
|
||||||
import {MatSnackBar} from "@angular/material/snack-bar";
|
import {MatSnackBar} from "@angular/material/snack-bar";
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
|
import {MAT_DIALOG_DATA} from "@angular/material/dialog";
|
||||||
|
import { DataService } from '../../../services/data.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create-multiple-client',
|
selector: 'app-create-multiple-client',
|
||||||
|
@ -12,21 +14,29 @@ import {ToastrService} from "ngx-toastr";
|
||||||
export class CreateMultipleClientComponent implements OnInit{
|
export class CreateMultipleClientComponent implements OnInit{
|
||||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
parentUnits: any[] = [];
|
parentUnits: any[] = [];
|
||||||
|
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||||
uploadedClients: any[] = [];
|
uploadedClients: any[] = [];
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
displayedColumns: string[] = ['name', 'ip', 'mac'];
|
displayedColumns: string[] = ['name', 'ip', 'mac'];
|
||||||
showTextarea: boolean = true;
|
showTextarea: boolean = true;
|
||||||
organizationalUnit: any;
|
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(
|
constructor(
|
||||||
@Optional() private dialogRef: MatDialogRef<CreateMultipleClientComponent>,
|
@Optional() private dialogRef: MatDialogRef<CreateMultipleClientComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) private data: any,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private snackBar: MatSnackBar,
|
private snackBar: MatSnackBar,
|
||||||
private toastService: ToastrService
|
private toastService: ToastrService,
|
||||||
|
private dataService: DataService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadParentUnits();
|
this.loadParentUnits();
|
||||||
|
|
||||||
|
if (this.data?.organizationalUnit) {
|
||||||
|
this.organizationalUnit = this.data.organizationalUnit['@id'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadParentUnits(): void {
|
loadParentUnits(): void {
|
||||||
|
@ -36,6 +46,11 @@ export class CreateMultipleClientComponent implements OnInit{
|
||||||
this.http.get<any>(url).subscribe(
|
this.http.get<any>(url).subscribe(
|
||||||
response => {
|
response => {
|
||||||
this.parentUnits = response['hydra:member'];
|
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;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
error => {
|
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 {
|
setOrganizationalUnit(organizationalUnit: any): void {
|
||||||
console.log('Organizational unit selected:', organizationalUnit.value);
|
|
||||||
this.organizationalUnit = organizationalUnit.value;
|
this.organizationalUnit = organizationalUnit.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,15 +76,14 @@ export class CreateMultipleClientComponent implements OnInit{
|
||||||
|
|
||||||
reader.onload = (e: any) => {
|
reader.onload = (e: any) => {
|
||||||
const textData = e.target.result;
|
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;
|
let match;
|
||||||
const clients = [];
|
const clients = [];
|
||||||
|
|
||||||
while ((match = regex.exec(textData)) !== null) {
|
while ((match = this.regex.exec(textData)) !== null) {
|
||||||
clients.push({
|
clients.push({
|
||||||
name: match[1],
|
name: match[1],
|
||||||
mac: match[2],
|
mac: match[2],
|
||||||
ip: match[3]
|
ip: match[4]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,15 +102,14 @@ export class CreateMultipleClientComponent implements OnInit{
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextarea(text: string): void {
|
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;
|
let match;
|
||||||
const clients = [];
|
const clients = [];
|
||||||
|
|
||||||
while ((match = regex.exec(text)) !== null) {
|
while ((match = this.regex.exec(text)) !== null) {
|
||||||
clients.push({
|
clients.push({
|
||||||
name: match[1],
|
name: match[1],
|
||||||
mac: match[2],
|
mac: match[2],
|
||||||
ip: match[3]
|
ip: match[4]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +125,9 @@ export class CreateMultipleClientComponent implements OnInit{
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
if (this.uploadedClients.length > 0) {
|
if (this.uploadedClients.length > 0) {
|
||||||
|
let successCount = 0;
|
||||||
|
let errorMessages: string[] = [];
|
||||||
|
|
||||||
this.uploadedClients.forEach(client => {
|
this.uploadedClients.forEach(client => {
|
||||||
const formData = {
|
const formData = {
|
||||||
organizationalUnit: this.organizationalUnit,
|
organizationalUnit: this.organizationalUnit,
|
||||||
|
@ -118,20 +138,37 @@ export class CreateMultipleClientComponent implements OnInit{
|
||||||
|
|
||||||
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
||||||
response => {
|
response => {
|
||||||
this.toastService.success(`Cliente ${client.name} creado exitosamente`, 'Éxito');
|
successCount++;
|
||||||
|
if (successCount + errorMessages.length === this.uploadedClients.length) {
|
||||||
|
this.showFinalToast(successCount, errorMessages);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error => {
|
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 {
|
} else {
|
||||||
this.toastService.error('No hay clientes cargados para añadir', 'Error');
|
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 {
|
onNoClick(): void {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,12 @@
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="organizationalUnit">
|
<mat-select formControlName="organizationalUnit">
|
||||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
|
<mat-select-trigger>
|
||||||
{{ unit.name }}
|
{{ getSelectedParentName() }}
|
||||||
|
</mat-select-trigger>
|
||||||
|
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
|
||||||
|
<div class="unit-name">{{ unit.name }}</div>
|
||||||
|
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
|
@ -15,6 +15,7 @@ export class EditClientComponent {
|
||||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
clientForm!: FormGroup;
|
clientForm!: FormGroup;
|
||||||
parentUnits: any[] = [];
|
parentUnits: any[] = [];
|
||||||
|
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||||
hardwareProfiles: any[] = [];
|
hardwareProfiles: any[] = [];
|
||||||
repositories: any[] = [];
|
repositories: any[] = [];
|
||||||
ogLives: 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`;
|
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;
|
||||||
|
|
||||||
this.http.get<any>(url).subscribe(
|
this.http.get<any>(url).subscribe(
|
||||||
response => {
|
response => {
|
||||||
this.parentUnits = response['hydra:member'];
|
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 => {
|
error => {
|
||||||
console.error('Error fetching parent units:', 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 {
|
loadHardwareProfiles(): void {
|
||||||
this.dataService.getHardwareProfiles().subscribe(
|
this.dataService.getHardwareProfiles().subscribe(
|
||||||
(data: any[]) => {
|
(data: any[]) => {
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
<h1 mat-dialog-title>{{ 'addOrgUnitTitle' | translate }}</h1>
|
<h1 mat-dialog-title>{{ 'addOrgUnitTitle' | translate }}</h1>
|
||||||
|
|
||||||
<div mat-dialog-content>
|
<div mat-dialog-content>
|
||||||
<mat-stepper orientation="vertical" [linear]="isLinear">
|
|
||||||
<!-- Paso 1: General -->
|
<!-- Paso 1: General -->
|
||||||
<mat-step [stepControl]="generalFormGroup">
|
|
||||||
<form [formGroup]="generalFormGroup">
|
<form [formGroup]="generalFormGroup">
|
||||||
<ng-template matStepLabel>{{ 'generalStepLabel' | translate }}</ng-template>
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="type" required>
|
<mat-select formControlName="type" required>
|
||||||
|
@ -21,24 +17,23 @@
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'createOrgUnitparentLabel' | translate }}</mat-label>
|
<mat-label>{{ 'createOrgUnitparentLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="parent">
|
<mat-select formControlName="parent">
|
||||||
<mat-option>{{ 'noParentOption' | translate }}</mat-option>
|
<mat-select-trigger>
|
||||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">{{ unit.name }}</mat-option>
|
{{ getSelectedParentName() }}
|
||||||
|
</mat-select-trigger>
|
||||||
|
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
|
||||||
|
<div>{{ unit.name }}</div>
|
||||||
|
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||||
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
|
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
|
||||||
<textarea matInput formControlName="description"></textarea>
|
<textarea matInput formControlName="description"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div>
|
|
||||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</mat-step>
|
|
||||||
|
|
||||||
<!-- Paso 2: Información del Aula -->
|
<!-- Paso 2: Información del Aula -->
|
||||||
<mat-step *ngIf="generalFormGroup.value.type === 'classroom'" [stepControl]="classroomInfoFormGroup">
|
<form *ngIf="generalFormGroup.value.type === 'classroom'" [formGroup]="classroomInfoFormGroup">
|
||||||
<form [formGroup]="classroomInfoFormGroup">
|
|
||||||
<ng-template matStepLabel>{{ 'classroomInfoStepLabel' | translate }}</ng-template>
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
|
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
|
||||||
<input matInput formControlName="location">
|
<input matInput formControlName="location">
|
||||||
|
@ -57,33 +52,18 @@
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<div>
|
|
||||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
|
||||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</mat-step>
|
|
||||||
|
|
||||||
<!-- Paso 3: Información Adicional -->
|
<!-- Paso 3: Información Adicional -->
|
||||||
<mat-step [stepControl]="additionalInfoFormGroup">
|
|
||||||
<form [formGroup]="additionalInfoFormGroup">
|
<form [formGroup]="additionalInfoFormGroup">
|
||||||
<ng-template matStepLabel>{{ 'additionalInfoStepLabel' | translate }}</ng-template>
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
||||||
<textarea matInput formControlName="comments"></textarea>
|
<textarea matInput formControlName="comments"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div>
|
|
||||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
|
||||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</mat-step>
|
|
||||||
|
|
||||||
<!-- Paso 4: Configuración de Red -->
|
<!-- Paso 4: Configuración de Red -->
|
||||||
<mat-step *ngIf="generalFormGroup.value.type === 'classroom'" [stepControl]="networkSettingsFormGroup">
|
<form *ngIf="generalFormGroup.value.type === 'classroom' || generalFormGroup.value.type === 'clients-group'" [formGroup]="networkSettingsFormGroup">
|
||||||
<form [formGroup]="networkSettingsFormGroup">
|
|
||||||
<ng-template matStepLabel>{{ 'networkSettingsStepLabel' | translate }}</ng-template>
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="oglive" (selectionChange)="onOgLiveChange($event)">
|
<mat-select formControlName="oglive" (selectionChange)="onOgLiveChange($event)">
|
||||||
|
@ -173,10 +153,8 @@
|
||||||
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
|
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</form>
|
</form>
|
||||||
</mat-step>
|
|
||||||
</mat-stepper>
|
|
||||||
</div>
|
</div>
|
||||||
<div mat-dialog-actions align="end">
|
<div mat-dialog-actions align="end">
|
||||||
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||||
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid">{{ 'submitButton' | translate }}</button>
|
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid">{{ 'addOUSubmitButton' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,9 +25,9 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
||||||
'clients-group': 'Grupo de clientes'
|
'clients-group': 'Grupo de clientes'
|
||||||
};
|
};
|
||||||
protected p2pModeOptions = [
|
protected p2pModeOptions = [
|
||||||
{ name: 'Leecher', value: 'p2p-mode-leecher' },
|
{ name: 'Leecher', value: 'leecher' },
|
||||||
{ name: 'Peer', value: 'p2p-mode-peer' },
|
{ name: 'Peer', value: 'peer' },
|
||||||
{ name: 'Seeder', value: 'p2p-mode-seeder' },
|
{ name: 'Seeder', value: 'seeder' },
|
||||||
];
|
];
|
||||||
protected multicastModeOptions = [
|
protected multicastModeOptions = [
|
||||||
{"name": 'Half duplex', "value": "half"},
|
{"name": 'Half duplex', "value": "half"},
|
||||||
|
@ -39,9 +39,9 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
||||||
ogLives: any[] = [];
|
ogLives: any[] = [];
|
||||||
repositories: any[] = [];
|
repositories: any[] = [];
|
||||||
selectedCalendarUuid: string | null = null;
|
selectedCalendarUuid: string | null = null;
|
||||||
|
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||||
|
|
||||||
|
@Output() unitAdded = new EventEmitter<{ uuid: string; name: string }>();
|
||||||
@Output() unitAdded = new EventEmitter();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _formBuilder: FormBuilder,
|
private _formBuilder: FormBuilder,
|
||||||
|
@ -104,11 +104,23 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
||||||
loadParentUnits() {
|
loadParentUnits() {
|
||||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
|
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
|
||||||
this.http.get<any>(url).subscribe(
|
this.http.get<any>(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)
|
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 {
|
loadHardwareProfiles(): void {
|
||||||
this.dataService.getHardwareProfiles().subscribe(
|
this.dataService.getHardwareProfiles().subscribe(
|
||||||
(data: any[]) => this.hardwareProfiles = data,
|
(data: any[]) => this.hardwareProfiles = data,
|
||||||
|
@ -118,7 +130,9 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
||||||
|
|
||||||
loadOgLives() {
|
loadOgLives() {
|
||||||
this.dataService.getOgLives().subscribe(
|
this.dataService.getOgLives().subscribe(
|
||||||
(data: any[]) => this.ogLives = data,
|
(data: any[]) => {
|
||||||
|
this.ogLives = data
|
||||||
|
},
|
||||||
error => console.error('Error fetching ogLives', error)
|
error => console.error('Error fetching ogLives', error)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -160,10 +174,10 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
||||||
|
|
||||||
this.http.post<any>(postUrl, formData, { headers }).subscribe(
|
this.http.post<any>(postUrl, formData, { headers }).subscribe(
|
||||||
response => {
|
response => {
|
||||||
this.unitAdded.emit();
|
this.unitAdded.emit(response);
|
||||||
this.dialogRef.close();
|
this.dialogRef.close(response);
|
||||||
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
this.toastService.success('Unidad creada exitosamente', 'Éxito');
|
||||||
this.openSnackBar(false, 'Cliente creado exitosamente');
|
this.openSnackBar(false, 'Unidad creada exitosamente');
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
console.error('Error al realizar POST:', error);
|
console.error('Error al realizar POST:', error);
|
||||||
|
@ -218,9 +232,9 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
||||||
|
|
||||||
openSnackBar(isError: boolean, message: string) {
|
openSnackBar(isError: boolean, message: string) {
|
||||||
if (isError) {
|
if (isError) {
|
||||||
this.toastService.error('Error al crear el cliente: ' + message, 'Error');
|
this.toastService.error('Error al crear la unidad: ' + message, 'Error');
|
||||||
} else {
|
} else {
|
||||||
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
this.toastService.success('Unidad creada exitosamente', 'Éxito');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,4 +36,3 @@ h1 {
|
||||||
.mat-slide-toggle {
|
.mat-slide-toggle {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
<h1 mat-dialog-title>{{ 'editOrgUnitTitle' | translate }}</h1>
|
<h1 mat-dialog-title>{{ 'editOrgUnitTitle' | translate }}</h1>
|
||||||
<div mat-dialog-content>
|
<div mat-dialog-content>
|
||||||
<mat-stepper orientation="vertical" [linear]="isLinear">
|
|
||||||
<!-- Paso 1: General -->
|
<!-- Paso 1: General -->
|
||||||
<mat-step [stepControl]="generalFormGroup">
|
|
||||||
<form [formGroup]="generalFormGroup">
|
<form [formGroup]="generalFormGroup">
|
||||||
<ng-template matStepLabel>{{ 'generalStepLabel' | translate }}</ng-template>
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="type" required>
|
<mat-select formControlName="type" required>
|
||||||
<mat-option *ngFor="let type of filteredTypes" [value]="type">{{ type }}</mat-option>
|
<mat-option *ngFor="let type of filteredTypes" [value]="type">
|
||||||
|
{{ typeTranslations[type] }}
|
||||||
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
|
@ -18,23 +17,23 @@
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'editOrgUnitParentLabel' | translate }}</mat-label>
|
<mat-label>{{ 'editOrgUnitParentLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="parent">
|
<mat-select formControlName="parent">
|
||||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">{{ unit.name }}</mat-option>
|
<mat-select-trigger>
|
||||||
|
{{ getSelectedParentName() }}
|
||||||
|
</mat-select-trigger>
|
||||||
|
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
|
||||||
|
<div>{{ unit.name }}</div>
|
||||||
|
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||||
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
|
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
|
||||||
<textarea matInput formControlName="description"></textarea>
|
<textarea matInput formControlName="description"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div>
|
|
||||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</mat-step>
|
|
||||||
|
|
||||||
<!-- Paso 2: Información del Aula -->
|
<!-- Paso 2: Información del Aula -->
|
||||||
<mat-step *ngIf="generalFormGroup.value.type === 'classroom'" [stepControl]="classroomInfoFormGroup">
|
<form *ngIf="generalFormGroup.value.type === 'classroom'" [formGroup]="classroomInfoFormGroup">
|
||||||
<form [formGroup]="classroomInfoFormGroup">
|
|
||||||
<ng-template matStepLabel>{{ 'classroomInfoStepLabel' | translate }}</ng-template>
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
|
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
|
||||||
<input matInput formControlName="location">
|
<input matInput formControlName="location">
|
||||||
|
@ -45,7 +44,6 @@
|
||||||
<mat-label>{{ 'capacityLabel' | translate }}</mat-label>
|
<mat-label>{{ 'capacityLabel' | translate }}</mat-label>
|
||||||
<input matInput formControlName="capacity" type="number">
|
<input matInput formControlName="capacity" type="number">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field class="form-field" appearance="fill">
|
<mat-form-field class="form-field" appearance="fill">
|
||||||
<mat-label>{{ 'associatedCalendarLabel' | translate }}</mat-label>
|
<mat-label>{{ 'associatedCalendarLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
|
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
|
||||||
|
@ -54,33 +52,18 @@
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<div>
|
|
||||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
|
||||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</mat-step>
|
|
||||||
|
|
||||||
<!-- Paso 3: Información Adicional -->
|
<!-- Paso 3: Información Adicional -->
|
||||||
<mat-step [stepControl]="additionalInfoFormGroup">
|
|
||||||
<form [formGroup]="additionalInfoFormGroup">
|
<form [formGroup]="additionalInfoFormGroup">
|
||||||
<ng-template matStepLabel>{{ 'additionalInfoStepLabel' | translate }}</ng-template>
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
||||||
<textarea matInput formControlName="comments"></textarea>
|
<textarea matInput formControlName="comments"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div>
|
|
||||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
|
||||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</mat-step>
|
|
||||||
|
|
||||||
<!-- Paso 4: Configuración de Red -->
|
<!-- Paso 4: Configuración de Red -->
|
||||||
<mat-step [stepControl]="networkSettingsFormGroup">
|
<form *ngIf="generalFormGroup.value.type === 'classroom' || generalFormGroup.value.type === 'clients-group'" [formGroup]="networkSettingsFormGroup">
|
||||||
<form [formGroup]="networkSettingsFormGroup">
|
|
||||||
<ng-template matStepLabel>{{ 'networkSettingsStepLabel' | translate }}</ng-template>
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
||||||
|
@ -157,14 +140,9 @@
|
||||||
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
|
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-slide-toggle formControlName="validation">{{ 'validationToggle' | translate }}</mat-slide-toggle>
|
<mat-slide-toggle formControlName="validation">{{ 'validationToggle' | translate }}</mat-slide-toggle>
|
||||||
<div>
|
|
||||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</mat-step>
|
|
||||||
</mat-stepper>
|
|
||||||
</div>
|
</div>
|
||||||
<div mat-dialog-actions align="end">
|
<div mat-dialog-actions align="end">
|
||||||
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||||
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid">{{ 'submitButton' | translate }}</button>
|
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid">{{ 'editOUSubmitButton' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,16 +19,23 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
||||||
networkSettingsFormGroup: FormGroup;
|
networkSettingsFormGroup: FormGroup;
|
||||||
classroomInfoFormGroup: FormGroup;
|
classroomInfoFormGroup: FormGroup;
|
||||||
types: string[] = ['organizational-unit', 'classrooms-group', 'classroom', 'clients-group'];
|
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[] = [];
|
parentUnits: any[] = [];
|
||||||
hardwareProfiles: any[] = [];
|
hardwareProfiles: any[] = [];
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
currentCalendar: any = [];
|
currentCalendar: any = [];
|
||||||
ogLives: any[] = [];
|
ogLives: any[] = [];
|
||||||
repositories: any[] = [];
|
repositories: any[] = [];
|
||||||
|
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||||
protected p2pModeOptions = [
|
protected p2pModeOptions = [
|
||||||
{"name": 'Leecher', "value": "p2p-mode-leecher"},
|
{"name": 'Leecher', "value": "leecher"},
|
||||||
{"name": 'Peer', "value": "p2p-mode-peer"},
|
{"name": 'Peer', "value": "peer"},
|
||||||
{"name": 'Seeder', "value": "p2p-mode-seeder"},
|
{"name": 'Seeder', "value": "seeder"},
|
||||||
];
|
];
|
||||||
protected multicastModeOptions = [
|
protected multicastModeOptions = [
|
||||||
{"name": 'Half duplex', "value": "half"},
|
{"name": 'Half duplex', "value": "half"},
|
||||||
|
@ -103,18 +110,25 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadParentUnits() {
|
loadParentUnits() {
|
||||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;
|
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
|
||||||
|
|
||||||
this.http.get<any>(url).subscribe(
|
this.http.get<any>(url).subscribe(
|
||||||
response => {
|
response => {
|
||||||
this.parentUnits = response['hydra:member'];
|
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 => {
|
error => console.error('Error fetching parent units:', 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 {
|
loadHardwareProfiles(): void {
|
||||||
this.dataService.getHardwareProfiles().subscribe(
|
this.dataService.getHardwareProfiles().subscribe(
|
||||||
(data: any[]) => {
|
(data: any[]) => {
|
||||||
|
@ -266,7 +280,7 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
console.error('Error al realizar POST:', 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']);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
<h1 mat-dialog-title>{{ 'viewTreeTitle' | translate }}</h1>
|
|
||||||
|
|
||||||
<mat-dialog-content>
|
|
||||||
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="tree">
|
|
||||||
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
|
|
||||||
<mat-icon [ngSwitch]="node.type">
|
|
||||||
<ng-container *ngSwitchCase="'organizational-unit'">apartment</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'classrooms-group'">meeting_room</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'classroom'">school</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'client'">computer</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'clients-group'">lan</ng-container>
|
|
||||||
<ng-container *ngSwitchDefault>help_outline</ng-container>
|
|
||||||
</mat-icon>
|
|
||||||
{{ node.name }}
|
|
||||||
</mat-tree-node>
|
|
||||||
|
|
||||||
<!-- Nodo expandible -->
|
|
||||||
<mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
|
|
||||||
<div class="mat-tree-node">
|
|
||||||
<button mat-icon-button matTreeNodeToggle [attr.aria-label]="'Toggle ' + node.name | translate">
|
|
||||||
<mat-icon class="mat-icon-rtl-mirror">
|
|
||||||
{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
|
|
||||||
</mat-icon>
|
|
||||||
</button>
|
|
||||||
<div class="item-content">
|
|
||||||
<mat-icon [ngSwitch]="node.type">
|
|
||||||
<ng-container *ngSwitchCase="'organizational-unit'">apartment</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'classrooms-group'">meeting_room</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'classroom'">school</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'client'">computer</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'clients-group'">lan</ng-container>
|
|
||||||
<ng-container *ngSwitchDefault>help_outline</ng-container>
|
|
||||||
</mat-icon>
|
|
||||||
{{ node.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div [class.tree-invisible]="!treeControl.isExpanded(node)" role="group">
|
|
||||||
<ng-container matTreeNodeOutlet></ng-container>
|
|
||||||
<mat-list *ngIf="node.clients">
|
|
||||||
<mat-list-item *ngFor="let client of node.clients">
|
|
||||||
<mat-icon matListItemIcon>computer</mat-icon>
|
|
||||||
<span matListItemTitle>{{ client.name }}</span>
|
|
||||||
<span>{{ client.ip }} | {{ client.mac }}</span>
|
|
||||||
</mat-list-item>
|
|
||||||
</mat-list>
|
|
||||||
</div>
|
|
||||||
</mat-nested-tree-node>
|
|
||||||
</mat-tree>
|
|
||||||
</mat-dialog-content>
|
|
||||||
|
|
||||||
<mat-dialog-actions align="end">
|
|
||||||
<button mat-button (click)="close()">{{ 'closeButton' | translate }}</button>
|
|
||||||
</mat-dialog-actions>
|
|
|
@ -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<OrganizationalUnit>(node => node.children);
|
|
||||||
dataSource = new MatTreeNestedDataSource<OrganizationalUnit>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private dialogRef: MatDialogRef<TreeViewComponent>,
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -42,6 +42,13 @@
|
||||||
{{ 'remotePcLabel' | translate }}
|
{{ 'remotePcLabel' | translate }}
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
|
|
||||||
|
<mat-checkbox
|
||||||
|
formControlName="isGlobal"
|
||||||
|
class="example-margin"
|
||||||
|
>
|
||||||
|
{{ 'globalImageLabel' | translate }}
|
||||||
|
</mat-checkbox>
|
||||||
|
|
||||||
<mat-divider *ngIf="imageId && partitionInfo"></mat-divider>
|
<mat-divider *ngIf="imageId && partitionInfo"></mat-divider>
|
||||||
|
|
||||||
<div *ngIf="imageId && partitionInfo" class="partition-info-container">
|
<div *ngIf="imageId && partitionInfo" class="partition-info-container">
|
||||||
|
|
|
@ -30,6 +30,7 @@ export class CreateImageComponent implements OnInit {
|
||||||
description: [''],
|
description: [''],
|
||||||
comments: [''],
|
comments: [''],
|
||||||
remotePc: [false],
|
remotePc: [false],
|
||||||
|
isGlobal: [false],
|
||||||
softwareProfile: [''],
|
softwareProfile: [''],
|
||||||
imageRepository: ['', Validators.required],
|
imageRepository: ['', Validators.required],
|
||||||
});
|
});
|
||||||
|
@ -51,6 +52,7 @@ export class CreateImageComponent implements OnInit {
|
||||||
description: [response.description],
|
description: [response.description],
|
||||||
comments: [response.comments],
|
comments: [response.comments],
|
||||||
remotePc: [response.remotePc],
|
remotePc: [response.remotePc],
|
||||||
|
isGlobal: [response.isGlobal],
|
||||||
softwareProfile: [response.softwareProfile ? response.softwareProfile['@id'] : null, Validators.required],
|
softwareProfile: [response.softwareProfile ? response.softwareProfile['@id'] : null, Validators.required],
|
||||||
imageRepository: [response.imageRepository ? response.imageRepository['@id'] : null, Validators.required],
|
imageRepository: [response.imageRepository ? response.imageRepository['@id'] : null, Validators.required],
|
||||||
});
|
});
|
||||||
|
@ -90,16 +92,12 @@ export class CreateImageComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
saveImage(): void {
|
saveImage(): void {
|
||||||
if (this.imageForm.invalid) {
|
|
||||||
this.toastService.error('Por favor, rellena los campos obligatorios');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
name: this.imageForm.value.name,
|
name: this.imageForm.value.name,
|
||||||
description: this.imageForm.value.description,
|
description: this.imageForm.value.description,
|
||||||
comments: this.imageForm.value.comments,
|
comments: this.imageForm.value.comments,
|
||||||
remotePc: this.imageForm.value.remotePc,
|
remotePc: this.imageForm.value.remotePc,
|
||||||
|
isGlobal: this.imageForm.value.isGlobal,
|
||||||
imageRepository: this.imageForm.value.imageRepository,
|
imageRepository: this.imageForm.value.imageRepository,
|
||||||
...(this.imageForm.value.softwareProfile ? { softwareProfile: this.imageForm.value.softwareProfile } : {}),
|
...(this.imageForm.value.softwareProfile ? { softwareProfile: this.imageForm.value.softwareProfile } : {}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<h2 mat-dialog-title>Exportar imagen {{data.image?.name}}</h2>
|
||||||
|
|
||||||
|
<mat-dialog-content>
|
||||||
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
|
<mat-label>Seleccione repositorio destino</mat-label>
|
||||||
|
<mat-select [(value)]="selectedRepository">
|
||||||
|
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">{{ repository.name }}</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions>
|
||||||
|
<button mat-button (click)="close()">Cancelar</button>
|
||||||
|
<button mat-button (click)="save()">Continuar</button>
|
||||||
|
</mat-dialog-actions>
|
|
@ -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<ExportImageComponent>;
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<ExportImageComponent>,
|
||||||
|
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<any>(`${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<any>(`${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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -91,3 +91,14 @@ table {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chip-transferring {
|
||||||
|
background-color: #f5a623 !important;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container-title {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||||
<mat-icon>help</mat-icon>
|
<mat-icon>help</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<h2 class="title">{{ 'imagesTitle' | translate }}</h2>
|
<div class="header-container-title">
|
||||||
|
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
||||||
|
{{ 'imagesTitle' | translate }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
<div class="images-button-row">
|
<div class="images-button-row">
|
||||||
<button mat-flat-button color="primary" (click)="addImage()">
|
<button mat-flat-button color="primary" (click)="addImage()">
|
||||||
{{ 'addImageButton' | translate }}
|
{{ 'addImageButton' | translate }}
|
||||||
|
@ -31,17 +35,23 @@
|
||||||
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
|
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
|
||||||
</mat-icon>
|
</mat-icon>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<ng-container *ngIf="column.columnDef === 'isGlobal'">
|
||||||
|
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
|
||||||
|
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
|
||||||
|
</mat-icon>
|
||||||
|
</ng-container>
|
||||||
<ng-container *ngIf="column.columnDef === 'status'">
|
<ng-container *ngIf="column.columnDef === 'status'">
|
||||||
<mat-chip [ngClass]="{
|
<mat-chip [ngClass]="{
|
||||||
'chip-failed': image.status === 'failed',
|
'chip-failed': image.status === 'failed',
|
||||||
'chip-success': image.status === 'success',
|
'chip-success': image.status === 'success',
|
||||||
'chip-pending': image.status === 'pending',
|
'chip-pending': image.status === 'pending',
|
||||||
'chip-in-progress': image.status === 'in-progress'
|
'chip-in-progress': image.status === 'in-progress',
|
||||||
|
'chip-transferring': image.status === 'transferring',
|
||||||
}">
|
}">
|
||||||
{{ getStatusLabel(image[column.columnDef]) }}
|
{{ getStatusLabel(image[column.columnDef]) }}
|
||||||
</mat-chip>
|
</mat-chip>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status'">
|
<ng-container *ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status' && column.columnDef !== 'isGlobal'">
|
||||||
{{ column.cell(image) }}
|
{{ column.cell(image) }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
|
@ -61,8 +71,9 @@
|
||||||
<mat-menu #menu="matMenu">
|
<mat-menu #menu="matMenu">
|
||||||
<button mat-menu-item (click)="toggleAction(image, 'get-aux')">Obtener ficheros auxiliares</button>
|
<button mat-menu-item (click)="toggleAction(image, 'get-aux')">Obtener ficheros auxiliares</button>
|
||||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'" (click)="toggleAction(image, 'delete-trash')">Eliminar imagen temporalmente</button>
|
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'" (click)="toggleAction(image, 'delete-trash')">Eliminar imagen temporalmente</button>
|
||||||
|
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'" (click)="toggleAction(image, 'delete-permanent')">Eliminar imagen</button>
|
||||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'trash'" (click)="toggleAction(image, 'recover')">Recuperar imagen de la papelera</button>
|
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'trash'" (click)="toggleAction(image, 'recover')">Recuperar imagen de la papelera</button>
|
||||||
|
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'" (click)="toggleAction(image, 'export')">Exportar imagen</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
import { CreateImageComponent } from './create-image/create-image.component';
|
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 {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 {ServerInfoDialogComponent} from "../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component";
|
||||||
import {Observable} from "rxjs";
|
import {Observable} from "rxjs";
|
||||||
import {InfoImageComponent} from "../ogboot/pxe-images/info-image/info-image/info-image.component";
|
|
||||||
import { JoyrideService } from 'ngx-joyride';
|
import { JoyrideService } from 'ngx-joyride';
|
||||||
|
import {ExportImageComponent} from "./export-image/export-image.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-images',
|
selector: 'app-images',
|
||||||
|
@ -48,6 +47,11 @@ export class ImagesComponent implements OnInit {
|
||||||
header: 'Remote Pc',
|
header: 'Remote Pc',
|
||||||
cell: (image: any) => `${image.remotePc}`
|
cell: (image: any) => `${image.remotePc}`
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
columnDef: 'isGlobal',
|
||||||
|
header: 'Imagen Global',
|
||||||
|
cell: (image: any) => `${image.isGlobal}`
|
||||||
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'status',
|
columnDef: 'status',
|
||||||
header: 'Estado',
|
header: 'Estado',
|
||||||
|
@ -67,17 +71,35 @@ export class ImagesComponent implements OnInit {
|
||||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||||
|
|
||||||
private apiUrl = `${this.baseUrl}/images`;
|
private apiUrl = `${this.baseUrl}/images`;
|
||||||
|
@Input() repositoryUuid: any
|
||||||
|
private repositoryId: any;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public dialog: MatDialog,
|
public dialog: MatDialog,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private toastService: ToastrService,
|
private toastService: ToastrService,
|
||||||
private joyrideService: JoyrideService
|
private joyrideService: JoyrideService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
if (this.repositoryUuid) {
|
||||||
|
this.loadRepository()
|
||||||
|
} else {
|
||||||
this.search();
|
this.search();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRepository(): void {
|
||||||
|
this.http.get<any>(`${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 {
|
getStatusLabel(status: string): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
@ -110,7 +132,7 @@ export class ImagesComponent implements OnInit {
|
||||||
|
|
||||||
search(): void {
|
search(): void {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.http.get<any>(`${this.apiUrl}?page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
|
this.http.get<any>(`${this.apiUrl}?page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}&repository.id=${this.repositoryId}`, { params: this.filters }).subscribe(
|
||||||
data => {
|
data => {
|
||||||
this.dataSource.data = data['hydra:member'];
|
this.dataSource.data = data['hydra:member'];
|
||||||
this.length = data['hydra:totalItems'];
|
this.length = data['hydra:totalItems'];
|
||||||
|
@ -205,6 +227,17 @@ export class ImagesComponent implements OnInit {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
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':
|
case 'recover':
|
||||||
this.http.post(`${this.baseUrl}/images/server/${image.uuid}/recover`, {}).subscribe({
|
this.http.post(`${this.baseUrl}/images/server/${image.uuid}/recover`, {}).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
@ -216,6 +249,14 @@ export class ImagesComponent implements OnInit {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'export':
|
||||||
|
this.dialog.open(ExportImageComponent, {
|
||||||
|
width: '600px',
|
||||||
|
data: {
|
||||||
|
image: image
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.error('Acción no soportada:', action);
|
console.error('Acción no soportada:', action);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<h2 mat-dialog-title>Importar imagenes a {{data.repository?.name}}</h2>
|
||||||
|
|
||||||
|
<mat-dialog-content>
|
||||||
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
|
<mat-label>Seleccione imagenes a importar</mat-label>
|
||||||
|
<mat-select [(value)]="selectedClients" multiple>
|
||||||
|
<mat-option *ngFor="let image of images" [value]="image['@id']">{{ image.name }}</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div *ngIf="selectedClients.length > 0" class="selected-list">
|
||||||
|
<h3>Imágenes seleccionadas:</h3>
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let imageId of selectedClients" class="selected-item">
|
||||||
|
<span>{{ getImageName(imageId) }}</span>
|
||||||
|
<button mat-icon-button color="warn" (click)="removeImage(imageId)">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions>
|
||||||
|
<button mat-button (click)="close()">Cancelar</button>
|
||||||
|
<button mat-button (click)="save()">Continuar</button>
|
||||||
|
</mat-dialog-actions>
|
|
@ -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<ImportImageComponent>;
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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<ImportImageComponent>,
|
||||||
|
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<any>(`${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<any>(`${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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -135,6 +135,10 @@
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.mat-tab-body-wrapper {
|
.mat-tab-body-wrapper {
|
||||||
min-height: inherit;
|
min-height: inherit;
|
||||||
}
|
}
|
||||||
|
@ -210,13 +214,6 @@ p {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-led {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-led.active {
|
.status-led.active {
|
||||||
background-color: green;
|
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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,38 +1,63 @@
|
||||||
<mat-tab-group dynamicHeight>
|
<mat-tab-group class="main-container" dynamicHeight>
|
||||||
<mat-tab label="Estado servidor">
|
<mat-tab label="Estado servidor">
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<h2>OgRepository server Status</h2>
|
<h2>OgRepository Server Status</h2>
|
||||||
<div class="disk-usage-container">
|
|
||||||
<div class="disk-usage">
|
<div class="row top-row">
|
||||||
<h3>Uso de disco</h3>
|
<div class="card">
|
||||||
|
<h3>Uso de Disco</h3>
|
||||||
<ngx-charts-pie-chart
|
<ngx-charts-pie-chart
|
||||||
[view]="view"
|
[view]="view"
|
||||||
[scheme]="colorScheme"
|
[scheme]="colorScheme"
|
||||||
[results]="diskUsageChartData"
|
[results]="diskUsageChartData"
|
||||||
[gradient]="gradient"
|
[gradient]="gradient"
|
||||||
[doughnut]="isDoughnut"
|
[doughnut]="isDoughnut"
|
||||||
[labels]="showLabels"
|
[labels]="showLabels" >
|
||||||
[legend]="showLegend">
|
|
||||||
</ngx-charts-pie-chart>
|
</ngx-charts-pie-chart>
|
||||||
<div class="disk-usage-info">
|
<div class="info">
|
||||||
<p>Total: {{ diskUsage.total }}</p>
|
<p>Total: {{ diskUsage.total }}</p>
|
||||||
<p>Ocupado: {{ diskUsage.used }}</p>
|
<p>Ocupado: {{ diskUsage.used }}</p>
|
||||||
<p>Disponible: {{ diskUsage.available }}</p>
|
<p>Disponible: {{ diskUsage.available }}</p>
|
||||||
<p>Ocupado ( % ): {{ diskUsage.percentage }}</p>
|
<p>Ocupado (%): {{ diskUsage.percentage }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="services-status">
|
<div class="card">
|
||||||
|
<h3>Uso de RAM</h3>
|
||||||
|
<ngx-charts-pie-chart
|
||||||
|
[view]="view"
|
||||||
|
[scheme]="colorScheme"
|
||||||
|
[results]="ramUsageChartData"
|
||||||
|
[gradient]="gradient"
|
||||||
|
[doughnut]="isDoughnut"
|
||||||
|
[labels]="showLabels">
|
||||||
|
</ngx-charts-pie-chart>
|
||||||
|
<div class="info">
|
||||||
|
<p>Total: {{ ramUsage.total }}</p>
|
||||||
|
<p>Ocupado: {{ ramUsage.used }}</p>
|
||||||
|
<p>Disponible: {{ ramUsage.available }}</p>
|
||||||
|
<p>Ocupado (%): {{ ramUsage.percentage }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row bottom-row">
|
||||||
|
<div class="card">
|
||||||
|
<h3>Uso de CPU</h3>
|
||||||
|
<div class="cpu-usage-bar">
|
||||||
|
<div class="cpu-bar" [style.width]="cpuUsage.percentage" [ngClass]="{'high': cpuUsage.percentage > '80%'}"></div>
|
||||||
|
</div>
|
||||||
|
<p>Usado: {{ cpuUsage.percentage }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
<h3>Servicios</h3>
|
<h3>Servicios</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li *ngFor="let service of getServices()">
|
<li *ngFor="let service of getServices()">
|
||||||
<span
|
<span class="status-led" [ngClass]="{
|
||||||
class="status-led"
|
|
||||||
[ngClass]="{
|
|
||||||
'active': service.status === 'active',
|
'active': service.status === 'active',
|
||||||
'inactive': service.status === 'stopped' || service.status === 'status not accesible'
|
'inactive': service.status === 'stopped' || service.status === 'status not accesible'
|
||||||
}"
|
}"></span>
|
||||||
></span>
|
|
||||||
{{ service.name }}:
|
{{ service.name }}:
|
||||||
<span [ngSwitch]="service.status">
|
<span [ngSwitch]="service.status">
|
||||||
<span *ngSwitchCase="'active'">Activo</span>
|
<span *ngSwitchCase="'active'">Activo</span>
|
||||||
|
@ -43,9 +68,24 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Procesos</h3>
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let process of getProcesses()">
|
||||||
|
<span class="status-led" [ngClass]="{
|
||||||
|
'active': process.status === 'running',
|
||||||
|
'inactive': process.status === 'stopped'
|
||||||
|
}"></span>
|
||||||
|
{{ process.name }}: {{ process.status }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
|
||||||
|
|
||||||
<mat-tab label="Datos generales">
|
<mat-tab label="Datos generales">
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<div class="header-button-container">
|
<div class="header-button-container">
|
||||||
|
@ -83,59 +123,6 @@
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
|
||||||
<mat-tab label="Listado de imágenes">
|
<mat-tab label="Listado de imágenes">
|
||||||
<div class="dashboard">
|
<app-images [repositoryUuid]="repositoryId"></app-images>
|
||||||
<h2>Imágenes</h2>
|
|
||||||
<div class="search-container">
|
|
||||||
<mat-form-field appearance="fill" class="search-string">
|
|
||||||
<mat-label>Buscar nombre de imagen</mat-label>
|
|
||||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="searchImages()" i18n-placeholder="@@searchPlaceholder">
|
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
|
||||||
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
|
||||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
|
||||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
|
||||||
<td mat-cell *matCellDef="let image" >
|
|
||||||
<ng-container *ngIf="column.columnDef === 'remotePc' || column.columnDef === 'created'">
|
|
||||||
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
|
|
||||||
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
|
|
||||||
</mat-icon>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'created'">
|
|
||||||
{{ column.cell(image) }}
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
|
||||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
|
||||||
<td mat-cell *matCellDef="let image" style="text-align: center;">
|
|
||||||
<button mat-icon-button color="info" (click)="showImageInfo($event, image)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
|
|
||||||
<button mat-icon-button color="primary" (click)="editImage($event, image)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
|
|
||||||
<button mat-icon-button color="warn" (click)="toggleAction(image, 'delete')">
|
|
||||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button mat-icon-button [matMenuTriggerFor]="menu">
|
|
||||||
<mat-icon>menu</mat-icon>
|
|
||||||
</button>
|
|
||||||
<mat-menu #menu="matMenu">
|
|
||||||
<button mat-menu-item [disabled]="!image.imageFullsum" (click)="toggleAction(image, 'get-aux')">Obtener ficheros auxiliares</button>
|
|
||||||
</mat-menu>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
|
||||||
</table>
|
|
||||||
<div class="paginator-container">
|
|
||||||
<mat-paginator [length]="length"
|
|
||||||
[pageSize]="itemsPerPage"
|
|
||||||
[pageIndex]="page"
|
|
||||||
[pageSizeOptions]="[5, 10, 20, 40, 100]"
|
|
||||||
(page)="onPageChange($event)">
|
|
||||||
</mat-paginator>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
</mat-tab-group>
|
</mat-tab-group>
|
||||||
|
|
|
@ -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 {FormBuilder, FormGroup, Validators} from "@angular/forms";
|
||||||
import {HttpClient} from "@angular/common/http";
|
import {HttpClient} from "@angular/common/http";
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
|
@ -17,7 +17,7 @@ import {MatDialog} from "@angular/material/dialog";
|
||||||
templateUrl: './main-repository-view.component.html',
|
templateUrl: './main-repository-view.component.html',
|
||||||
styleUrl: './main-repository-view.component.css'
|
styleUrl: './main-repository-view.component.css'
|
||||||
})
|
})
|
||||||
export class MainRepositoryViewComponent {
|
export class MainRepositoryViewComponent implements OnInit {
|
||||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
repositoryForm: FormGroup<any>;
|
repositoryForm: FormGroup<any>;
|
||||||
repositoryId: string | null = null;
|
repositoryId: string | null = null;
|
||||||
|
@ -25,16 +25,20 @@ export class MainRepositoryViewComponent {
|
||||||
loading: boolean = true;
|
loading: boolean = true;
|
||||||
diskUsage: any = {};
|
diskUsage: any = {};
|
||||||
servicesStatus: any = {};
|
servicesStatus: any = {};
|
||||||
|
processesStatus: any = {};
|
||||||
diskUsageChartData: any[] = [];
|
diskUsageChartData: any[] = [];
|
||||||
|
ramUsageChartData: any[] = [];
|
||||||
|
ramUsage: any = {};
|
||||||
|
cpuUsage: any = {};
|
||||||
alertMessage: string | null = null;
|
alertMessage: string | null = null;
|
||||||
length: number = 0;
|
length: number = 0;
|
||||||
itemsPerPage: number = 10;
|
itemsPerPage: number = 10;
|
||||||
page: number = 0;
|
page: number = 0;
|
||||||
view: [number, number] = [800, 500];
|
view: [number, number] = [800, 500];
|
||||||
gradient: boolean = true;
|
gradient: boolean = true;
|
||||||
showLegend: boolean = true;
|
|
||||||
showLabels: boolean = true;
|
showLabels: boolean = true;
|
||||||
isDoughnut: boolean = true;
|
isDoughnut: boolean = true;
|
||||||
|
status: boolean = false;
|
||||||
repositoryData: any = {};
|
repositoryData: any = {};
|
||||||
colorScheme: any = {
|
colorScheme: any = {
|
||||||
domain: ['#FF6384', '#3f51b5']
|
domain: ['#FF6384', '#3f51b5']
|
||||||
|
@ -115,7 +119,6 @@ export class MainRepositoryViewComponent {
|
||||||
comments: [response.comments],
|
comments: [response.comments],
|
||||||
});
|
});
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
// Llamar searchImages() solo cuando la data de repository esté cargada
|
|
||||||
this.searchImages();
|
this.searchImages();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
@ -157,28 +160,33 @@ export class MainRepositoryViewComponent {
|
||||||
|
|
||||||
loadStatus(): void {
|
loadStatus(): void {
|
||||||
this.http.get<any>(`${this.baseUrl}/image-repositories/server/${this.repositoryId}/status`).subscribe(data => {
|
this.http.get<any>(`${this.baseUrl}/image-repositories/server/${this.repositoryId}/status`).subscribe(data => {
|
||||||
const diskData = data.output.disk;
|
if (!data.success) {
|
||||||
const servicesData = data.output.services;
|
console.error('Error: No se pudo obtener los datos del servidor');
|
||||||
|
this.status = false;
|
||||||
this.diskUsage = {
|
return;
|
||||||
total: diskData.total,
|
|
||||||
used: diskData.used,
|
|
||||||
available: diskData.available,
|
|
||||||
percentage: diskData.used_percentage
|
|
||||||
};
|
|
||||||
|
|
||||||
this.diskUsageChartData = [
|
|
||||||
{
|
|
||||||
name: 'Usado',
|
|
||||||
value: parseFloat(diskData.used)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Disponible',
|
|
||||||
value: parseFloat(diskData.available)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.status = true;
|
||||||
|
|
||||||
|
const { disk, services, ram, cpu, processes } = data.output;
|
||||||
|
|
||||||
|
this.diskUsage = { ...disk };
|
||||||
|
this.diskUsageChartData = [
|
||||||
|
{ 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 => {
|
}, error => {
|
||||||
console.error('Error fetching status', error);
|
console.error('Error fetching status', error);
|
||||||
|
@ -186,10 +194,17 @@ export class MainRepositoryViewComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
getServices(): { name: string, status: string }[] {
|
getServices(): { name: string, status: string }[] {
|
||||||
return Object.keys(this.servicesStatus).map(key => ({
|
if (!this.status) {
|
||||||
name: key,
|
return [];
|
||||||
status: this.servicesStatus[key]
|
}
|
||||||
}));
|
return this.servicesStatus ? this.servicesStatus : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getProcesses(): { name: string, status: string }[] {
|
||||||
|
if (!this.status) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.processesStatus ? this.processesStatus : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
searchImages(): void {
|
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<any> {
|
|
||||||
return this.http.get<any>(`${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<any> {
|
loadAlert(): Observable<any> {
|
||||||
return this.http.get<any>(`${this.baseUrl}/image-repositories/server/get-collection`);
|
return this.http.get<any>(`${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() {
|
openImageInfoDialog() {
|
||||||
this.loadAlert().subscribe(
|
this.loadAlert().subscribe(
|
||||||
response => {
|
response => {
|
||||||
|
|
|
@ -100,3 +100,10 @@ table {
|
||||||
margin: 8px 8px 8px 0;
|
margin: 8px 8px 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-container-title {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||||
<mat-icon>help</mat-icon>
|
<mat-icon>help</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<h2 class="title" joyrideStep="titleStep" text="Desde esta pantalla podrás ver y administrar los respositioros exitentes.">Administrar repositorios</h2>
|
<div class="header-container-title">
|
||||||
|
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
||||||
|
{{ 'repositoryTitle' | translate }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
<div class="images-button-row">
|
<div class="images-button-row">
|
||||||
<button mat-flat-button color="primary" (click)="addImage()" joyrideStep="addStep" text="Utiliza este botón para añadir un nuevo repositorio.">Añadir repositorio</button>
|
<button mat-flat-button color="primary" (click)="addImage()" joyrideStep="addStep" text="Utiliza este botón para añadir un nuevo repositorio.">Añadir repositorio</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,11 +15,17 @@
|
||||||
|
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<mat-form-field appearance="fill" class="search-string">
|
<mat-form-field appearance="fill" class="search-string">
|
||||||
<mat-label>Buscar nombre de imagen</mat-label>
|
<mat-label>Buscar nombre de repositorio</mat-label>
|
||||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
|
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="fill" class="search-string">
|
||||||
|
<mat-label>Buscar IP de repositorio</mat-label>
|
||||||
|
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||||
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
|
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||||
|
@ -30,9 +40,10 @@
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||||
<td mat-cell *matCellDef="let client" style="text-align: center;">
|
<td mat-cell *matCellDef="let repository" style="text-align: center;">
|
||||||
<button mat-icon-button color="primary" (click)="editRepository($event, client)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
|
<button mat-icon-button color="primary" (click)="importImage($event, repository)" i18n="@@editImage"> <mat-icon>move_to_inbox</mat-icon></button>
|
||||||
<button mat-icon-button color="warn" (click)="deleteRepository($event, client)">
|
<button mat-icon-button color="primary" (click)="editRepository($event, repository)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
|
||||||
|
<button mat-icon-button color="warn" (click)="deleteRepository($event, repository)">
|
||||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delet
|
||||||
import { JoyrideService } from 'ngx-joyride';
|
import { JoyrideService } from 'ngx-joyride';
|
||||||
import {CreateRepositoryComponent} from "./create-repository/create-repository.component";
|
import {CreateRepositoryComponent} from "./create-repository/create-repository.component";
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import {ImportImageComponent} from "./import-image/import-image.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-repositories',
|
selector: 'app-repositories',
|
||||||
|
@ -91,6 +92,16 @@ export class RepositoriesComponent {
|
||||||
this.router.navigate(['repository', repository.uuid]);
|
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 {
|
deleteRepository(event: MouseEvent,command: any): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.dialog.open(DeleteModalComponent, {
|
this.dialog.open(DeleteModalComponent, {
|
||||||
|
|
|
@ -9,10 +9,11 @@
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
|
z-index: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin: 10px;
|
margin: 0px 10px 10px 10px;
|
||||||
padding: 10px;
|
padding: 0px 10px 10px 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div *ngIf="isLoading" class="overlay">
|
||||||
|
<mat-spinner></mat-spinner>
|
||||||
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { LoadingComponent } from './loading.component';
|
||||||
|
|
||||||
|
describe('LoadingComponent', () => {
|
||||||
|
let component: LoadingComponent;
|
||||||
|
let fixture: ComponentFixture<LoadingComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [LoadingComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(LoadingComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
|
@ -156,6 +156,8 @@
|
||||||
"newOrganizationalUnitTooltip": "Open modal to create organizational units of any type (Center, Classroom, Classroom Group, or Client Group)",
|
"newOrganizationalUnitTooltip": "Open modal to create organizational units of any type (Center, Classroom, Classroom Group, or Client Group)",
|
||||||
"newOrganizationalUnitButton": "New Organizational Unit",
|
"newOrganizationalUnitButton": "New Organizational Unit",
|
||||||
"newClientButton": "New Client",
|
"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",
|
"keyStepText": "The legend will show you the types of organizational units and their corresponding icons",
|
||||||
"legendButton": "Legend",
|
"legendButton": "Legend",
|
||||||
"unitStepText": "This is the section where 'Center' type organizational units will be displayed",
|
"unitStepText": "This is the section where 'Center' type organizational units will be displayed",
|
||||||
|
@ -215,7 +217,8 @@
|
||||||
"hardwareProfileLabel": "Hardware Profile",
|
"hardwareProfileLabel": "Hardware Profile",
|
||||||
"urlFormatError": "Invalid URL format.",
|
"urlFormatError": "Invalid URL format.",
|
||||||
"validationToggle": "Validation",
|
"validationToggle": "Validation",
|
||||||
"submitButton": "Add",
|
"addOUSubmitButton": "Add",
|
||||||
|
"editOUSubmitButton": "Edit",
|
||||||
"addOrgUnitTitle": "Add Organizational Unit",
|
"addOrgUnitTitle": "Add Organizational Unit",
|
||||||
"createOrgUnitparentLabel": "Parent organizational unit",
|
"createOrgUnitparentLabel": "Parent organizational unit",
|
||||||
"noParentOption": "--",
|
"noParentOption": "--",
|
||||||
|
@ -266,11 +269,13 @@
|
||||||
"diskUsedLabel": "Used",
|
"diskUsedLabel": "Used",
|
||||||
"diskTotalLabel": "Total",
|
"diskTotalLabel": "Total",
|
||||||
"diskImageAssistantTitle": "Disk image assistant",
|
"diskImageAssistantTitle": "Disk image assistant",
|
||||||
|
"deployImage": "Deploy image",
|
||||||
"partitionColumn": "Partition",
|
"partitionColumn": "Partition",
|
||||||
"isoImageColumn": "ISO Image",
|
"isoImageColumn": "ISO Image",
|
||||||
"ogliveColumn": "OgLive",
|
"ogliveColumn": "OgLive",
|
||||||
"selectImageOption": "Select image",
|
"selectImageOption": "Select image",
|
||||||
"selectOgLiveOption": "Select OgLive",
|
"selectOgLiveOption": "Select OgLive",
|
||||||
|
"repositoryTitle": "Admin Repository",
|
||||||
"saveAssociationsButton": "Save Associations",
|
"saveAssociationsButton": "Save Associations",
|
||||||
"partitionAssistantTitle": "Partition assistant",
|
"partitionAssistantTitle": "Partition assistant",
|
||||||
"diskSizeLabel": "Size",
|
"diskSizeLabel": "Size",
|
||||||
|
@ -278,6 +283,8 @@
|
||||||
"partitionSizeColumn": "Size (MB)",
|
"partitionSizeColumn": "Size (MB)",
|
||||||
"usageColumn": "Usage (%)",
|
"usageColumn": "Usage (%)",
|
||||||
"formatColumn": "Format",
|
"formatColumn": "Format",
|
||||||
|
"remotePcLabel": "Remote PC",
|
||||||
|
"globalImageLabel": "Global image",
|
||||||
"ntfsOption": "NTFS",
|
"ntfsOption": "NTFS",
|
||||||
"linuxOption": "LINUX",
|
"linuxOption": "LINUX",
|
||||||
"cacheOption": "CACHE",
|
"cacheOption": "CACHE",
|
||||||
|
@ -428,7 +435,7 @@
|
||||||
"addClientMenu": "Add client",
|
"addClientMenu": "Add client",
|
||||||
"filters": "Filters",
|
"filters": "Filters",
|
||||||
"searchClient": "Search client",
|
"searchClient": "Search client",
|
||||||
"searchTree": "Search in tree",
|
"searchTree": "Search organizational unit",
|
||||||
"filterByType": "Filter by type",
|
"filterByType": "Filter by type",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"classroomsGroup": "Classroom groups",
|
"classroomsGroup": "Classroom groups",
|
||||||
|
|
|
@ -157,6 +157,8 @@
|
||||||
"newOrganizationalUnitTooltip": "Abrir modal para crear unidades organizativas de cualquier tipo (Centro, Aula, Grupo de aulas o Grupo de clientes)",
|
"newOrganizationalUnitTooltip": "Abrir modal para crear unidades organizativas de cualquier tipo (Centro, Aula, Grupo de aulas o Grupo de clientes)",
|
||||||
"newOrganizationalUnitButton": "Nueva Unidad Organizativa",
|
"newOrganizationalUnitButton": "Nueva Unidad Organizativa",
|
||||||
"newClientButton": "Nuevo Cliente",
|
"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",
|
"keyStepText": "La leyenda te mostrará los tipos de unidades organizativas y sus iconos correspondientes",
|
||||||
"legendButton": "Leyenda",
|
"legendButton": "Leyenda",
|
||||||
"unitStepText": "Esta es la sección donde se mostrarán las unidades organizativas de tipo 'Centro'",
|
"unitStepText": "Esta es la sección donde se mostrarán las unidades organizativas de tipo 'Centro'",
|
||||||
|
@ -215,7 +217,8 @@
|
||||||
"hardwareProfileLabel": "Perfil de Hardware",
|
"hardwareProfileLabel": "Perfil de Hardware",
|
||||||
"urlFormatError": "Formato de URL inválido.",
|
"urlFormatError": "Formato de URL inválido.",
|
||||||
"validationToggle": "Validación",
|
"validationToggle": "Validación",
|
||||||
"submitButton": "Añadir",
|
"addOUSubmitButton": "Añadir",
|
||||||
|
"editOUSubmitButton": "Editar",
|
||||||
"addOrgUnitTitle": "Añadir Unidad Organizativa",
|
"addOrgUnitTitle": "Añadir Unidad Organizativa",
|
||||||
"createOrgUnitparentLabel": "Unidad organizativa padre",
|
"createOrgUnitparentLabel": "Unidad organizativa padre",
|
||||||
"noParentOption": "--",
|
"noParentOption": "--",
|
||||||
|
@ -271,6 +274,7 @@
|
||||||
"isoImageColumn": "Imagen ISO",
|
"isoImageColumn": "Imagen ISO",
|
||||||
"ogliveColumn": "OgLive",
|
"ogliveColumn": "OgLive",
|
||||||
"selectImageOption": "Seleccionar imagen",
|
"selectImageOption": "Seleccionar imagen",
|
||||||
|
"deployImage": "Desplegar imagen",
|
||||||
"selectOgLiveOption": "Seleccionar OgLive",
|
"selectOgLiveOption": "Seleccionar OgLive",
|
||||||
"saveAssociationsButton": "Guardar Asociaciones",
|
"saveAssociationsButton": "Guardar Asociaciones",
|
||||||
"partitionAssistantTitle": "Asistente de particionado",
|
"partitionAssistantTitle": "Asistente de particionado",
|
||||||
|
@ -296,6 +300,9 @@
|
||||||
"internalUnits": "Unidades internas",
|
"internalUnits": "Unidades internas",
|
||||||
"noResultsMessage": "No hay resultados para mostrar.",
|
"noResultsMessage": "No hay resultados para mostrar.",
|
||||||
"imagesTitle": "Administrar imágenes",
|
"imagesTitle": "Administrar imágenes",
|
||||||
|
"repositoryTitle": "Administrar repositorios",
|
||||||
|
"remotePcLabel": "Remote PC",
|
||||||
|
"globalImageLabel": "Imagen Global",
|
||||||
"addImageButton": "Añadir imagen",
|
"addImageButton": "Añadir imagen",
|
||||||
"searchNameDescription": "Busca imágenes por nombre para encontrar rápidamente una imagen específica.",
|
"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.",
|
"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",
|
"addClientMenu": "Añadir cliente",
|
||||||
"filters": "Filtros",
|
"filters": "Filtros",
|
||||||
"searchClient": "Buscar cliente",
|
"searchClient": "Buscar cliente",
|
||||||
"searchTree": "Buscar en árbol",
|
"searchTree": "Buscar unidad organizativa",
|
||||||
"filterByType": "Filtrar por tipo",
|
"filterByType": "Filtrar por tipo",
|
||||||
"all": "Todos",
|
"all": "Todos",
|
||||||
"classroomsGroup": "Grupos de aulas",
|
"classroomsGroup": "Grupos de aulas",
|
||||||
|
|
Loading…
Reference in New Issue