develop #12
|
@ -31,7 +31,7 @@ import {
|
|||
} from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component";
|
||||
import {RepositoriesComponent} from "./components/repositories/repositories.component";
|
||||
import {
|
||||
CreateImageComponent
|
||||
CreateClientImageComponent
|
||||
} from "./components/groups/components/client-main-view/create-image/create-image.component";
|
||||
import {
|
||||
DeployImageComponent
|
||||
|
@ -63,10 +63,10 @@ const routes: Routes = [
|
|||
{ path: 'commands-task', component: CommandsTaskComponent },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent },
|
||||
{ path: 'calendars', component: CalendarComponent },
|
||||
{ path: 'clients/deploy-image', component: DeployImageComponent },
|
||||
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent },
|
||||
{ path: 'clients/:id', component: ClientMainViewComponent },
|
||||
{ path: 'clients/:id/partition-assistant', component: PartitionAssistantComponent },
|
||||
{ path: 'clients/:id/create-image', component: CreateImageComponent },
|
||||
{ path: 'clients/:id/deploy-image', component: DeployImageComponent },
|
||||
{ path: 'clients/:id/create-image', component: CreateClientImageComponent },
|
||||
{ path: 'images', component: ImagesComponent },
|
||||
{ path: 'repositories', component: RepositoriesComponent },
|
||||
{ path: 'repository/:id', component: MainRepositoryViewComponent },
|
||||
|
|
|
@ -52,7 +52,6 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
|
|||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { ShowOrganizationalUnitComponent } from './components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component';
|
||||
import { MatGridList, MatGridTile } from "@angular/material/grid-list";
|
||||
import { TreeViewComponent } from './components/groups/shared/tree-view/tree-view.component';
|
||||
import {
|
||||
MatNestedTreeNode,
|
||||
MatTree,
|
||||
|
@ -102,6 +101,7 @@ import {MatSliderModule} from '@angular/material/slider';
|
|||
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||
import { ImagesComponent } from './components/images/images.component';
|
||||
import { CreateImageComponent } from './components/images/create-image/create-image.component';
|
||||
import { CreateClientImageComponent} from './components/groups/components/client-main-view/create-image/create-image.component';
|
||||
import { PartitionAssistantComponent } from './components/groups/components/client-main-view/partition-assistant/partition-assistant.component';
|
||||
import { SoftwareComponent } from './components/software/software.component';
|
||||
import { CreateSoftwareComponent } from './components/software/create-software/create-software.component';
|
||||
|
@ -124,6 +124,9 @@ import { MatSortModule } from '@angular/material/sort';
|
|||
import { MenusComponent } from './components/menus/menus.component';
|
||||
import { CreateMenuComponent } from './components/menus/create-menu/create-menu.component';
|
||||
import { CreateMultipleClientComponent } from './components/groups/shared/clients/create-multiple-client/create-multiple-client.component';
|
||||
import { ExportImageComponent } from './components/images/export-image/export-image.component';
|
||||
import {ImportImageComponent} from "./components/repositories/import-image/import-image.component";
|
||||
import { LoadingComponent } from './shared/loading/loading.component';
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
}
|
||||
|
@ -152,7 +155,6 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
ClassroomViewComponent,
|
||||
ClientViewComponent,
|
||||
ShowOrganizationalUnitComponent,
|
||||
TreeViewComponent,
|
||||
LegendComponent,
|
||||
ClassroomViewDialogComponent,
|
||||
SaveFiltersDialogComponent,
|
||||
|
@ -174,6 +176,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
CreateCommandComponent,
|
||||
CalendarComponent,
|
||||
CreateCalendarComponent,
|
||||
CreateClientImageComponent,
|
||||
CreateCalendarRuleComponent,
|
||||
CommandsGroupsComponent,
|
||||
CommandsTaskComponent,
|
||||
|
@ -205,7 +208,10 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
EnvVarsComponent,
|
||||
MenusComponent,
|
||||
CreateMenuComponent,
|
||||
CreateMultipleClientComponent
|
||||
CreateMultipleClientComponent,
|
||||
ExportImageComponent,
|
||||
ImportImageComponent,
|
||||
LoadingComponent,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
|
|
@ -90,3 +90,10 @@ table {
|
|||
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()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' | translate }}</h2>
|
||||
|
||||
<div class="header-container-title">
|
||||
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' | translate }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="resetFiltersStep" text="{{ 'resetFiltersStepText' | translate }}">
|
||||
{{ 'resetFilters' | translate }}
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
<button mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu">
|
||||
<mat-icon>terminal</mat-icon>
|
||||
</button>
|
||||
<ng-container [ngSwitch]="buttonType">
|
||||
<button *ngSwitchCase="'icon'" mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu">
|
||||
<mat-icon>{{ icon }}</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #commandMenu="matMenu">
|
||||
<button mat-menu-item [disabled]="command.disabled" *ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)">
|
||||
<button mat-flat-button [disabled]="clientData.length === 0" *ngSwitchCase="'text'" mat-button color="primary" [matMenuTriggerFor]="commandMenu">
|
||||
{{ 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 }}
|
||||
</button>
|
||||
</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 { HttpClient } from '@angular/common/http';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
|
@ -11,7 +11,10 @@ import {ToastrService} from "ngx-toastr";
|
|||
styleUrls: ['./execute-command.component.css']
|
||||
})
|
||||
export class ExecuteCommandComponent implements OnInit {
|
||||
@Input() clientData: any = {};
|
||||
@Input() clientData: any[] = [];
|
||||
@Input() buttonType: 'icon' | 'text' = 'icon';
|
||||
@Input() buttonText: string = 'Ejecutar Comandos';
|
||||
@Input() icon: string = 'terminal';
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
loading: boolean = true;
|
||||
|
||||
|
@ -29,6 +32,8 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
{name: 'Ejecutar script', slug: 'run-script', disabled: true},
|
||||
];
|
||||
|
||||
client: any = {};
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
|
@ -39,20 +44,13 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.clientData = this.clientData || {};
|
||||
this.loadClient(this.clientData)
|
||||
this.clientData = this.clientData || [];
|
||||
}
|
||||
|
||||
loadClient = (uuid: string) => {
|
||||
this.http.get<any>(`${this.baseUrl}${uuid}`).subscribe({
|
||||
next: data => {
|
||||
this.clientData = data;
|
||||
this.loading = false;
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error al obtener el cliente:', error);
|
||||
}
|
||||
});
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['clientData']) {
|
||||
console.log(this.clientData.length)
|
||||
}
|
||||
}
|
||||
|
||||
onCommandSelect(action: any): void {
|
||||
|
@ -82,7 +80,9 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}
|
||||
|
||||
rebootClient(): void {
|
||||
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/reboot`, {}).subscribe(
|
||||
this.http.post(`${this.baseUrl}/clients/server/reboot`, {
|
||||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
|
@ -93,13 +93,11 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}
|
||||
|
||||
powerOnClient(): void {
|
||||
const payload = {
|
||||
client: this.clientData['@id']
|
||||
}
|
||||
|
||||
this.http.post(`${this.baseUrl}${this.clientData.repository['@id']}/wol`, payload).subscribe(
|
||||
this.http.post(`${this.baseUrl}/image-repositories/wol`, {
|
||||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
this.toastService.success('Petición de encendido enviada correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
|
@ -108,9 +106,11 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}
|
||||
|
||||
powerOffClient(): void {
|
||||
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/power-off`, {}).subscribe(
|
||||
this.http.post(`${this.baseUrl}/clients/server/power-off`, {
|
||||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
this.toastService.success('Petición de apagado enviada correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
|
@ -119,21 +119,24 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}
|
||||
|
||||
openPartitionAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/partition-assistant`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
this.router.navigate(['/clients/partition-assistant'], {
|
||||
state: { clientData: this.clientData },
|
||||
}).then(r => {
|
||||
console.log('Navigated to partition assistant with data:', this.clientData);
|
||||
});
|
||||
}
|
||||
|
||||
openCreateImageAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/create-image`]).then(r => {
|
||||
this.router.navigate([`/clients/${this.clientData[0].uuid}/create-image`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
||||
openDeployImageAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/deploy-image`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
this.router.navigate(['/clients/deploy-image'], {
|
||||
state: { clientData: this.clientData },
|
||||
}).then(r => {
|
||||
console.log('Navigated to deploy image with data:', this.clientData);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -302,7 +302,7 @@ export class ClientMainViewComponent implements OnInit {
|
|||
}
|
||||
|
||||
openDeployImageAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/deploy-image`]).then(r => {
|
||||
this.router.navigate([`/clients/deploy-image`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div class="header-container">
|
||||
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
||||
<h2 class="title" i18n="@@subnetsTitle">Crear Imagen desde {{ clientName }}</h2>
|
||||
<div class="header-container-title">
|
||||
<h2 >
|
||||
Crear imagen desde {{ clientName }}
|
||||
</h2>
|
||||
</div>
|
||||
<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>
|
||||
<mat-divider></mat-divider>
|
||||
|
@ -12,14 +17,6 @@
|
|||
<mat-label>Nombre canónico</mat-label>
|
||||
<input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
|
||||
</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>
|
||||
|
||||
<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 {ToastrService} from "ngx-toastr";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {MatDivider} from "@angular/material/divider";
|
||||
import {NgForOf, NgIf} from "@angular/common";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {
|
||||
MatCell, MatCellDef,
|
||||
MatColumnDef,
|
||||
MatHeaderCell,
|
||||
MatHeaderCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef,
|
||||
MatTable,
|
||||
MatTableDataSource
|
||||
} from "@angular/material/table";
|
||||
import {MatChip} from "@angular/material/chips";
|
||||
import {MatCheckbox} from "@angular/material/checkbox";
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {SelectionModel} from "@angular/cdk/collections";
|
||||
import {MatRadioButton, MatRadioGroup} from "@angular/material/radio";
|
||||
import {MatFormField, MatLabel} from "@angular/material/form-field";
|
||||
import {MatOption} from "@angular/material/autocomplete";
|
||||
import {MatSelect} from "@angular/material/select";
|
||||
import {MatInput} from "@angular/material/input";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-image',
|
||||
templateUrl: './create-image.component.html',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatButton,
|
||||
MatDivider,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
ReactiveFormsModule,
|
||||
MatTable,
|
||||
MatColumnDef,
|
||||
MatHeaderCell,
|
||||
MatHeaderCellDef,
|
||||
MatCell,
|
||||
MatCellDef,
|
||||
MatChip,
|
||||
MatHeaderRow,
|
||||
MatRow,
|
||||
MatHeaderRowDef,
|
||||
MatRowDef,
|
||||
MatCheckbox,
|
||||
MatRadioGroup,
|
||||
MatRadioButton,
|
||||
MatFormField,
|
||||
MatLabel,
|
||||
MatOption,
|
||||
MatSelect,
|
||||
MatInput,
|
||||
FormsModule
|
||||
],
|
||||
styleUrl: './create-image.component.css'
|
||||
})
|
||||
export class CreateImageComponent {
|
||||
export class CreateClientImageComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
|
||||
|
@ -65,10 +18,10 @@ export class CreateImageComponent {
|
|||
partitions: any[] = [];
|
||||
images: any[] = [];
|
||||
clientName: string = '';
|
||||
selectedImage: string | null = null;
|
||||
selectedPartition: any = null;
|
||||
name: string = '';
|
||||
client: any = null;
|
||||
loading: boolean = false;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
columns = [
|
||||
{
|
||||
|
@ -111,7 +64,6 @@ export class CreateImageComponent {
|
|||
|
||||
ngOnInit() {
|
||||
this.clientId = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
this.loadPartitions();
|
||||
this.loadImages();
|
||||
}
|
||||
|
@ -147,15 +99,12 @@ export class CreateImageComponent {
|
|||
);
|
||||
}
|
||||
|
||||
back() {
|
||||
this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.loading = true;
|
||||
|
||||
const payload = {
|
||||
client: `/clients/${this.clientId}`,
|
||||
name: this.name,
|
||||
image: this.selectedImage,
|
||||
partition: this.selectedPartition['@id'],
|
||||
source: 'assistant'
|
||||
};
|
||||
|
@ -165,11 +114,12 @@ export class CreateImageComponent {
|
|||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Petición de creación de imagen enviada');
|
||||
this.loading = false;
|
||||
this.router.navigate(['/images']);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error:', error);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -37,6 +37,7 @@ table {
|
|||
width: 100%;
|
||||
padding: 0 5px;
|
||||
box-sizing: border-box;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
|
@ -70,7 +71,6 @@ table {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
|
@ -82,3 +82,47 @@ table {
|
|||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
display: block;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,45 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div class="header-container">
|
||||
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
||||
<h2 class="title" i18n="@@subnetsTitle">Desplegar imagen en {{ clientName }}</h2>
|
||||
<div class="header-container-title">
|
||||
<h2>
|
||||
{{ 'deployImage' | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
<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>
|
||||
<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="option-container">
|
||||
<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-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 appearance="fill" class="input-field">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import {MatExpansionModule} from "@angular/material/expansion";
|
||||
import {LoadingComponent} from "../../../../../shared/loading/loading.component";
|
||||
|
||||
describe('DeployImageComponent', () => {
|
||||
let component: DeployImageComponent;
|
||||
|
@ -24,7 +26,7 @@ describe('DeployImageComponent', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DeployImageComponent],
|
||||
declarations: [DeployImageComponent, LoadingComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
|
@ -32,6 +34,7 @@ describe('DeployImageComponent', () => {
|
|||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatExpansionModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatDividerModule,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Component, EventEmitter, Output} from '@angular/core';
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {SelectionModel} from "@angular/cdk/collections";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
|
@ -33,11 +33,13 @@ export class DeployImageComponent {
|
|||
p2pTime: Number = 0;
|
||||
name: string = '';
|
||||
client: any = null;
|
||||
clientData: any = [];
|
||||
loading: boolean = false;
|
||||
|
||||
protected p2pModeOptions = [
|
||||
{ name: 'Leecher', value: 'p2p-mode-leecher' },
|
||||
{ name: 'Peer', value: 'p2p-mode-peer' },
|
||||
{ name: 'Seeder', value: 'p2p-mode-seeder' },
|
||||
{ name: 'Leecher', value: 'leecher' },
|
||||
{ name: 'Peer', value: 'peer' },
|
||||
{ name: 'Seeder', value: 'seeder' },
|
||||
];
|
||||
protected multicastModeOptions = [
|
||||
{ name: 'Half duplex', value: "half"},
|
||||
|
@ -47,7 +49,6 @@ export class DeployImageComponent {
|
|||
allMethods = [
|
||||
'uftp',
|
||||
'udpcast',
|
||||
'multicast-direct',
|
||||
'unicast',
|
||||
'unicast-direct',
|
||||
'p2p'
|
||||
|
@ -56,7 +57,6 @@ export class DeployImageComponent {
|
|||
updateCacheMethods = [
|
||||
'uftp',
|
||||
'udpcast',
|
||||
'multicast',
|
||||
'unicast',
|
||||
'p2p'
|
||||
];
|
||||
|
@ -98,13 +98,12 @@ export class DeployImageComponent {
|
|||
private toastService: ToastrService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.clientId = this.route.snapshot.paramMap.get('id');
|
||||
this.selectedOption = 'deploy-image';
|
||||
this.loadPartitions();
|
||||
) {
|
||||
const navigation = this.router.getCurrentNavigation();
|
||||
this.clientData = navigation?.extras?.state?.['clientData'];
|
||||
this.clientId = this.clientData?.[0]['@id'];
|
||||
this.loadImages();
|
||||
this.loadPartitions()
|
||||
}
|
||||
|
||||
get deployMethods() {
|
||||
|
@ -116,7 +115,7 @@ export class DeployImageComponent {
|
|||
}
|
||||
|
||||
loadPartitions() {
|
||||
const url = `${this.baseUrl}/clients/${this.clientId}`;
|
||||
const url = `${this.baseUrl}${this.clientId}`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
|
@ -151,11 +150,9 @@ export class DeployImageComponent {
|
|||
);
|
||||
}
|
||||
|
||||
back() {
|
||||
this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.loading = true;
|
||||
|
||||
if (!this.selectedImage) {
|
||||
this.toastService.error('Debe seleccionar una imagen');
|
||||
return;
|
||||
|
@ -171,26 +168,44 @@ export class DeployImageComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.toastService.info('Preparando petición de despliegue');
|
||||
|
||||
|
||||
const payload = {
|
||||
client: `/clients/${this.clientId}`,
|
||||
clients: this.clientData.map((client: any) => client['@id']),
|
||||
method: this.selectedMethod,
|
||||
partition: this.selectedPartition['@id'],
|
||||
// partition: this.selectedPartition['@id'],
|
||||
diskNumber: this.selectedPartition.diskNumber,
|
||||
partitionNumber: this.selectedPartition.partitionNumber,
|
||||
p2pMode: this.p2pMode,
|
||||
p2pTime: this.p2pTime,
|
||||
mcastIp: this.mcastIp,
|
||||
mcastPort: this.mcastPort,
|
||||
mcastMode: this.mcastMode,
|
||||
mcastSpeed: this.mcastSpeed,
|
||||
maxTime: this.mcastMaxTime,
|
||||
maxClients: this.mcastMaxClients,
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}${this.selectedImage}/deploy-image`, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Petición de despliegue enviada correctamente');
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error:', error);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.toastService.error(error.error['hydra:description'], 'Se ha detectado un error en el despliegue de imágenes.', {
|
||||
"closeButton": true,
|
||||
"newestOnTop": false,
|
||||
"progressBar": false,
|
||||
"positionClass": "toast-bottom-right",
|
||||
"timeOut": 0,
|
||||
"extendedTimeOut": 0,
|
||||
"tapToDismiss": false
|
||||
});
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -167,3 +167,58 @@ button.remove-btn:hover {
|
|||
padding: 20px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
display: block;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
box-sizing: border-box;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,12 +1,45 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div class="header-container">
|
||||
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
||||
<h2 class="title" i18n="@@subnetsTitle">Asistente de particionado</h2>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
||||
Asistente de particionado
|
||||
</h2>
|
||||
</div>
|
||||
<div class="subnets-button-row">
|
||||
<button mat-flat-button color="primary" [disabled]="data.status === 'busy'" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div class="disk-select">
|
||||
<mat-form-field appearance="fill">
|
||||
|
|
|
@ -26,7 +26,7 @@ interface Partition {
|
|||
templateUrl: './partition-assistant.component.html',
|
||||
styleUrls: ['./partition-assistant.component.css']
|
||||
})
|
||||
export class PartitionAssistantComponent implements OnInit {
|
||||
export class PartitionAssistantComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
partitionTypes = PARTITION_TYPES;
|
||||
|
@ -39,6 +39,8 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
updateRequests: any[] = [];
|
||||
data: any = {};
|
||||
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = [];
|
||||
clientData: any = [];
|
||||
loading: boolean = false;
|
||||
|
||||
private apiUrl: string = this.baseUrl + '/partitions';
|
||||
|
||||
|
@ -51,11 +53,12 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
private toastService: ToastrService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.clientId = this.route.snapshot.paramMap.get('id');
|
||||
) {
|
||||
const navigation = this.router.getCurrentNavigation();
|
||||
this.clientData = navigation?.extras?.state?.['clientData'];
|
||||
this.clientId = this.clientData[0]['@id'];
|
||||
this.loadPartitions();
|
||||
|
||||
}
|
||||
|
||||
get selectedDisk():any {
|
||||
|
@ -63,7 +66,7 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadPartitions() {
|
||||
const url = `${this.baseUrl}/clients/${this.clientId}`;
|
||||
const url = `${this.baseUrl}${this.clientId}`;
|
||||
this.http.get(url).subscribe(
|
||||
(response) => {
|
||||
this.data = response;
|
||||
|
@ -250,16 +253,14 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
return modifiedPartitions;
|
||||
}
|
||||
|
||||
back() {
|
||||
this.router.navigate(['clients', this.data.uuid], { state: { clientData: this.data } });
|
||||
}
|
||||
|
||||
save() {
|
||||
if (!this.selectedDisk) {
|
||||
this.errorMessage = 'Por favor selecciona un disco antes de guardar.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const totalPartitionSize = this.selectedDisk.partitions.reduce((sum: any, partition: { size: any; }) => sum + partition.size, 0);
|
||||
|
||||
if (totalPartitionSize > this.selectedDisk.totalDiskSize) {
|
||||
|
@ -283,22 +284,26 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
size: partition.size,
|
||||
partitionCode: partition.partitionCode,
|
||||
filesystem: partition.filesystem,
|
||||
client: `/clients/${this.clientId}`,
|
||||
uuid: partition.uuid,
|
||||
removed: partition.removed || false,
|
||||
format: partition.format || false,
|
||||
}));
|
||||
|
||||
if (newPartitions.length > 0) {
|
||||
const bulkPayload = { partitions: newPartitions };
|
||||
const bulkPayload = {
|
||||
partitions: newPartitions,
|
||||
clients: this.clientData.map((client: any) => client['@id']),
|
||||
};
|
||||
|
||||
this.http.post(this.apiUrl, bulkPayload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
|
||||
this.loading = false;
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al crear las particiones:', error);
|
||||
this.loading = false;
|
||||
this.toastService.error('Error al crear las particiones.');
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
|
@ -33,58 +32,6 @@ button[mat-raised-button] {
|
|||
font-size: 16px;
|
||||
}
|
||||
|
||||
mat-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
mat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.unidad-card {
|
||||
cursor: pointer;
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.unidad-card.selected-item {
|
||||
border: 2px solid #1976d2;
|
||||
}
|
||||
|
||||
mat-card-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
mat-card-title mat-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 8px;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
mat-card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.actions mat-icon {
|
||||
color: #757575;
|
||||
cursor: pointer;
|
||||
|
@ -186,7 +133,7 @@ button[mat-raised-button] {
|
|||
|
||||
mat-tree {
|
||||
background-color: #f9f9f9;
|
||||
padding: 10px;
|
||||
padding: 0px 10px 10px 10px;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node {
|
||||
|
@ -298,18 +245,14 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
|
||||
.filters-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
justify-content: start;
|
||||
gap: 1rem;
|
||||
margin: 2rem 0px 0.7rem 10px;
|
||||
}
|
||||
|
||||
.filters-container mat-form-field {
|
||||
flex: 1 1 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 16px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.chip-busy {
|
||||
|
@ -394,24 +337,17 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
|
||||
.tree-container {
|
||||
width: 25%;
|
||||
padding: 16px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.clients-container {
|
||||
width: 75%;
|
||||
padding: 16px;
|
||||
padding: 0px 16px 16px 16px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.clients-container h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.5em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -516,23 +452,10 @@ button[mat-raised-button] {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filters-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin: 16px 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.filters-container mat-form-field {
|
||||
flex: 1 1 300px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.client-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -586,7 +509,7 @@ button[mat-raised-button] {
|
|||
.clients-title-name {
|
||||
font-size: x-large;
|
||||
display: block;
|
||||
padding: 1rem 1rem 1rem 15px;
|
||||
padding: 1rem 1rem 1rem 13px;
|
||||
}
|
||||
|
||||
.no-clients-info {
|
||||
|
@ -594,4 +517,5 @@ button[mat-raised-button] {
|
|||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 1.5rem;
|
||||
margin-left: 16px;
|
||||
}
|
|
@ -15,8 +15,8 @@
|
|||
</button>
|
||||
<button mat-flat-button color="primary" [matMenuTriggerFor]="menuClients">{{ 'newClientButton' | translate }}</button>
|
||||
<mat-menu #menuClients="matMenu">
|
||||
<button mat-menu-item (click)="addClient($event)" >Añadir cliente unitario</button>
|
||||
<button mat-menu-item (click)="addMultipleClients($event)">Añadir clientes masivamente</button>
|
||||
<button mat-menu-item (click)="addClient($event)">{{ 'newSingleClientButton' | translate }}</button>
|
||||
<button mat-menu-item (click)="addMultipleClients($event)">{{ 'newMultipleClientButton' | translate }}</button>
|
||||
</mat-menu>
|
||||
|
||||
<button mat-flat-button (click)="openBottomSheet()" joyrideStep="keyStep" text="{{ 'keyStepText' | translate }}"
|
||||
|
@ -27,26 +27,43 @@
|
|||
</div>
|
||||
|
||||
<!-- Filters Panel -->
|
||||
<mat-expansion-panel *ngIf="isTreeViewActive" 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-panel" joyrideStep="filtersPanelStep" text="{{ 'filtersPanelStepText' | translate }}">
|
||||
<div class="filters-container">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'searchClient' | translate }}</mat-label>
|
||||
<input matInput (input)="onClientFilterInput($event)" placeholder="Buscar nombre, IP, estado o MAC">
|
||||
</mat-form-field>
|
||||
<mat-label>{{ 'searchTree' | translate }}</mat-label>
|
||||
<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 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-option *ngFor="let savedFilter of savedFilterNames" [value]="savedFilter">
|
||||
{{ savedFilter[0] }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</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-label>{{ 'filterByType' | translate }}</mat-label>
|
||||
<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="group">{{ 'computerGroups' | translate }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</mat-form-field> -->
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<!-- Unit details view-->
|
||||
<div class="main-container">
|
||||
<!-- Tree view -->
|
||||
<div class="tree-container">
|
||||
<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}">
|
||||
<mat-icon>{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}</mat-icon>
|
||||
</button>
|
||||
|
@ -83,7 +100,7 @@
|
|||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</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>
|
||||
<mat-icon style="color: green;">
|
||||
{{
|
||||
|
@ -125,20 +142,20 @@
|
|||
</button>
|
||||
<button mat-menu-item (click)="addClient($event, selectedNode)">
|
||||
<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 mat-menu-item (click)="addOU($event, selectedNode)">
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
<mat-icon>account_tree</mat-icon>
|
||||
<span>{{ 'addOrganizationalUnit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onEditNode($event, selectedNode)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</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)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
|
@ -148,10 +165,15 @@
|
|||
<!-- Clients view -->
|
||||
<div class="clients-container">
|
||||
<div class="clients-view-header">
|
||||
<span class="clients-title-name">{{ 'clients' | translate }}
|
||||
<span class="clients-title-name">{{ 'clients' | translate }}
|
||||
<strong>{{ selectedNode?.name }}</strong>
|
||||
</span>
|
||||
</span>
|
||||
<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'">
|
||||
<mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
|
||||
</button>
|
||||
|
@ -193,7 +215,11 @@
|
|||
<button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</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>
|
||||
|
@ -202,9 +228,27 @@
|
|||
<!-- List view -->
|
||||
<div class="clients-table" *ngIf="currentView === 'list'">
|
||||
<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">
|
||||
<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
|
||||
[src]="'assets/images/ordenador_' + client.status + '.png'"
|
||||
alt="Client Icon"
|
||||
|
@ -231,7 +275,8 @@
|
|||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<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-name">{{ client.name }}</div>
|
||||
<div class="client-ip">{{ client.ip }}</div>
|
||||
|
@ -268,7 +313,11 @@
|
|||
<button mat-icon-button [matMenuTriggerFor]="clientMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</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">
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
|
@ -278,7 +327,7 @@
|
|||
<mat-icon>visibility</mat-icon>
|
||||
<span>{{ 'viewDetails' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client, selectedNode)">
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
|
@ -288,7 +337,7 @@
|
|||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</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>
|
||||
|
@ -299,8 +348,8 @@
|
|||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
<div *ngIf="!isLoadingClients" class="no-clients-info">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ 'noClients' | translate }}</span>
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||
import { JoyrideModule } from 'ngx-joyride';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTreeModule } from '@angular/material/tree';
|
||||
import { TreeNode } from './model/model';
|
||||
import {ExecuteCommandComponent} from "../commands/main-commands/execute-command/execute-command.component";
|
||||
|
||||
describe('GroupsComponent', () => {
|
||||
let component: GroupsComponent;
|
||||
|
@ -31,7 +33,7 @@ describe('GroupsComponent', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [GroupsComponent],
|
||||
declarations: [GroupsComponent, ExecuteCommandComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
|
@ -80,21 +82,43 @@ describe('GroupsComponent', () => {
|
|||
expect(component.search).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call getFilters on ngOnInit', () => {
|
||||
spyOn(component, 'getFilters');
|
||||
component.ngOnInit();
|
||||
expect(component.getFilters).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call search method', () => {
|
||||
spyOn(component, 'search');
|
||||
component.search();
|
||||
expect(component.search).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call getFilters method', () => {
|
||||
spyOn(component, 'getFilters');
|
||||
component.getFilters();
|
||||
expect(component.getFilters).toHaveBeenCalled();
|
||||
it('should clear selection', () => {
|
||||
spyOn(component, 'clearSelection');
|
||||
component.clearSelection();
|
||||
expect(component.clearSelection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should toggle view', () => {
|
||||
component.toggleView('card');
|
||||
expect(component.currentView).toBe('card');
|
||||
component.toggleView('list');
|
||||
expect(component.currentView).toBe('list');
|
||||
});
|
||||
|
||||
it('should filter tree', () => {
|
||||
const searchTerm = 'test';
|
||||
spyOn(component, 'filterTree');
|
||||
component.filterTree(searchTerm);
|
||||
expect(component.filterTree).toHaveBeenCalledWith(searchTerm);
|
||||
});
|
||||
|
||||
it('should add multiple clients', () => {
|
||||
spyOn(component, 'addMultipleClients');
|
||||
const event = new MouseEvent('click');
|
||||
component.addMultipleClients(event);
|
||||
expect(component.addMultipleClients).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
it('should expand path to node', () => {
|
||||
const node: TreeNode = { id: '1', name: 'Node 1', type: 'type', children: [] };
|
||||
spyOn(component, 'expandPathToNode');
|
||||
component.expandPathToNode(node);
|
||||
expect(component.expandPathToNode).toHaveBeenCalledWith(node);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,6 @@ import { HttpClient } from '@angular/common/http';
|
|||
import { Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatBottomSheet } from '@angular/material/bottom-sheet';
|
||||
import { MatTabChangeEvent } from '@angular/material/tabs';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||
|
@ -16,7 +15,6 @@ import { CreateClientComponent } from './shared/clients/create-client/create-cli
|
|||
import { EditOrganizationalUnitComponent } from './shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component';
|
||||
import { EditClientComponent } from './shared/clients/edit-client/edit-client.component';
|
||||
import { ShowOrganizationalUnitComponent } from './shared/organizational-units/show-organizational-unit/show-organizational-unit.component';
|
||||
import { TreeViewComponent } from './shared/tree-view/tree-view.component';
|
||||
import { LegendComponent } from './shared/legend/legend.component';
|
||||
import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
|
||||
|
@ -24,6 +22,7 @@ import { MatSort } from '@angular/material/sort';
|
|||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import {CreateMultipleClientComponent} from "./shared/clients/create-multiple-client/create-multiple-client.component";
|
||||
import {SelectionModel} from "@angular/cdk/collections";
|
||||
|
||||
enum NodeType {
|
||||
OrganizationalUnit = 'organizational-unit',
|
||||
|
@ -53,17 +52,17 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
commands: Command[] = [];
|
||||
commandsLoading = false;
|
||||
selectedClients = new MatTableDataSource<Client>([]);
|
||||
selection = new SelectionModel<any>(true, []);
|
||||
cols = 4;
|
||||
selectedClientsOriginal: Client[] = [];
|
||||
currentView: 'card' | 'list' = 'list';
|
||||
isTreeViewActive = false;
|
||||
savedFilterNames: [string, string][] = [];
|
||||
selectedTreeFilter = '';
|
||||
syncStatus = false;
|
||||
syncingClientId: string | null = null;
|
||||
private originalTreeData: TreeNode[] = [];
|
||||
arrayClients: any[] = [];
|
||||
|
||||
displayedColumns: string[] = ['status','sync', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
|
||||
displayedColumns: string[] = ['select', 'status','sync', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
|
||||
|
||||
private _sort!: MatSort;
|
||||
private _paginator!: MatPaginator;
|
||||
|
@ -112,9 +111,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
ngOnInit(): void {
|
||||
this.search();
|
||||
this.getFilters();
|
||||
this.updateGridCols();
|
||||
this.loadOrganizationalUnits();
|
||||
this.refreshData();
|
||||
window.addEventListener('resize', this.updateGridCols);
|
||||
|
||||
this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => {
|
||||
|
@ -144,40 +142,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
'@id': node['@id'],
|
||||
});
|
||||
|
||||
private loadOrganizationalUnits(): void {
|
||||
this.loading = true;
|
||||
this.isLoadingClients = true;
|
||||
this.dataService.getOrganizationalUnits().subscribe(
|
||||
(data) => {
|
||||
this.organizationalUnits = data;
|
||||
this.loading = false;
|
||||
|
||||
if (this.organizationalUnits.length > 0) {
|
||||
const treeData = this.organizationalUnits.map((unidad) => this.convertToTreeData(unidad));
|
||||
this.treeDataSource.data = treeData.flat();
|
||||
|
||||
this.isTreeViewActive = true;
|
||||
|
||||
const firstNode = this.treeDataSource.data[0];
|
||||
if (firstNode) {
|
||||
this.selectedNode = firstNode;
|
||||
this.fetchClientsForNode(firstNode);
|
||||
}
|
||||
} else {
|
||||
this.toastr.info('No existen unidades organizativas');
|
||||
this.isTreeViewActive = false;
|
||||
this.isLoadingClients = false;
|
||||
return;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching organizational units', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
toggleView(view: 'card' | 'list'): void {
|
||||
this.currentView = view;
|
||||
}
|
||||
|
@ -191,10 +155,22 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.selectedUnidad = null;
|
||||
this.selectedDetail = null;
|
||||
this.selectedClients.data = [];
|
||||
this.isTreeViewActive = false;
|
||||
this.selectedNode = null;
|
||||
}
|
||||
|
||||
// Función para obtener los filtros guardados actualmente deshabilitada
|
||||
// getFilters(): void {
|
||||
// this.subscriptions.add(
|
||||
// this.dataService.getFilters().subscribe(
|
||||
// (data) => {
|
||||
// this.savedFilterNames = data.map((filter: { name: string; uuid: string; }) => [filter.name, filter.uuid]);
|
||||
// },
|
||||
// (error) => {
|
||||
// console.error('Error fetching filters:', error);
|
||||
// }
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
getFilters(): void {
|
||||
this.subscriptions.add(
|
||||
this.dataService.getFilters().subscribe(
|
||||
|
@ -208,21 +184,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
loadSelectedFilter(savedFilter: [string, string]): void {
|
||||
this.subscriptions.add(
|
||||
this.dataService.getFilter(savedFilter[1]).subscribe(
|
||||
(response) => {
|
||||
if (response) {
|
||||
console.log('Filter:', response.filters);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
search(): void {
|
||||
this.loading = true;
|
||||
this.subscriptions.add(
|
||||
|
@ -239,49 +200,112 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
private async loadChildrenAndClients(id: string): Promise<UnidadOrganizativa> {
|
||||
try {
|
||||
const childrenData = await this.dataService.getChildren(id).toPromise();
|
||||
|
||||
const processHierarchy = (nodes: UnidadOrganizativa[]): UnidadOrganizativa[] => {
|
||||
return nodes.map((node) => ({
|
||||
...node,
|
||||
children: node.children ? processHierarchy(node.children) : [],
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
...this.selectedUnidad!,
|
||||
children: childrenData ? processHierarchy(childrenData) : [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading children:', error);
|
||||
return this.selectedUnidad!;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private convertToTreeData(data: UnidadOrganizativa): TreeNode[] {
|
||||
private convertToTreeData(data: UnidadOrganizativa): TreeNode {
|
||||
const processNode = (node: UnidadOrganizativa): TreeNode => ({
|
||||
id: node.id,
|
||||
uuid: node.uuid,
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
'@id': node['@id'],
|
||||
children: node.children?.map(processNode) || [],
|
||||
hasClients: (node.clients?.length ?? 0) > 0,
|
||||
});
|
||||
return [processNode(data)];
|
||||
return processNode(data);
|
||||
}
|
||||
|
||||
private refreshData(selectedNodeIdOrUuid?: string): void {
|
||||
this.loading = true;
|
||||
this.isLoadingClients = !!selectedNodeIdOrUuid;
|
||||
|
||||
this.dataService.getOrganizationalUnits().subscribe({
|
||||
next: (data) => {
|
||||
this.originalTreeData = data.map((unidad) => this.convertToTreeData(unidad));
|
||||
this.treeDataSource.data = [...this.originalTreeData];
|
||||
|
||||
if (selectedNodeIdOrUuid) {
|
||||
this.selectedNode = this.findNodeByIdOrUuid(this.treeDataSource.data, selectedNodeIdOrUuid);
|
||||
|
||||
if (this.selectedNode) {
|
||||
this.treeControl.collapseAll();
|
||||
this.expandPathToNode(this.selectedNode);
|
||||
this.fetchClientsForNode(this.selectedNode);
|
||||
}
|
||||
} else {
|
||||
this.treeControl.collapseAll();
|
||||
if (this.treeDataSource.data.length > 0) {
|
||||
this.selectedNode = this.treeDataSource.data[0];
|
||||
this.fetchClientsForNode(this.selectedNode);
|
||||
} else {
|
||||
this.selectedNode = null;
|
||||
this.selectedClients.data = [];
|
||||
}
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.isLoadingClients = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error fetching organizational units', error);
|
||||
this.toastr.error('Ocurrió un error al cargar las unidades organizativas');
|
||||
this.loading = false;
|
||||
this.isLoadingClients = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
expandPathToNode(node: TreeNode): void {
|
||||
const path: TreeNode[] = [];
|
||||
let currentNode: TreeNode | null = node;
|
||||
|
||||
while (currentNode) {
|
||||
path.unshift(currentNode);
|
||||
currentNode = currentNode.id ? this.findParentNode(this.treeDataSource.data, currentNode.id) : null;
|
||||
}
|
||||
|
||||
path.forEach((pathNode) => {
|
||||
const flatNode = this.treeControl.dataNodes?.find((n) => n.id === pathNode.id);
|
||||
if (flatNode) {
|
||||
this.treeControl.expand(flatNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private findParentNode(treeData: TreeNode[], childId: string): TreeNode | null {
|
||||
for (const node of treeData) {
|
||||
if (node.children?.some((child) => child.id === childId)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
const parent = this.findParentNode(node.children, childId);
|
||||
if (parent) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private findNodeByIdOrUuid(treeData: TreeNode[], identifier: string): TreeNode | null {
|
||||
const search = (nodes: TreeNode[]): TreeNode | null => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === identifier || node.uuid === identifier) return node;
|
||||
if (node.children && node.children.length > 0) {
|
||||
const found = search(node.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return search(treeData);
|
||||
}
|
||||
|
||||
onNodeClick(node: TreeNode): void {
|
||||
console.log('Node clicked:', node);
|
||||
this.selectedNode = node;
|
||||
this.fetchClientsForNode(node);
|
||||
}
|
||||
|
||||
private fetchClientsForNode(node: TreeNode): void {
|
||||
console.log('Node:', node);
|
||||
this.isLoadingClients = true;
|
||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({
|
||||
next: (response) => {
|
||||
|
@ -294,110 +318,82 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
getNodeIcon(node: TreeNode): string {
|
||||
switch (node.type) {
|
||||
case NodeType.OrganizationalUnit:
|
||||
return 'apartment';
|
||||
case NodeType.ClassroomsGroup:
|
||||
return 'doors';
|
||||
case NodeType.Classroom:
|
||||
return 'school';
|
||||
case NodeType.ClientsGroup:
|
||||
return 'lan';
|
||||
case NodeType.Client:
|
||||
return 'computer';
|
||||
default:
|
||||
return 'group';
|
||||
}
|
||||
}
|
||||
|
||||
addOU(event: MouseEvent, parent: TreeNode | null = null): void {
|
||||
event.stopPropagation();
|
||||
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, {
|
||||
data: { parent },
|
||||
width: '900px',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.refreshOrganizationalUnits();
|
||||
dialogRef.afterClosed().subscribe((newUnit) => {
|
||||
if (newUnit?.uuid) {
|
||||
console.log('Unidad organizativa creada:', newUnit);
|
||||
this.refreshData(newUnit.uuid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
||||
event.stopPropagation();
|
||||
const targetNode = organizationalUnit || this.selectedNode;
|
||||
const dialogRef = this.dialog.open(CreateClientComponent, {
|
||||
data: { organizationalUnit },
|
||||
data: { organizationalUnit: targetNode },
|
||||
width: '900px',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.refreshOrganizationalUnits();
|
||||
if (organizationalUnit && organizationalUnit['@id']) {
|
||||
this.refreshClientsForNode(organizationalUnit);
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result?.client && result?.organizationalUnit) {
|
||||
const organizationalUnitUrl = result.organizationalUnit;
|
||||
const uuid = organizationalUnitUrl.split('/')[2];
|
||||
const parentNode = this.findNodeByIdOrUuid(this.treeDataSource.data, uuid);
|
||||
|
||||
if (parentNode) {
|
||||
this.refreshData(parentNode.uuid);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
||||
event.stopPropagation();
|
||||
const targetNode = organizationalUnit || this.selectedNode;
|
||||
|
||||
const dialogRef = this.dialog.open(CreateMultipleClientComponent, {
|
||||
data: { organizationalUnit },
|
||||
data: { organizationalUnit: targetNode },
|
||||
width: '900px',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.refreshOrganizationalUnits();
|
||||
if (organizationalUnit && organizationalUnit['@id']) {
|
||||
this.refreshClientsForNode(organizationalUnit);
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result?.success) {
|
||||
const organizationalUnitUrl = result.organizationalUnit;
|
||||
const uuid = organizationalUnitUrl.split('/')[2];
|
||||
const parentNode = this.findNodeByIdOrUuid(this.treeDataSource.data, uuid);
|
||||
|
||||
if (parentNode) {
|
||||
console.log('Nodo padre encontrado para actualización:', parentNode);
|
||||
this.refreshData(parentNode.uuid);
|
||||
} else {
|
||||
console.error('No se encontró el nodo padre después de la creación masiva.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private refreshOrganizationalUnits(): void {
|
||||
const expandedNodeIds = this.treeControl.dataNodes
|
||||
? this.treeControl.dataNodes
|
||||
.filter(node => this.treeControl.isExpanded(node))
|
||||
.map(node => this.extractUuid(node['@id']))
|
||||
: [];
|
||||
|
||||
this.subscriptions.add(
|
||||
this.dataService.getOrganizationalUnits().subscribe(
|
||||
(data) => {
|
||||
this.organizationalUnits = data;
|
||||
if (this.selectedUnidad) {
|
||||
this.loadChildrenAndClients(this.selectedUnidad?.id || '').then((updatedData) => {
|
||||
this.selectedUnidad = updatedData;
|
||||
const treeData = this.convertToTreeData(updatedData);
|
||||
this.originalTreeData = treeData[0]?.children || [];
|
||||
this.treeDataSource.data = [...this.originalTreeData];
|
||||
|
||||
setTimeout(() => {
|
||||
this.treeControl.dataNodes.forEach(node => {
|
||||
const nodeId = this.extractUuid(node['@id']);
|
||||
if (nodeId && expandedNodeIds.includes(nodeId)) {
|
||||
this.treeControl.expand(node);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
(error) => console.error('Error fetching organizational units', error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
onEditNode(event: MouseEvent, node: TreeNode | null): void {
|
||||
event.stopPropagation();
|
||||
const uuid = node ? this.extractUuid(node['@id']) : null;
|
||||
if (!uuid) return;
|
||||
|
||||
if (node && node.type !== NodeType.Client) {
|
||||
this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' });
|
||||
} else {
|
||||
this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
|
||||
}
|
||||
const dialogRef = node?.type !== NodeType.Client
|
||||
? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
|
||||
: this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
if (node) {
|
||||
this.refreshData(node.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void {
|
||||
onDeleteClick(event: MouseEvent, node: TreeNode | null): void {
|
||||
event.stopPropagation();
|
||||
const uuid = node ? this.extractUuid(node['@id']) : null;
|
||||
if (!uuid) return;
|
||||
|
@ -410,44 +406,50 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result === true) {
|
||||
this.deleteEntity(uuid, node.type, node);
|
||||
this.deleteEntityorClient(uuid, node?.type);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private deleteEntity(uuid: string, type: string, node: TreeNode): void {
|
||||
this.subscriptions.add(
|
||||
this.dataService.deleteElement(uuid, type).subscribe(
|
||||
() => {
|
||||
this.refreshOrganizationalUnits();
|
||||
if (type === NodeType.Client) {
|
||||
this.refreshClientsForNode(node);
|
||||
}
|
||||
this.toastr.success('Entidad eliminada exitosamente');
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error deleting entity:', error);
|
||||
this.toastr.error('Error al eliminar la entidad', error.message);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
private deleteEntityorClient(uuid: string, type: string): void {
|
||||
if (!this.selectedNode) return;
|
||||
|
||||
private refreshClientsForNode(node: TreeNode): void {
|
||||
if (!node['@id']) {
|
||||
this.selectedClients.data = [];
|
||||
return;
|
||||
}
|
||||
this.fetchClientsForNode(node);
|
||||
const parentNode = this.selectedNode?.id
|
||||
? this.findParentNode(this.treeDataSource.data, this.selectedNode.id)
|
||||
: null;
|
||||
|
||||
this.dataService.deleteElement(uuid, type).subscribe({
|
||||
next: () => {
|
||||
const entityType = type === NodeType.Client ? 'Cliente' : 'Entidad';
|
||||
const verb = type === NodeType.Client ? 'eliminado' : 'eliminada';
|
||||
|
||||
this.toastr.success(`${entityType} ${verb} exitosamente`);
|
||||
|
||||
if (type === NodeType.Client) {
|
||||
this.refreshData(this.selectedNode?.id);
|
||||
} else if (parentNode) {
|
||||
this.refreshData(parentNode.id);
|
||||
} else {
|
||||
this.refreshData();
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting entity:', error);
|
||||
const entityType = type === NodeType.Client ? 'cliente' : 'entidad';
|
||||
this.toastr.error(`Error al eliminar el ${entityType}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onEditClick(event: MouseEvent, type: string, uuid: string): void {
|
||||
event.stopPropagation();
|
||||
if (type !== NodeType.Client) {
|
||||
this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' });
|
||||
} else {
|
||||
this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
|
||||
}
|
||||
const dialogRef = type !== NodeType.Client
|
||||
? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
|
||||
: this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.refreshData(this.selectedNode?.id);
|
||||
});
|
||||
}
|
||||
|
||||
onRoomMap(room: TreeNode | null): void {
|
||||
|
@ -467,22 +469,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
fetchCommands(): void {
|
||||
this.commandsLoading = true;
|
||||
this.subscriptions.add(
|
||||
this.http.get<{ 'hydra:member': Command[] }>(`${this.baseUrl}/commands?page=1&itemsPerPage=30`).subscribe(
|
||||
(response) => {
|
||||
this.commands = response['hydra:member'];
|
||||
this.commandsLoading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching commands:', error);
|
||||
this.commandsLoading = false;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
executeCommand(command: Command, selectedNode: TreeNode | null): void {
|
||||
|
||||
if (!selectedNode) {
|
||||
|
@ -509,13 +495,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
onTreeClick(event: MouseEvent, data: TreeNode): void {
|
||||
event.stopPropagation();
|
||||
if (data.type !== NodeType.Client) {
|
||||
this.dialog.open(TreeViewComponent, { data: { data }, width: '800px' });
|
||||
}
|
||||
}
|
||||
|
||||
openBottomSheet(): void {
|
||||
this.bottomSheet.open(LegendComponent);
|
||||
}
|
||||
|
@ -531,31 +510,54 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
hasChild = (_: number, node: FlatNode): boolean => node.expandable;
|
||||
isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable;
|
||||
|
||||
filterTree(searchTerm: string, filterType: string): void {
|
||||
const filterNodes = (nodes: TreeNode[]): TreeNode[] => {
|
||||
const filteredNodes: TreeNode[] = [];
|
||||
for (const node of nodes) {
|
||||
const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesType = filterType ? node.type.toLowerCase() === filterType.toLowerCase() : true;
|
||||
const filteredChildren = node.children ? filterNodes(node.children) : [];
|
||||
filterTree(searchTerm: string): void {
|
||||
const expandPaths: TreeNode[][] = [];
|
||||
|
||||
if ((matchesName && matchesType) || filteredChildren.length > 0) {
|
||||
filteredNodes.push({ ...node, children: filteredChildren });
|
||||
}
|
||||
}
|
||||
return filteredNodes;
|
||||
const filterNodes = (nodes: TreeNode[], parentPath: TreeNode[] = []): TreeNode[] => {
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const filteredChildren = node.children ? filterNodes(node.children, [...parentPath, node]) : [];
|
||||
|
||||
if (matchesName) {
|
||||
expandPaths.push([...parentPath, node]);
|
||||
return {
|
||||
...node,
|
||||
children: node.children,
|
||||
} as TreeNode;
|
||||
} else if (filteredChildren.length > 0) {
|
||||
return {
|
||||
...node,
|
||||
children: filteredChildren,
|
||||
} as TreeNode;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((node): node is TreeNode => node !== null);
|
||||
};
|
||||
|
||||
const filteredData = filterNodes(this.originalTreeData);
|
||||
this.treeDataSource.data = filteredData;
|
||||
if (!searchTerm) {
|
||||
this.treeDataSource.data = [...this.originalTreeData];
|
||||
this.treeControl.collapseAll();
|
||||
} else {
|
||||
this.treeDataSource.data = filterNodes(this.originalTreeData);
|
||||
expandPaths.forEach((path) => this.expandPath(path));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private expandPath(path: TreeNode[]): void {
|
||||
path.forEach((pathNode) => {
|
||||
const flatNode = this.treeControl.dataNodes?.find((n) => n.id === pathNode.id);
|
||||
if (flatNode) {
|
||||
this.treeControl.expand(flatNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onTreeFilterInput(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const searchTerm = input?.value || '';
|
||||
this.filterTree(searchTerm, this.selectedTreeFilter);
|
||||
const searchTerm = input?.value.trim() || '';
|
||||
this.filterTree(searchTerm);
|
||||
}
|
||||
|
||||
onClientFilterInput(event: Event): void {
|
||||
|
@ -569,7 +571,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.selectedClients.filter = this.searchTerm;
|
||||
}
|
||||
|
||||
|
||||
public setSelectedNode(node: TreeNode): void {
|
||||
this.selectedNode = node;
|
||||
}
|
||||
|
@ -586,19 +587,68 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.toastr.success('Cliente actualizado correctamente');
|
||||
this.syncStatus = false;
|
||||
this.syncingClientId = null;
|
||||
this.search()
|
||||
this.refreshData()
|
||||
},
|
||||
() => {
|
||||
this.toastr.error('Error de conexión con el cliente');
|
||||
this.syncStatus = false;
|
||||
this.syncingClientId = null;
|
||||
this.search()
|
||||
this.refreshData()
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
isAllSelected() {
|
||||
const numSelected = this.selection.selected.length;
|
||||
const numRows = this.selectedClients.data.length;
|
||||
return numSelected === numRows;
|
||||
}
|
||||
|
||||
toggleAllRows() {
|
||||
if (this.isAllSelected()) {
|
||||
this.selection.clear();
|
||||
this.arrayClients = []
|
||||
return;
|
||||
}
|
||||
|
||||
this.selection.select(...this.selectedClients.data);
|
||||
this.arrayClients = [...this.selection.selected];
|
||||
}
|
||||
|
||||
toggleRow(row: any) {
|
||||
this.selection.toggle(row);
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
updateSelectedClients() {
|
||||
this.arrayClients = [...this.selection.selected];
|
||||
}
|
||||
|
||||
getClientPath(client: Client): string {
|
||||
const path: string[] = [];
|
||||
let currentNode: TreeNode | null = this.findNodeByIdOrUuid(this.treeDataSource.data, client.organizationalUnit.uuid);
|
||||
|
||||
while (currentNode) {
|
||||
path.unshift(currentNode.name);
|
||||
currentNode = currentNode.id ? this.findParentNode(this.treeDataSource.data, currentNode.id) : null;
|
||||
}
|
||||
|
||||
return path.join(' / ');
|
||||
}
|
||||
|
||||
private extractUuid(idPath: string | undefined): string | null {
|
||||
return idPath ? idPath.split('/').pop() || null : null;
|
||||
}
|
||||
|
||||
clearTreeSearch(inputElement: HTMLInputElement): void {
|
||||
inputElement.value = '';
|
||||
this.filterTree('');
|
||||
}
|
||||
|
||||
clearClientSearch(inputElement: HTMLInputElement): void {
|
||||
inputElement.value = '';
|
||||
this.filterClients('');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ export interface ClientCollection {
|
|||
|
||||
export interface TreeNode {
|
||||
id?: string
|
||||
uuid?: string;
|
||||
name: string;
|
||||
type: 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-label i18n="@@organizational-unit-label">Padre</mat-label>
|
||||
<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-path">{{ unit.path }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -15,6 +15,7 @@ export class CreateClientComponent implements OnInit {
|
|||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
clientForm!: FormGroup;
|
||||
parentUnits: any[] = [];
|
||||
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||
hardwareProfiles: any[] = [];
|
||||
ogLives: any[] = [];
|
||||
menus: any[] = [];
|
||||
|
@ -80,6 +81,11 @@ export class CreateClientComponent implements OnInit {
|
|||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.parentUnits = response['hydra:member'];
|
||||
this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
|
||||
id: unit['@id'],
|
||||
name: unit.name,
|
||||
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits)
|
||||
}));
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
|
@ -89,6 +95,11 @@ export class CreateClientComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
getSelectedParentName(): string | undefined {
|
||||
const parentId = this.clientForm.get('organizationalUnit')?.value;
|
||||
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
|
||||
}
|
||||
|
||||
loadHardwareProfiles(): void {
|
||||
this.dataService.getHardwareProfiles().subscribe(
|
||||
(data: any[]) => {
|
||||
|
@ -157,15 +168,21 @@ export class CreateClientComponent implements OnInit {
|
|||
onSubmit(): void {
|
||||
if (this.clientForm.valid) {
|
||||
const formData = this.clientForm.value;
|
||||
|
||||
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
||||
response => {
|
||||
(response) => {
|
||||
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
||||
this.dialogRef.close(response);
|
||||
this.dialogRef.close({
|
||||
client: response,
|
||||
organizationalUnit: formData.organizationalUnit,
|
||||
});
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error al crear el cliente', 'Error');
|
||||
(error) => {
|
||||
this.toastService.error(error.error['hydra:description'], 'Error al crear el cliente');
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.toastService.error('Formulario inválido. Por favor, revise los campos obligatorios.', 'Error');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,9 +6,13 @@
|
|||
<form class="client-form grid-form" *ngIf="!loading">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select (selectionChange)="setOrganizationalUnit($event)">
|
||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
|
||||
<mat-select (selectionChange)="setOrganizationalUnit($event)" [value]="organizationalUnit">
|
||||
<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 style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -3,6 +3,8 @@ import {MatDialogRef} from "@angular/material/dialog";
|
|||
import {HttpClient} from "@angular/common/http";
|
||||
import {MatSnackBar} from "@angular/material/snack-bar";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {MAT_DIALOG_DATA} from "@angular/material/dialog";
|
||||
import { DataService } from '../../../services/data.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-multiple-client',
|
||||
|
@ -12,21 +14,29 @@ import {ToastrService} from "ngx-toastr";
|
|||
export class CreateMultipleClientComponent implements OnInit{
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
parentUnits: any[] = [];
|
||||
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||
uploadedClients: any[] = [];
|
||||
loading: boolean = false;
|
||||
displayedColumns: string[] = ['name', 'ip', 'mac'];
|
||||
showTextarea: boolean = true;
|
||||
organizationalUnit: any;
|
||||
regex: RegExp = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([a-zA-Z0-9]{2}(:[a-zA-Z0-9]{2}){5});\s+fixed-address\s+((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?));\s+\}/g;
|
||||
|
||||
constructor(
|
||||
@Optional() private dialogRef: MatDialogRef<CreateMultipleClientComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) private data: any,
|
||||
private http: HttpClient,
|
||||
private snackBar: MatSnackBar,
|
||||
private toastService: ToastrService
|
||||
private toastService: ToastrService,
|
||||
private dataService: DataService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadParentUnits();
|
||||
|
||||
if (this.data?.organizationalUnit) {
|
||||
this.organizationalUnit = this.data.organizationalUnit['@id'];
|
||||
}
|
||||
}
|
||||
|
||||
loadParentUnits(): void {
|
||||
|
@ -36,6 +46,11 @@ export class CreateMultipleClientComponent implements OnInit{
|
|||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.parentUnits = response['hydra:member'];
|
||||
this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
|
||||
id: unit['@id'],
|
||||
name: unit.name,
|
||||
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits)
|
||||
}));
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
|
@ -45,8 +60,12 @@ export class CreateMultipleClientComponent implements OnInit{
|
|||
);
|
||||
}
|
||||
|
||||
getSelectedParentName(): string | undefined {
|
||||
const parentId = this.organizationalUnit;
|
||||
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
|
||||
}
|
||||
|
||||
setOrganizationalUnit(organizationalUnit: any): void {
|
||||
console.log('Organizational unit selected:', organizationalUnit.value);
|
||||
this.organizationalUnit = organizationalUnit.value;
|
||||
}
|
||||
|
||||
|
@ -57,15 +76,14 @@ export class CreateMultipleClientComponent implements OnInit{
|
|||
|
||||
reader.onload = (e: any) => {
|
||||
const textData = e.target.result;
|
||||
const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g;
|
||||
let match;
|
||||
const clients = [];
|
||||
|
||||
while ((match = regex.exec(textData)) !== null) {
|
||||
while ((match = this.regex.exec(textData)) !== null) {
|
||||
clients.push({
|
||||
name: match[1],
|
||||
mac: match[2],
|
||||
ip: match[3]
|
||||
ip: match[4]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -84,15 +102,14 @@ export class CreateMultipleClientComponent implements OnInit{
|
|||
}
|
||||
|
||||
onTextarea(text: string): void {
|
||||
const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g;
|
||||
let match;
|
||||
const clients = [];
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
while ((match = this.regex.exec(text)) !== null) {
|
||||
clients.push({
|
||||
name: match[1],
|
||||
mac: match[2],
|
||||
ip: match[3]
|
||||
ip: match[4]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -108,6 +125,9 @@ export class CreateMultipleClientComponent implements OnInit{
|
|||
|
||||
onSubmit(): void {
|
||||
if (this.uploadedClients.length > 0) {
|
||||
let successCount = 0;
|
||||
let errorMessages: string[] = [];
|
||||
|
||||
this.uploadedClients.forEach(client => {
|
||||
const formData = {
|
||||
organizationalUnit: this.organizationalUnit,
|
||||
|
@ -118,20 +138,37 @@ export class CreateMultipleClientComponent implements OnInit{
|
|||
|
||||
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
||||
response => {
|
||||
this.toastService.success(`Cliente ${client.name} creado exitosamente`, 'Éxito');
|
||||
successCount++;
|
||||
if (successCount + errorMessages.length === this.uploadedClients.length) {
|
||||
this.showFinalToast(successCount, errorMessages);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description'], `Error al crear el cliente ${client.name}`);
|
||||
errorMessages.push(`Error al crear el cliente ${client.name}: ${error.error['hydra:description']}`);
|
||||
if (successCount + errorMessages.length === this.uploadedClients.length) {
|
||||
this.showFinalToast(successCount, errorMessages);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
this.uploadedClients = [];
|
||||
this.dialogRef.close();
|
||||
} else {
|
||||
this.toastService.error('No hay clientes cargados para añadir', 'Error');
|
||||
}
|
||||
}
|
||||
|
||||
showFinalToast(successCount: number, errorMessages: string[]): void {
|
||||
if (successCount > 0) {
|
||||
this.toastService.success(`${successCount} clientes creados exitosamente`, 'Éxito');
|
||||
}
|
||||
if (errorMessages.length > 0) {
|
||||
errorMessages.forEach(message => this.toastService.error(message, 'Error'));
|
||||
}
|
||||
this.dialogRef.close({
|
||||
success: successCount > 0,
|
||||
organizationalUnit: this.organizationalUnit,
|
||||
});
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
|
|
@ -6,8 +6,12 @@
|
|||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit">
|
||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
<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 style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -15,6 +15,7 @@ export class EditClientComponent {
|
|||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
clientForm!: FormGroup;
|
||||
parentUnits: any[] = [];
|
||||
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||
hardwareProfiles: any[] = [];
|
||||
repositories: any[] = [];
|
||||
ogLives: any[] = [];
|
||||
|
@ -68,19 +69,32 @@ export class EditClientComponent {
|
|||
});
|
||||
}
|
||||
|
||||
loadParentUnits() {
|
||||
loadParentUnits(): void {
|
||||
this.loading = true;
|
||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.parentUnits = response['hydra:member'];
|
||||
this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
|
||||
id: unit['@id'],
|
||||
name: unit.name,
|
||||
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits)
|
||||
}));
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching parent units:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getSelectedParentName(): string | undefined {
|
||||
const parentId = this.clientForm.get('organizationalUnit')?.value;
|
||||
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
|
||||
}
|
||||
|
||||
loadHardwareProfiles(): void {
|
||||
this.dataService.getHardwareProfiles().subscribe(
|
||||
(data: any[]) => {
|
||||
|
|
|
@ -39,4 +39,4 @@ button {
|
|||
|
||||
mat-slide-toggle{
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,182 +1,160 @@
|
|||
<h1 mat-dialog-title>{{ 'addOrgUnitTitle' | translate }}</h1>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<mat-stepper orientation="vertical" [linear]="isLinear">
|
||||
<!-- Paso 1: General -->
|
||||
<mat-step [stepControl]="generalFormGroup">
|
||||
<form [formGroup]="generalFormGroup">
|
||||
<ng-template matStepLabel>{{ 'generalStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
<mat-option *ngFor="let type of filteredTypes" [value]="type">
|
||||
{{ typeTranslations[type] }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'createOrgUnitparentLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="parent">
|
||||
<mat-option>{{ 'noParentOption' | translate }}</mat-option>
|
||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">{{ unit.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="description"></textarea>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
<!-- Paso 1: General -->
|
||||
<form [formGroup]="generalFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
<mat-option *ngFor="let type of filteredTypes" [value]="type">
|
||||
{{ typeTranslations[type] }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'createOrgUnitparentLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="parent">
|
||||
<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-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="description"></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<!-- Paso 2: Información del Aula -->
|
||||
<mat-step *ngIf="generalFormGroup.value.type === 'classroom'" [stepControl]="classroomInfoFormGroup">
|
||||
<form [formGroup]="classroomInfoFormGroup">
|
||||
<ng-template matStepLabel>{{ 'classroomInfoStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="location">
|
||||
</mat-form-field>
|
||||
<mat-slide-toggle formControlName="projector">{{ 'projectorToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board">{{ 'boardToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'capacityLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="capacity" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>{{ 'associatedCalendarLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
|
||||
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
|
||||
{{ calendar.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- Paso 2: Información del Aula -->
|
||||
<form *ngIf="generalFormGroup.value.type === 'classroom'" [formGroup]="classroomInfoFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="location">
|
||||
</mat-form-field>
|
||||
<mat-slide-toggle formControlName="projector">{{ 'projectorToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board">{{ 'boardToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'capacityLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="capacity" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>{{ 'associatedCalendarLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
|
||||
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
|
||||
{{ calendar.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
<!-- Paso 3: Información Adicional -->
|
||||
<form [formGroup]="additionalInfoFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="comments"></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<!-- Paso 3: Información Adicional -->
|
||||
<mat-step [stepControl]="additionalInfoFormGroup">
|
||||
<form [formGroup]="additionalInfoFormGroup">
|
||||
<ng-template matStepLabel>{{ 'additionalInfoStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="comments"></textarea>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
<!-- Paso 4: Configuración de Red -->
|
||||
<form *ngIf="generalFormGroup.value.type === 'classroom' || generalFormGroup.value.type === 'clients-group'" [formGroup]="networkSettingsFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="oglive" (selectionChange)="onOgLiveChange($event)">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.filename }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'nextServerLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="nextServer">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'bootFileNameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="bootFileName">
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Paso 4: Configuración de Red -->
|
||||
<mat-step *ngIf="generalFormGroup.value.type === 'classroom'" [stepControl]="networkSettingsFormGroup">
|
||||
<form [formGroup]="networkSettingsFormGroup">
|
||||
<ng-template matStepLabel>{{ 'networkSettingsStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="oglive" (selectionChange)="onOgLiveChange($event)">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.filename }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'nextServerLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="nextServer">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'bootFileNameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="bootFileName">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'proxyUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="proxy">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'dnsIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="dns">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'netmaskLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="netmask">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'routerLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="router">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ntpIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="ntp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'p2pModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'p2pTimeLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="p2pTime" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastIp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastSpeedLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastSpeed" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastPortLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastPort" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="mcastMode">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'menuUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="menu" type="url">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'proxyUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="proxy">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'dnsIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="dns">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'netmaskLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="netmask">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'routerLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="router">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ntpIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="ntp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'p2pModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'p2pTimeLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="p2pTime" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastIp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastSpeedLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastSpeed" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastPortLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastPort" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="mcastMode">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'menuUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="menu" type="url">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<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>
|
||||
|
|
|
@ -25,9 +25,9 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
'clients-group': 'Grupo de clientes'
|
||||
};
|
||||
protected p2pModeOptions = [
|
||||
{ name: 'Leecher', value: 'p2p-mode-leecher' },
|
||||
{ name: 'Peer', value: 'p2p-mode-peer' },
|
||||
{ name: 'Seeder', value: 'p2p-mode-seeder' },
|
||||
{ name: 'Leecher', value: 'leecher' },
|
||||
{ name: 'Peer', value: 'peer' },
|
||||
{ name: 'Seeder', value: 'seeder' },
|
||||
];
|
||||
protected multicastModeOptions = [
|
||||
{"name": 'Half duplex', "value": "half"},
|
||||
|
@ -39,9 +39,9 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
ogLives: any[] = [];
|
||||
repositories: any[] = [];
|
||||
selectedCalendarUuid: string | null = null;
|
||||
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||
|
||||
|
||||
@Output() unitAdded = new EventEmitter();
|
||||
@Output() unitAdded = new EventEmitter<{ uuid: string; name: string }>();
|
||||
|
||||
constructor(
|
||||
private _formBuilder: FormBuilder,
|
||||
|
@ -104,11 +104,23 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
loadParentUnits() {
|
||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
|
||||
this.http.get<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)
|
||||
);
|
||||
}
|
||||
|
||||
getSelectedParentName(): string | undefined {
|
||||
const parentId = this.generalFormGroup.get('parent')?.value;
|
||||
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
|
||||
}
|
||||
|
||||
loadHardwareProfiles(): void {
|
||||
this.dataService.getHardwareProfiles().subscribe(
|
||||
(data: any[]) => this.hardwareProfiles = data,
|
||||
|
@ -118,7 +130,9 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
|
||||
loadOgLives() {
|
||||
this.dataService.getOgLives().subscribe(
|
||||
(data: any[]) => this.ogLives = data,
|
||||
(data: any[]) => {
|
||||
this.ogLives = data
|
||||
},
|
||||
error => console.error('Error fetching ogLives', error)
|
||||
);
|
||||
}
|
||||
|
@ -160,10 +174,10 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
|
||||
this.http.post<any>(postUrl, formData, { headers }).subscribe(
|
||||
response => {
|
||||
this.unitAdded.emit();
|
||||
this.dialogRef.close();
|
||||
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
||||
this.openSnackBar(false, 'Cliente creado exitosamente');
|
||||
this.unitAdded.emit(response);
|
||||
this.dialogRef.close(response);
|
||||
this.toastService.success('Unidad creada exitosamente', 'Éxito');
|
||||
this.openSnackBar(false, 'Unidad creada exitosamente');
|
||||
},
|
||||
error => {
|
||||
console.error('Error al realizar POST:', error);
|
||||
|
@ -218,9 +232,9 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
|
||||
openSnackBar(isError: boolean, message: string) {
|
||||
if (isError) {
|
||||
this.toastService.error('Error al crear el cliente: ' + message, 'Error');
|
||||
this.toastService.error('Error al crear la unidad: ' + message, 'Error');
|
||||
} else {
|
||||
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
||||
this.toastService.success('Unidad creada exitosamente', 'Éxito');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,35 +5,34 @@ h1 {
|
|||
color: #3f51b5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.network-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
.mat-dialog-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.mat-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
text-transform: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
.mat-slide-toggle {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
|
@ -1,170 +1,148 @@
|
|||
<h1 mat-dialog-title>{{ 'editOrgUnitTitle' | translate }}</h1>
|
||||
<div mat-dialog-content>
|
||||
<mat-stepper orientation="vertical" [linear]="isLinear">
|
||||
<!-- Paso 1: General -->
|
||||
<mat-step [stepControl]="generalFormGroup">
|
||||
<form [formGroup]="generalFormGroup">
|
||||
<ng-template matStepLabel>{{ 'generalStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
<mat-option *ngFor="let type of filteredTypes" [value]="type">{{ type }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'editOrgUnitParentLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="parent">
|
||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">{{ unit.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="description"></textarea>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
<!-- Paso 1: General -->
|
||||
<form [formGroup]="generalFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
<mat-option *ngFor="let type of filteredTypes" [value]="type">
|
||||
{{ typeTranslations[type] }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'editOrgUnitParentLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="parent">
|
||||
<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-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="description"></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<!-- Paso 2: Información del Aula -->
|
||||
<mat-step *ngIf="generalFormGroup.value.type === 'classroom'" [stepControl]="classroomInfoFormGroup">
|
||||
<form [formGroup]="classroomInfoFormGroup">
|
||||
<ng-template matStepLabel>{{ 'classroomInfoStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="location">
|
||||
</mat-form-field>
|
||||
<mat-slide-toggle formControlName="projector">{{ 'projectorToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board">{{ 'boardToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'capacityLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="capacity" type="number">
|
||||
</mat-form-field>
|
||||
<!-- Paso 2: Información del Aula -->
|
||||
<form *ngIf="generalFormGroup.value.type === 'classroom'" [formGroup]="classroomInfoFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="location">
|
||||
</mat-form-field>
|
||||
<mat-slide-toggle formControlName="projector">{{ 'projectorToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board">{{ 'boardToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'capacityLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="capacity" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>{{ 'associatedCalendarLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
|
||||
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
|
||||
{{ calendar.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>{{ 'associatedCalendarLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
|
||||
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
|
||||
{{ calendar.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- Paso 3: Información Adicional -->
|
||||
<form [formGroup]="additionalInfoFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="comments"></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<!-- Paso 3: Información Adicional -->
|
||||
<mat-step [stepControl]="additionalInfoFormGroup">
|
||||
<form [formGroup]="additionalInfoFormGroup">
|
||||
<ng-template matStepLabel>{{ 'additionalInfoStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="comments"></textarea>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
||||
<button mat-button matStepperNext>{{ 'nextButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<!-- Paso 4: Configuración de Red -->
|
||||
<mat-step [stepControl]="networkSettingsFormGroup">
|
||||
<form [formGroup]="networkSettingsFormGroup">
|
||||
<ng-template matStepLabel>{{ 'networkSettingsStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.filename }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'proxyUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="proxy">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'dnsIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="dns">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'netmaskLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="netmask">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'routerLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="router">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ntpIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="ntp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'p2pModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">{{ option.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'p2pTimeLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="p2pTime" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastIp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastSpeedLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastSpeed" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastPortLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastPort" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="mcastMode">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">{{ option.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'menuUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="menu" type="url">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-slide-toggle formControlName="validation">{{ 'validationToggle' | translate }}</mat-slide-toggle>
|
||||
<div>
|
||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
<!-- Paso 4: Configuración de Red -->
|
||||
<form *ngIf="generalFormGroup.value.type === 'classroom' || generalFormGroup.value.type === 'clients-group'" [formGroup]="networkSettingsFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.filename }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'proxyUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="proxy">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'dnsIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="dns">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'netmaskLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="netmask">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'routerLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="router">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ntpIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="ntp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'p2pModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">{{ option.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'p2pTimeLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="p2pTime" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastIp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastSpeedLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastSpeed" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastPortLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastPort" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'mcastModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="mcastMode">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">{{ option.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'menuUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="menu" type="url">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-slide-toggle formControlName="validation">{{ 'validationToggle' | translate }}</mat-slide-toggle>
|
||||
</form>
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<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>
|
||||
|
|
|
@ -19,16 +19,23 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
networkSettingsFormGroup: FormGroup;
|
||||
classroomInfoFormGroup: FormGroup;
|
||||
types: string[] = ['organizational-unit', 'classrooms-group', 'classroom', 'clients-group'];
|
||||
typeTranslations: { [key: string]: string } = {
|
||||
'organizational-unit': 'Centro',
|
||||
'classrooms-group': 'Grupo de aulas',
|
||||
'classroom': 'Aula',
|
||||
'clients-group': 'Grupo de clientes'
|
||||
};
|
||||
parentUnits: any[] = [];
|
||||
hardwareProfiles: any[] = [];
|
||||
isEditMode: boolean;
|
||||
currentCalendar: any = [];
|
||||
ogLives: any[] = [];
|
||||
repositories: any[] = [];
|
||||
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||
protected p2pModeOptions = [
|
||||
{"name": 'Leecher', "value": "p2p-mode-leecher"},
|
||||
{"name": 'Peer', "value": "p2p-mode-peer"},
|
||||
{"name": 'Seeder', "value": "p2p-mode-seeder"},
|
||||
{"name": 'Leecher', "value": "leecher"},
|
||||
{"name": 'Peer', "value": "peer"},
|
||||
{"name": 'Seeder', "value": "seeder"},
|
||||
];
|
||||
protected multicastModeOptions = [
|
||||
{"name": 'Half duplex', "value": "half"},
|
||||
|
@ -103,18 +110,25 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadParentUnits() {
|
||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;
|
||||
|
||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.parentUnits = response['hydra:member'];
|
||||
this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
|
||||
id: unit['@id'],
|
||||
name: unit.name,
|
||||
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits)
|
||||
}));
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching parent units:', error);
|
||||
}
|
||||
error => console.error('Error fetching parent units:', error)
|
||||
);
|
||||
}
|
||||
|
||||
getSelectedParentName(): string | undefined {
|
||||
const parentId = this.generalFormGroup.get('parent')?.value;
|
||||
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
|
||||
}
|
||||
|
||||
loadHardwareProfiles(): void {
|
||||
this.dataService.getHardwareProfiles().subscribe(
|
||||
(data: any[]) => {
|
||||
|
@ -266,7 +280,7 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
},
|
||||
error => {
|
||||
console.error('Error al realizar POST:', error);
|
||||
this.toastService.error('Error al editar:', error);
|
||||
this.toastService.error('Error al editar:', error.error['hydra:description']);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 }}
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-checkbox
|
||||
formControlName="isGlobal"
|
||||
class="example-margin"
|
||||
>
|
||||
{{ 'globalImageLabel' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-divider *ngIf="imageId && partitionInfo"></mat-divider>
|
||||
|
||||
<div *ngIf="imageId && partitionInfo" class="partition-info-container">
|
||||
|
|
|
@ -30,6 +30,7 @@ export class CreateImageComponent implements OnInit {
|
|||
description: [''],
|
||||
comments: [''],
|
||||
remotePc: [false],
|
||||
isGlobal: [false],
|
||||
softwareProfile: [''],
|
||||
imageRepository: ['', Validators.required],
|
||||
});
|
||||
|
@ -51,6 +52,7 @@ export class CreateImageComponent implements OnInit {
|
|||
description: [response.description],
|
||||
comments: [response.comments],
|
||||
remotePc: [response.remotePc],
|
||||
isGlobal: [response.isGlobal],
|
||||
softwareProfile: [response.softwareProfile ? response.softwareProfile['@id'] : null, Validators.required],
|
||||
imageRepository: [response.imageRepository ? response.imageRepository['@id'] : null, Validators.required],
|
||||
});
|
||||
|
@ -90,16 +92,12 @@ export class CreateImageComponent implements OnInit {
|
|||
}
|
||||
|
||||
saveImage(): void {
|
||||
if (this.imageForm.invalid) {
|
||||
this.toastService.error('Por favor, rellena los campos obligatorios');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
name: this.imageForm.value.name,
|
||||
description: this.imageForm.value.description,
|
||||
comments: this.imageForm.value.comments,
|
||||
remotePc: this.imageForm.value.remotePc,
|
||||
isGlobal: this.imageForm.value.isGlobal,
|
||||
imageRepository: this.imageForm.value.imageRepository,
|
||||
...(this.imageForm.value.softwareProfile ? { softwareProfile: this.imageForm.value.softwareProfile } : {}),
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
.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()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</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">
|
||||
<button mat-flat-button color="primary" (click)="addImage()">
|
||||
{{ 'addImageButton' | translate }}
|
||||
|
@ -31,17 +35,23 @@
|
|||
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
|
||||
</mat-icon>
|
||||
</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'">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-failed': image.status === 'failed',
|
||||
'chip-success': image.status === 'success',
|
||||
'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]) }}
|
||||
</mat-chip>
|
||||
</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) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
|
@ -61,8 +71,9 @@
|
|||
<mat-menu #menu="matMenu">
|
||||
<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-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 !== 'success'" (click)="toggleAction(image, 'export')">Exportar imagen</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</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 { HttpClient } from '@angular/common/http';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { CreateImageComponent } from './create-image/create-image.component';
|
||||
import {CreateCommandComponent} from "../commands/main-commands/create-command/create-command.component";
|
||||
import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {ServerInfoDialogComponent} from "../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component";
|
||||
import {Observable} from "rxjs";
|
||||
import {InfoImageComponent} from "../ogboot/pxe-images/info-image/info-image/info-image.component";
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import {ExportImageComponent} from "./export-image/export-image.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-images',
|
||||
|
@ -48,6 +47,11 @@ export class ImagesComponent implements OnInit {
|
|||
header: 'Remote Pc',
|
||||
cell: (image: any) => `${image.remotePc}`
|
||||
},
|
||||
{
|
||||
columnDef: 'isGlobal',
|
||||
header: 'Imagen Global',
|
||||
cell: (image: any) => `${image.isGlobal}`
|
||||
},
|
||||
{
|
||||
columnDef: 'status',
|
||||
header: 'Estado',
|
||||
|
@ -67,16 +71,34 @@ export class ImagesComponent implements OnInit {
|
|||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
|
||||
private apiUrl = `${this.baseUrl}/images`;
|
||||
@Input() repositoryUuid: any
|
||||
private repositoryId: any;
|
||||
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
private joyrideService: JoyrideService
|
||||
private joyrideService: JoyrideService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.search();
|
||||
if (this.repositoryUuid) {
|
||||
this.loadRepository()
|
||||
} else {
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
loadRepository(): void {
|
||||
this.http.get<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 {
|
||||
|
@ -110,7 +132,7 @@ export class ImagesComponent implements OnInit {
|
|||
|
||||
search(): void {
|
||||
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 => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
|
@ -205,6 +227,17 @@ export class ImagesComponent implements OnInit {
|
|||
}
|
||||
});
|
||||
break;
|
||||
case 'delete-permanent':
|
||||
this.http.post(`${this.baseUrl}/images/server/${image.uuid}/delete-permanent`, {}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Petición de eliminación de la papelera temporal enviada');
|
||||
this.search()
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'recover':
|
||||
this.http.post(`${this.baseUrl}/images/server/${image.uuid}/recover`, {}).subscribe({
|
||||
next: () => {
|
||||
|
@ -216,6 +249,14 @@ export class ImagesComponent implements OnInit {
|
|||
}
|
||||
});
|
||||
break;
|
||||
case 'export':
|
||||
this.dialog.open(ExportImageComponent, {
|
||||
width: '600px',
|
||||
data: {
|
||||
image: image
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.error('Acción no soportada:', action);
|
||||
break;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.mat-tab-body-wrapper {
|
||||
min-height: inherit;
|
||||
}
|
||||
|
@ -210,13 +214,6 @@ p {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.status-led {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.status-led.active {
|
||||
background-color: green;
|
||||
|
@ -304,4 +301,85 @@ table {
|
|||
}
|
||||
|
||||
|
||||
.dashboard {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.top-row .card {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.status-led {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.cpu-usage-bar {
|
||||
background: lightgray;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cpu-bar {
|
||||
height: 100%;
|
||||
background: green;
|
||||
}
|
||||
|
||||
.cpu-bar.high {
|
||||
background: red;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.top-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-row .card {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,51 +1,91 @@
|
|||
<mat-tab-group dynamicHeight>
|
||||
<mat-tab-group class="main-container" dynamicHeight>
|
||||
<mat-tab label="Estado servidor">
|
||||
<div class="dashboard">
|
||||
<h2>OgRepository server Status</h2>
|
||||
<div class="disk-usage-container">
|
||||
<div class="disk-usage">
|
||||
<h3>Uso de disco</h3>
|
||||
<h2>OgRepository Server Status</h2>
|
||||
|
||||
<div class="row top-row">
|
||||
<div class="card">
|
||||
<h3>Uso de Disco</h3>
|
||||
<ngx-charts-pie-chart
|
||||
[view]="view"
|
||||
[scheme]="colorScheme"
|
||||
[results]="diskUsageChartData"
|
||||
[gradient]="gradient"
|
||||
[doughnut]="isDoughnut"
|
||||
[labels]="showLabels"
|
||||
[legend]="showLegend">
|
||||
[labels]="showLabels" >
|
||||
</ngx-charts-pie-chart>
|
||||
<div class="disk-usage-info">
|
||||
<div class="info">
|
||||
<p>Total: {{ diskUsage.total }}</p>
|
||||
<p>Ocupado: {{ diskUsage.used }}</p>
|
||||
<p>Disponible: {{ diskUsage.available }}</p>
|
||||
<p>Ocupado ( % ): {{ diskUsage.percentage }}</p>
|
||||
<p>Ocupado (%): {{ diskUsage.percentage }}</p>
|
||||
</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>
|
||||
<ul>
|
||||
<li *ngFor="let service of getServices()">
|
||||
<span
|
||||
class="status-led"
|
||||
[ngClass]="{
|
||||
'active': service.status === 'active',
|
||||
'inactive': service.status === 'stopped' || service.status === 'status not accesible'
|
||||
}"
|
||||
></span>
|
||||
<span class="status-led" [ngClass]="{
|
||||
'active': service.status === 'active',
|
||||
'inactive': service.status === 'stopped' || service.status === 'status not accesible'
|
||||
}"></span>
|
||||
{{ service.name }}:
|
||||
<span [ngSwitch]="service.status">
|
||||
<span *ngSwitchCase="'active'">Activo</span>
|
||||
<span *ngSwitchCase="'stopped'">Detenido</span>
|
||||
<span *ngSwitchCase="'status not accesible'">No accesible</span>
|
||||
<span *ngSwitchDefault>{{ service.status }}</span>
|
||||
</span>
|
||||
<span *ngSwitchCase="'active'">Activo</span>
|
||||
<span *ngSwitchCase="'stopped'">Detenido</span>
|
||||
<span *ngSwitchCase="'status not accesible'">No accesible</span>
|
||||
<span *ngSwitchDefault>{{ service.status }}</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
</mat-tab>
|
||||
|
||||
|
||||
<mat-tab label="Datos generales">
|
||||
<div class="dashboard">
|
||||
<div class="header-button-container">
|
||||
|
@ -83,59 +123,6 @@
|
|||
</mat-tab>
|
||||
|
||||
<mat-tab label="Listado de imágenes">
|
||||
<div class="dashboard">
|
||||
<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>
|
||||
<app-images [repositoryUuid]="repositoryId"></app-images>
|
||||
</mat-tab>
|
||||
</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 {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
@ -17,7 +17,7 @@ import {MatDialog} from "@angular/material/dialog";
|
|||
templateUrl: './main-repository-view.component.html',
|
||||
styleUrl: './main-repository-view.component.css'
|
||||
})
|
||||
export class MainRepositoryViewComponent {
|
||||
export class MainRepositoryViewComponent implements OnInit {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
repositoryForm: FormGroup<any>;
|
||||
repositoryId: string | null = null;
|
||||
|
@ -25,16 +25,20 @@ export class MainRepositoryViewComponent {
|
|||
loading: boolean = true;
|
||||
diskUsage: any = {};
|
||||
servicesStatus: any = {};
|
||||
processesStatus: any = {};
|
||||
diskUsageChartData: any[] = [];
|
||||
ramUsageChartData: any[] = [];
|
||||
ramUsage: any = {};
|
||||
cpuUsage: any = {};
|
||||
alertMessage: string | null = null;
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 10;
|
||||
page: number = 0;
|
||||
view: [number, number] = [800, 500];
|
||||
gradient: boolean = true;
|
||||
showLegend: boolean = true;
|
||||
showLabels: boolean = true;
|
||||
isDoughnut: boolean = true;
|
||||
status: boolean = false;
|
||||
repositoryData: any = {};
|
||||
colorScheme: any = {
|
||||
domain: ['#FF6384', '#3f51b5']
|
||||
|
@ -115,7 +119,6 @@ export class MainRepositoryViewComponent {
|
|||
comments: [response.comments],
|
||||
});
|
||||
this.loading = false;
|
||||
// Llamar searchImages() solo cuando la data de repository esté cargada
|
||||
this.searchImages();
|
||||
},
|
||||
(error) => {
|
||||
|
@ -157,28 +160,33 @@ export class MainRepositoryViewComponent {
|
|||
|
||||
loadStatus(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/image-repositories/server/${this.repositoryId}/status`).subscribe(data => {
|
||||
const diskData = data.output.disk;
|
||||
const servicesData = data.output.services;
|
||||
if (!data.success) {
|
||||
console.error('Error: No se pudo obtener los datos del servidor');
|
||||
this.status = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.diskUsage = {
|
||||
total: diskData.total,
|
||||
used: diskData.used,
|
||||
available: diskData.available,
|
||||
percentage: diskData.used_percentage
|
||||
};
|
||||
this.status = true;
|
||||
|
||||
const { disk, services, ram, cpu, processes } = data.output;
|
||||
|
||||
this.diskUsage = { ...disk };
|
||||
this.diskUsageChartData = [
|
||||
{
|
||||
name: 'Usado',
|
||||
value: parseFloat(diskData.used)
|
||||
},
|
||||
{
|
||||
name: 'Disponible',
|
||||
value: parseFloat(diskData.available)
|
||||
}
|
||||
{ name: 'Usado', value: parseFloat(disk.used.replace('GB', '')) },
|
||||
{ name: 'Disponible', value: parseFloat(disk.available.replace('GB', '')) }
|
||||
];
|
||||
|
||||
this.servicesStatus = servicesData;
|
||||
this.ramUsage = { ...ram };
|
||||
this.ramUsageChartData = [
|
||||
{ name: 'Usado', value: parseFloat(ram.used.replace('GB', '')) },
|
||||
{ name: 'Disponible', value: parseFloat(ram.available.replace('GB', '')) }
|
||||
];
|
||||
|
||||
this.cpuUsage = { percentage: cpu.used_percentage };
|
||||
|
||||
this.servicesStatus = Object.entries(services).map(([name, status]) => ({ name, status }));
|
||||
|
||||
this.processesStatus = Object.entries(processes).map(([name, status]) => ({ name, status }));
|
||||
|
||||
}, error => {
|
||||
console.error('Error fetching status', error);
|
||||
|
@ -186,10 +194,17 @@ export class MainRepositoryViewComponent {
|
|||
}
|
||||
|
||||
getServices(): { name: string, status: string }[] {
|
||||
return Object.keys(this.servicesStatus).map(key => ({
|
||||
name: key,
|
||||
status: this.servicesStatus[key]
|
||||
}));
|
||||
if (!this.status) {
|
||||
return [];
|
||||
}
|
||||
return this.servicesStatus ? this.servicesStatus : [];
|
||||
}
|
||||
|
||||
getProcesses(): { name: string, status: string }[] {
|
||||
if (!this.status) {
|
||||
return [];
|
||||
}
|
||||
return this.processesStatus ? this.processesStatus : [];
|
||||
}
|
||||
|
||||
searchImages(): void {
|
||||
|
@ -207,64 +222,6 @@ export class MainRepositoryViewComponent {
|
|||
);
|
||||
}
|
||||
|
||||
editImage(event: MouseEvent, image: any): void {
|
||||
event.stopPropagation();
|
||||
this.dialog.open(CreateImageComponent, {
|
||||
width: '800px',
|
||||
data: image['@id']
|
||||
}).afterClosed().subscribe(() => this.searchImages());
|
||||
}
|
||||
|
||||
deleteImage(image: any): void {
|
||||
this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: image.name },
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.http.delete(`${this.apiUrl}/server/${image.uuid}/delete`).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Imagen eliminada con éxito');
|
||||
this.searchImages();
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
console.error('Error al eliminar la imagen:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadImageAlert(image: any): Observable<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> {
|
||||
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() {
|
||||
this.loadAlert().subscribe(
|
||||
response => {
|
||||
|
|
|
@ -100,3 +100,10 @@ table {
|
|||
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()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</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">
|
||||
<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>
|
||||
|
@ -11,14 +15,20 @@
|
|||
|
||||
<div class="search-container">
|
||||
<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">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
|
||||
</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>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<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 repository" >
|
||||
|
@ -30,9 +40,10 @@
|
|||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<td mat-cell *matCellDef="let client" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="editRepository($event, client)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" (click)="deleteRepository($event, client)">
|
||||
<td mat-cell *matCellDef="let repository" style="text-align: center;">
|
||||
<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="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>
|
||||
</button>
|
||||
</td>
|
||||
|
|
|
@ -8,6 +8,7 @@ import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delet
|
|||
import { JoyrideService } from 'ngx-joyride';
|
||||
import {CreateRepositoryComponent} from "./create-repository/create-repository.component";
|
||||
import { Router } from '@angular/router';
|
||||
import {ImportImageComponent} from "./import-image/import-image.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-repositories',
|
||||
|
@ -91,6 +92,16 @@ export class RepositoriesComponent {
|
|||
this.router.navigate(['repository', repository.uuid]);
|
||||
}
|
||||
|
||||
importImage(event: MouseEvent, repository: any): void {
|
||||
event.stopPropagation();
|
||||
this.dialog.open(ImportImageComponent, {
|
||||
width: '600px',
|
||||
data: { repository }
|
||||
}).afterClosed().subscribe(() => {
|
||||
this.search();
|
||||
});
|
||||
}
|
||||
|
||||
deleteRepository(event: MouseEvent,command: any): void {
|
||||
event.stopPropagation();
|
||||
this.dialog.open(DeleteModalComponent, {
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: calc(100vh - 10vh);
|
||||
height: calc(100vh - 10vh);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
width: 250px;
|
||||
z-index: auto !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
margin: 0px 10px 10px 10px;
|
||||
padding: 0px 10px 10px 10px;
|
||||
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)",
|
||||
"newOrganizationalUnitButton": "New Organizational Unit",
|
||||
"newClientButton": "New Client",
|
||||
"newSingleClientButton": "Add single client",
|
||||
"newMultipleClientButton": "Add numerous clients",
|
||||
"keyStepText": "The legend will show you the types of organizational units and their corresponding icons",
|
||||
"legendButton": "Legend",
|
||||
"unitStepText": "This is the section where 'Center' type organizational units will be displayed",
|
||||
|
@ -215,7 +217,8 @@
|
|||
"hardwareProfileLabel": "Hardware Profile",
|
||||
"urlFormatError": "Invalid URL format.",
|
||||
"validationToggle": "Validation",
|
||||
"submitButton": "Add",
|
||||
"addOUSubmitButton": "Add",
|
||||
"editOUSubmitButton": "Edit",
|
||||
"addOrgUnitTitle": "Add Organizational Unit",
|
||||
"createOrgUnitparentLabel": "Parent organizational unit",
|
||||
"noParentOption": "--",
|
||||
|
@ -266,11 +269,13 @@
|
|||
"diskUsedLabel": "Used",
|
||||
"diskTotalLabel": "Total",
|
||||
"diskImageAssistantTitle": "Disk image assistant",
|
||||
"deployImage": "Deploy image",
|
||||
"partitionColumn": "Partition",
|
||||
"isoImageColumn": "ISO Image",
|
||||
"ogliveColumn": "OgLive",
|
||||
"selectImageOption": "Select image",
|
||||
"selectOgLiveOption": "Select OgLive",
|
||||
"repositoryTitle": "Admin Repository",
|
||||
"saveAssociationsButton": "Save Associations",
|
||||
"partitionAssistantTitle": "Partition assistant",
|
||||
"diskSizeLabel": "Size",
|
||||
|
@ -278,6 +283,8 @@
|
|||
"partitionSizeColumn": "Size (MB)",
|
||||
"usageColumn": "Usage (%)",
|
||||
"formatColumn": "Format",
|
||||
"remotePcLabel": "Remote PC",
|
||||
"globalImageLabel": "Global image",
|
||||
"ntfsOption": "NTFS",
|
||||
"linuxOption": "LINUX",
|
||||
"cacheOption": "CACHE",
|
||||
|
@ -428,7 +435,7 @@
|
|||
"addClientMenu": "Add client",
|
||||
"filters": "Filters",
|
||||
"searchClient": "Search client",
|
||||
"searchTree": "Search in tree",
|
||||
"searchTree": "Search organizational unit",
|
||||
"filterByType": "Filter by type",
|
||||
"all": "All",
|
||||
"classroomsGroup": "Classroom groups",
|
||||
|
|
|
@ -157,6 +157,8 @@
|
|||
"newOrganizationalUnitTooltip": "Abrir modal para crear unidades organizativas de cualquier tipo (Centro, Aula, Grupo de aulas o Grupo de clientes)",
|
||||
"newOrganizationalUnitButton": "Nueva Unidad Organizativa",
|
||||
"newClientButton": "Nuevo Cliente",
|
||||
"newSingleClientButton": "Añadir cliente unitario",
|
||||
"newMultipleClientButton": "Añadir clientes masivamente",
|
||||
"keyStepText": "La leyenda te mostrará los tipos de unidades organizativas y sus iconos correspondientes",
|
||||
"legendButton": "Leyenda",
|
||||
"unitStepText": "Esta es la sección donde se mostrarán las unidades organizativas de tipo 'Centro'",
|
||||
|
@ -215,7 +217,8 @@
|
|||
"hardwareProfileLabel": "Perfil de Hardware",
|
||||
"urlFormatError": "Formato de URL inválido.",
|
||||
"validationToggle": "Validación",
|
||||
"submitButton": "Añadir",
|
||||
"addOUSubmitButton": "Añadir",
|
||||
"editOUSubmitButton": "Editar",
|
||||
"addOrgUnitTitle": "Añadir Unidad Organizativa",
|
||||
"createOrgUnitparentLabel": "Unidad organizativa padre",
|
||||
"noParentOption": "--",
|
||||
|
@ -271,6 +274,7 @@
|
|||
"isoImageColumn": "Imagen ISO",
|
||||
"ogliveColumn": "OgLive",
|
||||
"selectImageOption": "Seleccionar imagen",
|
||||
"deployImage": "Desplegar imagen",
|
||||
"selectOgLiveOption": "Seleccionar OgLive",
|
||||
"saveAssociationsButton": "Guardar Asociaciones",
|
||||
"partitionAssistantTitle": "Asistente de particionado",
|
||||
|
@ -296,6 +300,9 @@
|
|||
"internalUnits": "Unidades internas",
|
||||
"noResultsMessage": "No hay resultados para mostrar.",
|
||||
"imagesTitle": "Administrar imágenes",
|
||||
"repositoryTitle": "Administrar repositorios",
|
||||
"remotePcLabel": "Remote PC",
|
||||
"globalImageLabel": "Imagen Global",
|
||||
"addImageButton": "Añadir imagen",
|
||||
"searchNameDescription": "Busca imágenes por nombre para encontrar rápidamente una imagen específica.",
|
||||
"searchDefaultDescription": "Filtra las imágenes para mostrar solo las imágenes por defecto o no por defecto.",
|
||||
|
@ -430,7 +437,7 @@
|
|||
"addClientMenu": "Añadir cliente",
|
||||
"filters": "Filtros",
|
||||
"searchClient": "Buscar cliente",
|
||||
"searchTree": "Buscar en árbol",
|
||||
"searchTree": "Buscar unidad organizativa",
|
||||
"filterByType": "Filtrar por tipo",
|
||||
"all": "Todos",
|
||||
"classroomsGroup": "Grupos de aulas",
|
||||
|
|
Loading…
Reference in New Issue