feat: standardise and refactor OU selection and migrate control flow
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/tag This commit looks good Details

main 1.14.0
Juan Manuel Bardallo 2026-10-05 09:26:55 +02:00
parent bdfe4ac0df
commit 609304acc0
24 changed files with 791 additions and 665 deletions

View File

@ -1,5 +1,15 @@
# Changelog
## [1.14.0] - 2026-10-05
### Added
- **UI/UX**: Añadido buscador en todos los desplegables de selección de Unidad Organizativa gracias al nuevo componente reutilizable `OuSelectComponent`.
### Changed
- **UI/UX**: Estandarizada la selección de Unidades Organizativas en toda la consola para mostrar de forma consistente la ruta jerárquica (breadcrumb) del elemento.
- **Refactor**: Extraída la lógica de selección de Unidades Organizativas a un componente genérico (`OuSelectComponent`) para cumplir con el principio DRY.
- **Refactor**: Migrada la sintaxis de plantillas (directivas estructurales `*ngIf`, `*ngFor`, `*ngSwitch`) a la nueva sintaxis de control de flujo de Angular 18 (`@if`, `@for`, `@switch`) en los componentes afectados por la refactorización.
## [1.13.0] - 2026-10-02
### Added

View File

@ -1,12 +1,12 @@
{
"name": "og-webconsole",
"version": "0.5.0",
"version": "1.14.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "og-webconsole",
"version": "0.5.0",
"version": "1.14.0",
"dependencies": {
"@angular/animations": "^18.0.0",
"@angular/cdk": "~18.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "og-webconsole",
"version": "0.5.0",
"version": "1.14.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
@ -54,4 +54,3 @@
"typescript": "~5.4.5"
}
}

View File

@ -210,6 +210,7 @@ export function HttpLoaderFactory(http: HttpClient) {
import { ImportLegacyComponent } from './components/admin/import-legacy/import-legacy.component';
import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component';
import { OuSelectComponent } from './components/shared/ou-select/ou-select.component';
export function initializeApp(configService: ConfigService) {
return () => configService.loadConfig();
@ -351,7 +352,8 @@ registerLocaleData(localeEs, 'es-ES');
OgdhcpStatusComponent,
StatusLogsComponent,
ImportLegacyComponent,
PageNotFoundComponent
PageNotFoundComponent,
OuSelectComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,

View File

@ -23,16 +23,13 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'labelOrganizationalUnit' | translate }}</mat-label>
<mat-select multiple formControlName="organizationalUnits">
@for (unit of organizationalUnits; track unit['@id']) {
<mat-option [value]="unit['@id']">
{{ unit.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<app-ou-select
class="full-width"
formControlName="organizationalUnits"
[multiple]="true"
[units]="organizationalUnits"
[label]="'labelOrganizationalUnit'">
</app-ou-select>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'cardView' | translate }}</mat-label>

View File

@ -7,6 +7,7 @@ import { ToastrService } from "ngx-toastr";
import { HttpClient } from "@angular/common/http";
import { DataService } from "../data.service";
import { ConfigService } from '@services/config.service';
import { DataService as GroupsDataService } from '../../../../groups/services/data.service';
interface UserGroup {
'@id': string;
@ -44,6 +45,7 @@ export class AddUserModalComponent implements OnInit {
private http: HttpClient,
private fb: FormBuilder,
private dataService: DataService,
private groupsDataService: GroupsDataService,
private toastService: ToastrService,
private configService: ConfigService
) {
@ -71,7 +73,13 @@ export class AddUserModalComponent implements OnInit {
});
this.dataService.getOrganizationalUnits().subscribe((data) => {
this.organizationalUnits = data['hydra:member'].filter((item: any) => item.type === 'organizational-unit');
const allUnits = data['hydra:member'];
this.organizationalUnits = allUnits
.filter((item: any) => item.type === 'organizational-unit')
.map((unit: any) => ({
...unit,
path: this.groupsDataService.getOrganizationalUnitPath(unit, allUnits)
}));
});
if (this.data) {

View File

@ -3,116 +3,127 @@
</h2>
<mat-dialog-content class="dialog-content">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
@if (loading) {
<mat-spinner class="loading-spinner"></mat-spinner>
}
<div class="task-form" *ngIf="!loading">
<mat-button-toggle-group *ngIf="data?.source === 'assistant'" [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode" appearance="standard">
<mat-button-toggle value="create">
<mat-icon>add_task</mat-icon>
{{ 'labelCreateTask' | translate }}
</mat-button-toggle>
<mat-button-toggle value="add">
<mat-icon>playlist_add</mat-icon>
{{ 'addToExistingTask' | translate }}
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<div *ngIf="taskMode === 'add'" class="select-task">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'selectTask' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedExistingTask" name="existingTask">
<mat-option *ngFor="let task of existingTasks" [value]="task">{{ task.name }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'executionOrder' | translate }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="executionOrder"
name="executionOrder"
min="1"
[placeholder]="'enterOrder' | translate"
>
</mat-form-field>
</div>
<form *ngIf="taskMode === 'create' && taskForm && !loading" [formGroup]="taskForm" class="task-form">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
<input matInput formControlName="name" [placeholder]="'nameLabel' | translate">
<mat-error *ngIf="taskForm.get('name')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'labelNotes' | translate }}</mat-label>
<textarea matInput formControlName="notes" [placeholder]="'notesPlaceholder' | translate"></textarea>
</mat-form-field>
<mat-checkbox *ngIf="!editing" formControlName="scheduleAfterCreate" class="schedule-checkbox">
{{ 'scheduleAfterCreate' | translate }}
</mat-checkbox>
<mat-divider style="margin: 15px 0;"></mat-divider>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'labelScope' | translate }}</mat-label>
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)"
[disabled]="data?.clients && data.clients.length >= 1">
<mat-option value="organizational-unit">{{ 'organizationalUnit' | translate }}</mat-option>
<mat-option value="classrooms-group">{{ 'scopeClassroomsGroup' | translate }}</mat-option>
<mat-option value="classroom">{{ 'classrooms' | translate }}</mat-option>
<mat-option value="clients-group">{{ 'clientGroupsTitle' | translate }}</mat-option>
<mat-option value="clients">{{ 'labelClients' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="taskForm.get('scope')?.value !== 'clients'" appearance="fill" class="full-width">
<mat-label>{{ 'labelOrganizationalUnit' | translate }}</mat-label>
<mat-select formControlName="organizationalUnit">
<mat-option *ngFor="let unit of availableOrganizationalUnits" [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>
<div *ngIf="taskForm.get('scope')?.value === 'clients'" class="clients-selection">
<h4>{{ 'selectedClients' | translate }}</h4>
<div *ngIf="data?.selectedClients && data.selectedClients.length > 0" class="pre-selected-info">
<mat-icon>info</mat-icon>
<span>{{ 'preSelectedInfo' | translate }}</span>
</div>
<div class="clients-list">
<div class="clients-grid">
<mat-card
*ngFor="let client of clients"
class="client-card"
[class.pre-selected]="isClientPreSelected(client)"
>
<mat-card-content>
<div class="client-info">
<div class="client-name">{{ client.name || client.hostname }}</div>
<div class="client-details">
<span class="client-ip">{{ client.ip }}</span>
<span class="client-status" [class]="'status-' + client.status">
{{ client.status }}
</span>
</div>
</div>
<mat-icon class="selected-icon">
check_circle
</mat-icon>
</mat-card-content>
</mat-card>
</div>
</div>
@if (!loading) {
<div class="task-form">
@if (data?.source === 'assistant') {
<mat-button-toggle-group [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode" appearance="standard">
<mat-button-toggle value="create">
<mat-icon>add_task</mat-icon>
{{ 'labelCreateTask' | translate }}
</mat-button-toggle>
<mat-button-toggle value="add">
<mat-icon>playlist_add</mat-icon>
{{ 'addToExistingTask' | translate }}
</mat-button-toggle>
</mat-button-toggle-group>
}
</div>
</form>
}
@if (taskMode === 'add') {
<div class="select-task">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'selectTask' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedExistingTask" name="existingTask">
@for (task of existingTasks; track task) {
<mat-option [value]="task">{{ task.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'executionOrder' | translate }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="executionOrder"
name="executionOrder"
min="1"
[placeholder]="'enterOrder' | translate"
>
</mat-form-field>
</div>
}
@if (taskMode === 'create' && taskForm && !loading) {
<form [formGroup]="taskForm" class="task-form">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
<input matInput formControlName="name" [placeholder]="'nameLabel' | translate">
@if (taskForm.get('name')?.invalid) {
<mat-error>{{ 'requiredFieldError' | translate }}</mat-error>
}
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'labelNotes' | translate }}</mat-label>
<textarea matInput formControlName="notes" [placeholder]="'notesPlaceholder' | translate"></textarea>
</mat-form-field>
@if (!editing) {
<mat-checkbox formControlName="scheduleAfterCreate" class="schedule-checkbox">
{{ 'scheduleAfterCreate' | translate }}
</mat-checkbox>
}
<mat-divider style="margin: 15px 0;"></mat-divider>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'labelScope' | translate }}</mat-label>
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)"
[disabled]="data?.clients && data.clients.length >= 1">
<mat-option value="organizational-unit">{{ 'organizationalUnit' | translate }}</mat-option>
<mat-option value="classrooms-group">{{ 'scopeClassroomsGroup' | translate }}</mat-option>
<mat-option value="classroom">{{ 'classrooms' | translate }}</mat-option>
<mat-option value="clients-group">{{ 'clientGroupsTitle' | translate }}</mat-option>
<mat-option value="clients">{{ 'labelClients' | translate }}</mat-option>
</mat-select>
</mat-form-field>
@if (taskForm.get('scope')?.value !== 'clients') {
<app-ou-select
class="full-width"
formControlName="organizationalUnit"
[units]="availableOrganizationalUnits"
[label]="'labelOrganizationalUnit'">
</app-ou-select>
}
@if (taskForm.get('scope')?.value === 'clients') {
<div class="clients-selection">
<h4>{{ 'selectedClients' | translate }}</h4>
@if (data?.selectedClients && data.selectedClients.length > 0) {
<div class="pre-selected-info">
<mat-icon>info</mat-icon>
<span>{{ 'preSelectedInfo' | translate }}</span>
</div>
}
<div class="clients-list">
<div class="clients-grid">
@for (client of clients; track client) {
<mat-card
class="client-card"
[class.pre-selected]="isClientPreSelected(client)"
>
<mat-card-content>
<div class="client-info">
<div class="client-name">{{ client.name || client.hostname }}</div>
<div class="client-details">
<span class="client-ip">{{ client.ip }}</span>
<span class="client-status" [class]="'status-' + client.status">
{{ client.status }}
</span>
</div>
</div>
<mat-icon class="selected-icon">
check_circle
</mat-icon>
</mat-card-content>
</mat-card>
}
</div>
</div>
</div>
}
</form>
}
</mat-dialog-content>
<mat-dialog-actions class="action-container">
@ -120,7 +131,7 @@
<button
class="submit-button"
(click)="taskMode === 'create' ? saveTask() : addToExistingTask()"
>
>
{{ 'labelSaveButton' | translate }}
</button>
</mat-dialog-actions>

View File

@ -10,6 +10,7 @@ import {of} from "rxjs";
import {startWith, switchMap} from "rxjs/operators";
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
import {ShowTaskScriptComponent} from "../show-task-script/show-task-script.component";
import { DataService as GroupsDataService } from "../../../groups/services/data.service";
@Component({
selector: 'app-create-task',
@ -42,7 +43,8 @@ export class CreateTaskComponent implements OnInit {
private toastr: ToastrService,
private dialog: MatDialog,
public dialogRef: MatDialogRef<CreateTaskComponent>,
@Inject(MAT_DIALOG_DATA) public data: any
@Inject(MAT_DIALOG_DATA) public data: any,
private dataService: GroupsDataService
) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/command-tasks`;
@ -214,7 +216,10 @@ export class CreateTaskComponent implements OnInit {
return new Promise((resolve, reject) => {
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=100`).subscribe(
(data) => {
this.allOrganizationalUnits = data['hydra:member'];
this.allOrganizationalUnits = data['hydra:member'].map((unit: any) => ({
...unit,
path: this.dataService.getOrganizationalUnitPath(unit, data['hydra:member'])
}));
this.availableOrganizationalUnits = [...this.allOrganizationalUnits];
resolve();
},

View File

@ -1,12 +1,12 @@
<h2 mat-dialog-title>{{ 'moveClientsTo' | translate }}</h2>
<mat-dialog-content>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'selectClassroomPrompt' | translate }}</mat-label>
<mat-select [(ngModel)]="newOU">
<mat-option *ngFor="let unit of units" [value]="unit">{{ unit.name }}</mat-option>
</mat-select>
</mat-form-field>
<app-ou-select
class="full-width"
[(ngModel)]="newOU"
[units]="units"
[label]="'selectClassroomPrompt'">
</app-ou-select>
</mat-dialog-content>
<div class="action-container">

View File

@ -6,6 +6,7 @@ import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ToastrService} from "ngx-toastr";
import {Router} from "@angular/router";
import {ConfigService} from "@services/config.service";
import {DataService} from "../../services/data.service";
@Component({
selector: 'app-change-parent',
@ -25,6 +26,7 @@ export class ChangeParentComponent implements OnInit {
private toastService: ToastrService,
private router: Router,
private configService: ConfigService,
private dataService: DataService,
@Inject(MAT_DIALOG_DATA) public data: { clients: any}
) {
this.baseUrl = this.configService.apiUrl;
@ -38,7 +40,11 @@ export class ChangeParentComponent implements OnInit {
loadUnits() {
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=500`).subscribe(
response => {
this.units = response['hydra:member'];
const allUnits = response['hydra:member'];
this.units = allUnits.map((unit: any) => ({
...unit,
path: this.dataService.getOrganizationalUnitPath(unit, allUnits)
}));
this.loading = false;
},
error => console.error(this.translate.instant('errorFetchingOrganizationalUnits1'), error)
@ -48,7 +54,7 @@ export class ChangeParentComponent implements OnInit {
save() {
this.http.post<any>(`${this.baseUrl}/clients/change-organizational-unit`, {
clients: this.data.clients.map((client: any) => client['@id']),
organizationalUnit: this.newOU['@id']
organizationalUnit: this.newOU
}).subscribe({
next: (response) => {
this.toastService.success(this.translate.instant('parentChangedSuccessfully'));

View File

@ -3,57 +3,59 @@
<div class="inputs-container">
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
<form class="client-form grid-form" *ngIf="!loading">
<mat-form-field class="form-field">
<mat-label>{{ 'labelOrganizationalUnit' | translate }}</mat-label>
<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>
</form>
@if (!loading) {
<form class="client-form grid-form">
<app-ou-select
class="form-field"
[ngModel]="organizationalUnit"
[units]="parentUnitsWithPaths"
[label]="'labelOrganizationalUnit'"
(selectionChange)="setOrganizationalUnit($event)">
</app-ou-select>
</form>
}
<div>
<div class="upload-container">
<button class="action-button" (click)="fileInput.click()">{{ 'buttonUploadFile' | translate }}</button>
<input #fileInput type="file" (change)="onFileUpload($event)" accept="*" hidden>
<p>{{ 'orAddThemManually' | translate }}</p>
<div *ngIf="showTextarea">
<textarea #textarea matInput
[placeholder]="'dhcpHostExample' | translate"
@if (showTextarea) {
<div>
<textarea #textarea matInput
[placeholder]="'dhcpHostExample' | translate"
rows="20" cols="100"></textarea>
<button class="action-button" style="margin-top: 0.5em;"
<button class="action-button" style="margin-top: 0.5em;"
(click)="onTextarea(textarea.value)">{{ 'buttonPreview' | translate }}</button>
</div>
}
</div>
@if (uploadedClients.length > 0) {
<h4>{{ 'importedClientsZero' | translate }}</h4>
}
@if (uploadedClients.length > 0) {
<div class="scrollable-table">
@if (uploadedClients.length > 0) {
<table mat-table [dataSource]="uploadedClients" class="mat-elevation-z8">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'labelRoleName' | translate }}</th>
<td mat-cell *matCellDef="let client"> {{ client.name }} </td>
</ng-container>
<ng-container matColumnDef="ip">
<th mat-header-cell *matHeaderCellDef> {{ 'labelIpCaps' | translate }}</th>
<td mat-cell *matCellDef="let client"> {{ client.ip }} </td>
</ng-container>
<ng-container matColumnDef="mac">
<th mat-header-cell *matHeaderCellDef> {{ 'labelMac' | translate }}</th>
<td mat-cell *matCellDef="let client"> {{ client.mac }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
}
</div>
</div>
<h4 *ngIf="uploadedClients.length > 0">{{ 'importedClientsZero' | translate }}</h4>
<div *ngIf="uploadedClients.length > 0" class="scrollable-table">
<table mat-table [dataSource]="uploadedClients" class="mat-elevation-z8" *ngIf="uploadedClients.length > 0">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'labelRoleName' | translate }}</th>
<td mat-cell *matCellDef="let client"> {{ client.name }} </td>
</ng-container>
<ng-container matColumnDef="ip">
<th mat-header-cell *matHeaderCellDef> {{ 'labelIpCaps' | translate }}</th>
<td mat-cell *matCellDef="let client"> {{ client.ip }} </td>
</ng-container>
<ng-container matColumnDef="mac">
<th mat-header-cell *matHeaderCellDef> {{ 'labelMac' | translate }}</th>
<td mat-cell *matCellDef="let client"> {{ client.mac }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
</div>
</div>
</div>

View File

@ -65,10 +65,7 @@ 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 {
this.organizationalUnit = organizationalUnit.value;

View File

@ -1,175 +1,155 @@
<mat-dialog-content class="create-client-container">
<h1 mat-dialog-title>{{ dialogTitle | translate }}</h1>
<div [ngClass]="{'loading': loading}">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<form *ngIf="clientForm && !loading" [formGroup]="clientForm" class="client-form grid-form">
<mat-form-field class="form-field">
<mat-label>{{ 'labelOrganizationalUnit' | translate }}</mat-label>
<mat-select formControlName="organizationalUnit" (selectionChange)="onParentChange($event)">
<mat-select-trigger>
{{ getSelectedParentName() }}
</mat-select-trigger>
@for (unit of parentUnitsWithPaths; track unit.id) {
<mat-option [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>
<mat-form-field class="form-field">
<mat-label>{{ 'labelName' | translate }}</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelSerialNumber' | translate }}</mat-label>
<input matInput formControlName="serialNumber">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelNetiface' | translate }}</mat-label>
<mat-select formControlName="netiface">
@for (type of netifaceTypes; track type.value) {
<mat-option [value]="type.value">{{ type.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelMac' | translate }}</mat-label>
<input matInput formControlName="mac" required>
<mat-error>{{ 'macError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelIp' | translate }}</mat-label>
<input matInput formControlName="ip" required>
<mat-error>{{ 'ipError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelRepository' | translate }}</mat-label>
<mat-select formControlName="repository">
@for (repository of repositories; track repository['@id']) {
<mat-option [value]="repository['@id']">{{ repository.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-checkbox formControlName="maintenance">
{{ 'maintenance' | translate }}
</mat-checkbox>
<!-- ===== Agrupación: Configuración del menú ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'menuConfiguration' | translate }}</legend>
<div class="fieldset-grid-2">
<mat-form-field class="form-field">
<mat-label>{{ 'labelMenu' | translate }}</mat-label>
<mat-select formControlName="menu">
@for (menu of menus; track menu['@id']) {
<mat-option [value]="menu['@id']">{{ menu.name }}</mat-option>
}
</mat-select>
<mat-error>{{ 'menuError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelResolution' | translate }}</mat-label>
<mat-select formControlName="resolution" required>
<mat-option value="769">{{ 'resolution640x480x8' | translate }}</mat-option>
<mat-option value="771">{{ 'resolution800x600x8' | translate }}</mat-option>
<mat-option value="773">{{ 'resolution1024x768x8' | translate }}</mat-option>
<mat-option value="775">{{ 'resolution1280x1024x8' | translate }}</mat-option>
<mat-option value="784">{{ 'resolution640x480x16' | translate }}</mat-option>
<mat-option value="787">{{ 'resolution800x600x16' | translate }}</mat-option>
<mat-option value="791">{{ 'resolution1024x768x16' | translate }}</mat-option>
<mat-option value="794">{{ 'resolution1280x1024x16' | translate }}</mat-option>
<mat-option value="785">{{ 'resolution640x480x24' | translate }}</mat-option>
<mat-option value="788">{{ 'resolution800x600x24' | translate }}</mat-option>
<mat-option value="792">{{ 'resolution1024x768x24' | translate }}</mat-option>
<mat-option value="795">{{ 'resolution1280x1024x24' | translate }}</mat-option>
</mat-select>
<mat-error>{{ 'resolutionError' | translate }}</mat-error>
</mat-form-field>
</div>
</fieldset>
<!-- ===== Agrupación: Configuración de arranque ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'bootConfiguration' | translate }}</legend>
<div class="fieldset-grid-3">
<mat-form-field class="form-field">
<mat-label>{{ 'bootServer' | translate }}</mat-label>
<mat-select formControlName="bootServer" (selectionChange)="onBootServerChange($event)">
@for (server of bootServers; track server['@id']) {
<mat-option [value]="server['@id']">{{ server.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelOgLive' | translate }}</mat-label>
<mat-select formControlName="ogLive">
@for (ogLive of ogLives; track ogLive['@id']) {
<mat-option [value]="ogLive['@id']">{{ ogLive.name }}</mat-option>
}
@if (ogLives.length === 0 && clientForm.get('bootServer')?.value) {
<mat-option disabled>{{ 'noOgLivesInServer' | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelTemplate' | translate }}</mat-label>
<mat-select formControlName="pxeTemplate">
@for (template of templates; track template['@id']) {
<mat-option [value]="template['@id']">{{ template.name }}</mat-option>
}
@if (templates.length === 0 && clientForm.get('bootServer')?.value) {
<mat-option disabled>{{ 'noTemplatesInServer' | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</fieldset>
@if (isEditMode) {
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'labelHardwareProfile' | translate }}</legend>
<div class="hardware-profile-box">
<mat-form-field class="form-field">
<mat-select formControlName="hardwareProfile">
<mat-option [value]="null">{{ 'noHardwareProfileAssigned' | translate }}</mat-option>
@for (profile of hardwareProfiles; track profile['@id']) {
<mat-option [value]="profile['@id']">{{ profile.description }}</mat-option>
}
</mat-select>
</mat-form-field>
<button type="button" mat-icon-button class="hardware-profile-link" (click)="goToHardwareProfiles()"
[matTooltip]="'manageHardwareProfiles' | translate">
<mat-icon>settings</mat-icon>
</button>
</div>
</fieldset>
<h1 mat-dialog-title>{{ dialogTitle | translate }}</h1>
<div [ngClass]="{'loading': loading}">
@if (loading) {
<mat-spinner class="loading-spinner"></mat-spinner>
}
@if (clientForm && !loading) {
<form [formGroup]="clientForm" class="client-form grid-form">
<app-ou-select
class="form-field"
formControlName="organizationalUnit"
[units]="parentUnitsWithPaths"
[label]="'labelOrganizationalUnit'"
(selectionChange)="onParentChange($event)">
</app-ou-select>
<mat-form-field class="form-field">
<mat-label>{{ 'labelName' | translate }}</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelSerialNumber' | translate }}</mat-label>
<input matInput formControlName="serialNumber">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelNetiface' | translate }}</mat-label>
<mat-select formControlName="netiface">
@for (type of netifaceTypes; track type.value) {
<mat-option [value]="type.value">{{ type.name }}</mat-option>
}
<mat-form-field class="form-field full-width">
<mat-label>{{ 'labelNotes' | translate }}</mat-label>
<textarea matInput formControlName="notes" rows="4"
[placeholder]="'notesPlaceholder' | translate"></textarea>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelMac' | translate }}</mat-label>
<input matInput formControlName="mac" required>
<mat-error>{{ 'macError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelIp' | translate }}</mat-label>
<input matInput formControlName="ip" required>
<mat-error>{{ 'ipError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelRepository' | translate }}</mat-label>
<mat-select formControlName="repository">
@for (repository of repositories; track repository['@id']) {
<mat-option [value]="repository['@id']">{{ repository.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-checkbox formControlName="maintenance">
{{ 'maintenance' | translate }}
</mat-checkbox>
<!-- ===== Agrupación: Configuración del menú ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'menuConfiguration' | translate }}</legend>
<div class="fieldset-grid-2">
<mat-form-field class="form-field">
<mat-label>{{ 'labelMenu' | translate }}</mat-label>
<mat-select formControlName="menu">
@for (menu of menus; track menu['@id']) {
<mat-option [value]="menu['@id']">{{ menu.name }}</mat-option>
}
</mat-select>
<mat-error>{{ 'menuError' | translate }}</mat-error>
</mat-form-field>
</form>
</div>
<mat-form-field class="form-field">
<mat-label>{{ 'labelResolution' | translate }}</mat-label>
<mat-select formControlName="resolution" required>
<mat-option value="769">{{ 'resolution640x480x8' | translate }}</mat-option>
<mat-option value="771">{{ 'resolution800x600x8' | translate }}</mat-option>
<mat-option value="773">{{ 'resolution1024x768x8' | translate }}</mat-option>
<mat-option value="775">{{ 'resolution1280x1024x8' | translate }}</mat-option>
<mat-option value="784">{{ 'resolution640x480x16' | translate }}</mat-option>
<mat-option value="787">{{ 'resolution800x600x16' | translate }}</mat-option>
<mat-option value="791">{{ 'resolution1024x768x16' | translate }}</mat-option>
<mat-option value="794">{{ 'resolution1280x1024x16' | translate }}</mat-option>
<mat-option value="785">{{ 'resolution640x480x24' | translate }}</mat-option>
<mat-option value="788">{{ 'resolution800x600x24' | translate }}</mat-option>
<mat-option value="792">{{ 'resolution1024x768x24' | translate }}</mat-option>
<mat-option value="795">{{ 'resolution1280x1024x24' | translate }}</mat-option>
</mat-select>
<mat-error>{{ 'resolutionError' | translate }}</mat-error>
</mat-form-field>
</div>
</fieldset>
<!-- ===== Agrupación: Configuración de arranque ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'bootConfiguration' | translate }}</legend>
<div class="fieldset-grid-3">
<mat-form-field class="form-field">
<mat-label>{{ 'bootServer' | translate }}</mat-label>
<mat-select formControlName="bootServer" (selectionChange)="onBootServerChange($event)">
@for (server of bootServers; track server['@id']) {
<mat-option [value]="server['@id']">{{ server.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelOgLive' | translate }}</mat-label>
<mat-select formControlName="ogLive">
@for (ogLive of ogLives; track ogLive['@id']) {
<mat-option [value]="ogLive['@id']">{{ ogLive.name }}</mat-option>
}
@if (ogLives.length === 0 && clientForm.get('bootServer')?.value) {
<mat-option disabled>{{ 'noOgLivesInServer' | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelTemplate' | translate }}</mat-label>
<mat-select formControlName="pxeTemplate">
@for (template of templates; track template['@id']) {
<mat-option [value]="template['@id']">{{ template.name }}</mat-option>
}
@if (templates.length === 0 && clientForm.get('bootServer')?.value) {
<mat-option disabled>{{ 'noTemplatesInServer' | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</fieldset>
@if (isEditMode) {
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'labelHardwareProfile' | translate }}</legend>
<div class="hardware-profile-box">
<mat-form-field class="form-field">
<mat-select formControlName="hardwareProfile">
<mat-option [value]="null">{{ 'noHardwareProfileAssigned' | translate }}</mat-option>
@for (profile of hardwareProfiles; track profile['@id']) {
<mat-option [value]="profile['@id']">{{ profile.description }}</mat-option>
}
</mat-select>
</mat-form-field>
<button type="button" mat-icon-button class="hardware-profile-link" (click)="goToHardwareProfiles()"
[matTooltip]="'manageHardwareProfiles' | translate">
<mat-icon>settings</mat-icon>
</button>
</div>
</fieldset>
}
<mat-form-field class="form-field full-width">
<mat-label>{{ 'labelNotes' | translate }}</mat-label>
<textarea matInput formControlName="notes" rows="4"
[placeholder]="'notesPlaceholder' | translate"></textarea>
</mat-form-field>
</form>
}
</div>
</mat-dialog-content>
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" [disabled]="!clientForm.valid" (click)="onSubmit()">
{{ isEditMode ? 'Guardar' : 'Crear' }}
</button>
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" [disabled]="!clientForm.valid" (click)="onSubmit()">
{{ isEditMode ? 'Guardar' : 'Crear' }}
</button>
</mat-dialog-actions>

View File

@ -137,10 +137,6 @@ export class ManageClientComponent implements OnInit {
});
}
getSelectedParentName(): string | undefined {
const parentId = this.clientForm.get('organizationalUnit')?.value;
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
}
loadOgLivesByServer(bootServerIri: string): Promise<void> {
// Extract UUID from IRI (e.g. "/boot-servers/xxxx-xxx" -> "xxxx-xxx")

View File

@ -1,261 +1,279 @@
<mat-dialog-content class="create-ou-container">
<h1 mat-dialog-title>{{ isEditMode ? ('edit' | translate) : ('createButton' | translate) }} {{
'labelOrganizationalUnit' | translate }}</h1>
<div [ngClass]="{'loading': loading}">
<!-- Paso 1: General -->
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<span *ngIf="!loading" class="step-title">{{ 'labelGeneralTab' | translate }}</span>
<form *ngIf="generalFormGroup && !loading" [formGroup]="generalFormGroup" class="grid-form">
<mat-form-field class="form-field" appearance="fill">
<mat-label>{{ 'labelType' | 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" appearance="fill">
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field class="form-field" appearance="fill">
<mat-label>{{ 'labelCreateOrgUnitparent' | translate }}</mat-label>
<mat-select formControlName="parent" (selectionChange)="onParentChange($event)">
<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>
<h1 mat-dialog-title>{{ isEditMode ? ('edit' | translate) : ('createButton' | translate) }} {{
'labelOrganizationalUnit' | translate }}</h1>
<div [ngClass]="{'loading': loading}">
<!-- Paso 1: General -->
@if (loading) {
<mat-spinner class="loading-spinner"></mat-spinner>
}
@if (!loading) {
<span class="step-title">{{ 'labelGeneralTab' | translate }}</span>
}
@if (generalFormGroup && !loading) {
<form [formGroup]="generalFormGroup" class="grid-form">
<mat-form-field class="form-field" appearance="fill">
<mat-label>{{ 'labelType' | translate }}</mat-label>
<mat-select formControlName="type" required>
@for (type of filteredTypes; track type) {
<mat-option [value]="type">
{{ typeTranslations[type] }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field" appearance="fill">
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
<app-ou-select
class="form-field"
formControlName="parent"
[units]="parentUnitsWithPaths"
[label]="'labelCreateOrgUnitparent'"
(selectionChange)="onParentChange($event)">
</app-ou-select>
<mat-form-field class="form-field description-form-field" appearance="fill">
<mat-label>{{ 'labelDescription' | translate }}</mat-label>
<textarea matInput formControlName="description"></textarea>
</mat-form-field>
<mat-checkbox formControlName="excludeParentChanges">
{{ 'excludeParentChanges' | translate }}
</mat-checkbox>
</form>
}
<mat-form-field class="form-field description-form-field" appearance="fill">
<mat-label>{{ 'labelDescription' | translate }}</mat-label>
<textarea matInput formControlName="description"></textarea>
</mat-form-field>
<!-- Paso 2: Información del Aula -->
@if (generalFormGroup.value.type === 'classroom' && !loading) {
<span class="step-title">{{ 'labelClassroomInfoStep' | translate }}</span>
<form class="grid-form" [formGroup]="classroomInfoFormGroup">
<mat-form-field class="form-field">
<mat-label>{{ 'labelLocation' | translate }}</mat-label>
<input matInput formControlName="location">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelCapacity' | translate }}</mat-label>
<input matInput formControlName="capacity" type="number" min="0">
@if (classroomInfoFormGroup.get('capacity')?.hasError('min')) {
<mat-error>
{{ 'capacityWarning' | translate }}
</mat-error>
}
</mat-form-field>
<mat-form-field class="form-field" appearance="fill" style="grid-column: span 1;">
<mat-label>{{ 'labelAssociatedCalendar' | translate }}</mat-label>
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
@for (calendar of calendars; track calendar) {
<mat-option [value]="calendar['@id']">
{{ calendar.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<div class="projector-board-field">
<mat-slide-toggle formControlName="projector">{{ 'projectorAlt' | translate }}</mat-slide-toggle>
<mat-slide-toggle formControlName="board">{{ 'boardToggle' | translate }}</mat-slide-toggle>
<mat-slide-toggle formControlName="remotePc">{{ 'remotePcLabel' | translate }}</mat-slide-toggle>
@if (classroomInfoFormGroup.get('remotePc')?.value) {
<mat-slide-toggle formControlName="remotePcRebootOnUnreserve">{{ 'remotePcRebootOnUnreserveLabel' | translate }}</mat-slide-toggle>
}
</div>
</form>
}
<mat-checkbox formControlName="excludeParentChanges">
{{ 'excludeParentChanges' | translate }}
</mat-checkbox>
</form>
<!-- Paso 2: Información del Aula -->
@if (generalFormGroup.value.type === 'classroom' && !loading) {
<span class="step-title">{{ 'labelClassroomInfoStep' | translate }}</span>
<form class="grid-form" [formGroup]="classroomInfoFormGroup">
<!-- Paso 3: Configuración de Red -->
@if (!loading) {
<span class="step-title">{{ 'labelNetworkSettingsStep' | translate }}</span>
}
@if (networkSettingsFormGroup && !loading) {
<form [formGroup]="networkSettingsFormGroup" class="grid-form">
<!-- ===== Agrupación: Configuración de arranque ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'bootConfiguration' | translate }}</legend>
<div class="fieldset-grid-3">
<mat-form-field class="form-field">
<mat-label>{{ 'labelLocation' | translate }}</mat-label>
<input matInput formControlName="location">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelCapacity' | translate }}</mat-label>
<input matInput formControlName="capacity" type="number" min="0">
<mat-error *ngIf="classroomInfoFormGroup.get('capacity')?.hasError('min')">
{{ 'capacityWarning' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="form-field" appearance="fill" style="grid-column: span 1;">
<mat-label>{{ 'labelAssociatedCalendar' | 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>
<div class="projector-board-field">
<mat-slide-toggle formControlName="projector">{{ 'projectorAlt' | translate }}</mat-slide-toggle>
<mat-slide-toggle formControlName="board">{{ 'boardToggle' | translate }}</mat-slide-toggle>
<mat-slide-toggle formControlName="remotePc">{{ 'remotePcLabel' | translate }}</mat-slide-toggle>
@if (classroomInfoFormGroup.get('remotePc')?.value) {
<mat-slide-toggle formControlName="remotePcRebootOnUnreserve">{{ 'remotePcRebootOnUnreserveLabel' | translate }}</mat-slide-toggle>
<mat-label>{{ 'bootServer' | translate }}</mat-label>
<mat-select formControlName="bootServer" (selectionChange)="onBootServerChange($event)">
@for (server of bootServers; track server['@id']) {
<mat-option [value]="server['@id']">{{ server.name }}</mat-option>
}
</div>
</form>
}
<!-- Paso 3: Configuración de Red -->
<span *ngIf="!loading" class="step-title">{{ 'labelNetworkSettingsStep' | translate }}</span>
<form *ngIf="networkSettingsFormGroup && !loading" [formGroup]="networkSettingsFormGroup" class="grid-form">
<!-- ===== Agrupación: Configuración de arranque ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'bootConfiguration' | translate }}</legend>
<div class="fieldset-grid-3">
<mat-form-field class="form-field">
<mat-label>{{ 'bootServer' | translate }}</mat-label>
<mat-select formControlName="bootServer" (selectionChange)="onBootServerChange($event)">
@for (server of bootServers; track server['@id']) {
<mat-option [value]="server['@id']">{{ server.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'ogLive' | translate }}</mat-label>
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
@for (oglive of ogLives; track oglive['@id']) {
<mat-option [value]="oglive['@id']">{{ oglive.name }}</mat-option>
}
@if (ogLives.length === 0 && networkSettingsFormGroup.get('bootServer')?.value) {
<mat-option disabled>{{ 'noOgLivesInServer' | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelTemplate' | translate }}</mat-label>
<mat-select formControlName="pxeTemplate" (selectionChange)="onPxeTemplateChange($event)">
@for (template of pxeTemplates; track template['@id']) {
<mat-option [value]="template['@id']">{{ template.name }}</mat-option>
}
@if (pxeTemplates.length === 0 && networkSettingsFormGroup.get('bootServer')?.value) {
<mat-option disabled>{{ 'noTemplatesInServer' | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</fieldset>
<mat-form-field class="form-field">
<mat-label>{{ 'labelRepository' | 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-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelProxy' | translate }}</mat-label>
<input matInput formControlName="proxy">
<mat-label>{{ 'ogLive' | translate }}</mat-label>
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
@for (oglive of ogLives; track oglive['@id']) {
<mat-option [value]="oglive['@id']">{{ oglive.name }}</mat-option>
}
@if (ogLives.length === 0 && networkSettingsFormGroup.get('bootServer')?.value) {
<mat-option disabled>{{ 'noOgLivesInServer' | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelDns' | translate }}</mat-label>
<input matInput formControlName="dns">
<mat-label>{{ 'labelTemplate' | translate }}</mat-label>
<mat-select formControlName="pxeTemplate" (selectionChange)="onPxeTemplateChange($event)">
@for (template of pxeTemplates; track template['@id']) {
<mat-option [value]="template['@id']">{{ template.name }}</mat-option>
}
@if (pxeTemplates.length === 0 && networkSettingsFormGroup.get('bootServer')?.value) {
<mat-option disabled>{{ 'noTemplatesInServer' | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</fieldset>
<mat-form-field class="form-field">
<mat-label>{{ 'labelRepository' | translate }}</mat-label>
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
@for (repository of repositories; track repository) {
<mat-option [value]="repository['@id']">
{{ repository.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelProxy' | translate }}</mat-label>
<input matInput formControlName="proxy">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelDns' | translate }}</mat-label>
<input matInput formControlName="dns">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelNetmask' | translate }}</mat-label>
<input matInput formControlName="netmask">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label i18n="@@netiface-label">{{ 'labelNetiface' | translate }}</mat-label>
<mat-select formControlName="netiface">
@for (type of netifaceTypes; track type) {
<mat-option [value]="type.value">
{{ type.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelRouter' | translate }}</mat-label>
<input matInput formControlName="router">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelNtp' | translate }}</mat-label>
<input matInput formControlName="ntp">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelHardwareProfile' | translate }}</mat-label>
<mat-select formControlName="hardwareProfile">
@for (unit of hardwareProfiles; track unit) {
<mat-option [value]="unit['@id']">{{ unit.description
}}</mat-option>
}
</mat-select>
</mat-form-field>
<!-- ===== Agrupación: Configuración de multicast ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'multicastConfiguration' | translate }}</legend>
<div class="fieldset-grid-2">
<mat-form-field class="form-field">
<mat-label>{{ 'labelMcastIp' | translate }}</mat-label>
<input matInput formControlName="mcastIp">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelNetmask' | translate }}</mat-label>
<input matInput formControlName="netmask">
<mat-label>{{ 'labelMcastSpeed' | translate }}</mat-label>
<input matInput formControlName="mcastSpeed" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label i18n="@@netiface-label">{{ 'labelNetiface' | translate }}</mat-label>
<mat-select formControlName="netiface">
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
{{ type.name }}
</mat-option>
</mat-select>
<mat-label>{{ 'labelMcastPort' | translate }}</mat-label>
<input matInput formControlName="mcastPort" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelRouter' | translate }}</mat-label>
<input matInput formControlName="router">
<mat-label>{{ 'labelMcastMode' | translate }}</mat-label>
<mat-select formControlName="mcastMode">
@for (option of multicastModeOptions; track option) {
<mat-option [value]="option.value">
{{ option.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</fieldset>
<!-- ===== Agrupación: Configuración de p2p ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'p2pConfiguration' | translate }}</legend>
<div class="fieldset-grid-2">
<mat-form-field class="form-field">
<mat-label>{{ 'labelP2pMode' | translate }}</mat-label>
<mat-select formControlName="p2pMode">
@for (option of p2pModeOptions; track option) {
<mat-option [value]="option.value">
{{ option.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
@if (networkSettingsFormGroup.get('p2pMode')?.value === 'seeder') {
<mat-form-field class="form-field">
<mat-label>{{ 'labelP2pTime' | translate }} (minutos)</mat-label>
<input matInput formControlName="p2pTime" type="number">
</mat-form-field>
}
</div>
</fieldset>
<!-- ===== Agrupación: Configuración del menú ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'menuConfiguration' | translate }}</legend>
<div class="fieldset-grid-2">
<mat-form-field class="form-field">
<mat-label>{{ 'labelMenu' | translate }}</mat-label>
<mat-select formControlName="menu" (selectionChange)="onMenuChange($event)">
@for (menu of menus; track menu) {
<mat-option [value]="menu['@id']">
{{ menu.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelNtp' | translate }}</mat-label>
<input matInput formControlName="ntp">
<mat-label>{{ 'labelResolution' | translate }}</mat-label>
<mat-select formControlName="resolution">
<mat-option value="769">{{ 'resolution640x480x8' | translate }}</mat-option>
<mat-option value="771">{{ 'resolution800x600x8' | translate }}</mat-option>
<mat-option value="773">{{ 'resolution1024x768x8' | translate }}</mat-option>
<mat-option value="775">{{ 'resolution1280x1024x8' | translate }}</mat-option>
<mat-option value="784">{{ 'resolution640x480x16' | translate }}</mat-option>
<mat-option value="787">{{ 'resolution800x600x16' | translate }}</mat-option>
<mat-option value="791">{{ 'resolution1024x768x16' | translate }}</mat-option>
<mat-option value="794">{{ 'resolution1280x1024x16' | translate }}</mat-option>
<mat-option value="785">{{ 'resolution640x480x24' | translate }}</mat-option>
<mat-option value="788">{{ 'resolution800x600x24' | translate }}</mat-option>
<mat-option value="792">{{ 'resolution1024x768x24' | translate }}</mat-option>
<mat-option value="795">{{ 'resolution1280x1024x24' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelHardwareProfile' | translate }}</mat-label>
<mat-select formControlName="hardwareProfile">
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description
}}</mat-option>
</mat-select>
</mat-form-field>
<!-- ===== Agrupación: Configuración de multicast ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'multicastConfiguration' | translate }}</legend>
<div class="fieldset-grid-2">
<mat-form-field class="form-field">
<mat-label>{{ 'labelMcastIp' | translate }}</mat-label>
<input matInput formControlName="mcastIp">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelMcastSpeed' | translate }}</mat-label>
<input matInput formControlName="mcastSpeed" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelMcastPort' | translate }}</mat-label>
<input matInput formControlName="mcastPort" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelMcastMode' | 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>
</div>
</fieldset>
</div>
</fieldset>
</form>
}
<!-- ===== Agrupación: Configuración de p2p ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'p2pConfiguration' | translate }}</legend>
<div class="fieldset-grid-2">
<mat-form-field class="form-field">
<mat-label>{{ 'labelP2pMode' | 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" *ngIf="networkSettingsFormGroup.get('p2pMode')?.value === 'seeder'">
<mat-label>{{ 'labelP2pTime' | translate }} (minutos)</mat-label>
<input matInput formControlName="p2pTime" type="number">
</mat-form-field>
</div>
</fieldset>
<!-- ===== Agrupación: Configuración del menú ===== -->
<fieldset class="fieldset-group">
<legend class="fieldset-legend">{{ 'menuConfiguration' | translate }}</legend>
<div class="fieldset-grid-2">
<mat-form-field class="form-field">
<mat-label>{{ 'labelMenu' | translate }}</mat-label>
<mat-select formControlName="menu" (selectionChange)="onMenuChange($event)">
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
{{ menu.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'labelResolution' | translate }}</mat-label>
<mat-select formControlName="resolution">
<mat-option value="769">{{ 'resolution640x480x8' | translate }}</mat-option>
<mat-option value="771">{{ 'resolution800x600x8' | translate }}</mat-option>
<mat-option value="773">{{ 'resolution1024x768x8' | translate }}</mat-option>
<mat-option value="775">{{ 'resolution1280x1024x8' | translate }}</mat-option>
<mat-option value="784">{{ 'resolution640x480x16' | translate }}</mat-option>
<mat-option value="787">{{ 'resolution800x600x16' | translate }}</mat-option>
<mat-option value="791">{{ 'resolution1024x768x16' | translate }}</mat-option>
<mat-option value="794">{{ 'resolution1280x1024x16' | translate }}</mat-option>
<mat-option value="785">{{ 'resolution640x480x24' | translate }}</mat-option>
<mat-option value="788">{{ 'resolution800x600x24' | translate }}</mat-option>
<mat-option value="792">{{ 'resolution1024x768x24' | translate }}</mat-option>
<mat-option value="795">{{ 'resolution1280x1024x24' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</fieldset>
</form>
<!-- Paso 4: Información Adicional -->
<span *ngIf="!loading" class="step-title">{{ 'labelAdditionalInfoStep' | translate }}</span>
<form *ngIf="additionalInfoFormGroup && !loading" [formGroup]="additionalInfoFormGroup">
<mat-form-field class="form-field description-form-field">
<mat-label>{{ 'labelComments' | translate }}</mat-label>
<textarea matInput formControlName="comments"></textarea>
</mat-form-field>
</form>
</div>
<!-- Paso 4: Información Adicional -->
@if (!loading) {
<span class="step-title">{{ 'labelAdditionalInfoStep' | translate }}</span>
}
@if (additionalInfoFormGroup && !loading) {
<form [formGroup]="additionalInfoFormGroup">
<mat-form-field class="form-field description-form-field">
<mat-label>{{ 'labelComments' | translate }}</mat-label>
<textarea matInput formControlName="comments"></textarea>
</mat-form-field>
</form>
}
</div>
</mat-dialog-content>
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="onSubmit()"
[disabled]="!generalFormGroup.valid || !additionalInfoFormGroup.valid || !networkSettingsFormGroup.valid">{{
isEditMode ? ('buttonEdit' | translate) : ('createButton' | translate) }}</button>
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="onSubmit()"
[disabled]="!generalFormGroup.valid || !additionalInfoFormGroup.valid || !networkSettingsFormGroup.valid">{{
isEditMode ? ('buttonEdit' | translate) : ('createButton' | translate) }}</button>
</mat-dialog-actions>

View File

@ -430,10 +430,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
);
}
getSelectedParentName(): string | undefined {
const parentId = this.generalFormGroup.get('parent')?.value;
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
}
loadBootServers(): Promise<void> {
return new Promise((resolve, reject) => {

View File

@ -25,19 +25,14 @@
</mat-form-field>
}
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="selectClassStep"
text="{{ 'selectClassDescription' | translate }}">
<mat-label>{{ 'labelSelectClass' | translate }}</mat-label>
<mat-select (selectionChange)="loadChildUnits($event)" (opened)="unitSearchText = ''">
<div class="dropdown-search-wrapper">
<input type="text" class="dropdown-search-input" [(ngModel)]="unitSearchText" [ngModelOptions]="{standalone: true}" (keydown)="$event.stopPropagation()"
placeholder="{{ 'labelSearch' | translate }}">
</div>
@for (unit of getFilteredUnits(); track unit.id) {
<mat-option [value]="unit">{{ unit.displayName }}</mat-option>
}
</mat-select>
</mat-form-field>
<app-ou-select
class="search-boolean"
joyrideStep="selectClassStep"
text="{{ 'selectClassDescription' | translate }}"
[units]="units"
[label]="'labelSelectClass'"
(selectionChange)="loadChildUnits($event)">
</app-ou-select>
</div>

View File

@ -11,6 +11,7 @@ import { ActivatedRoute } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { DeleteModalComponent } from 'src/app/shared/delete_modal/delete-modal/delete-modal.component';
import { forkJoin } from 'rxjs';
import { DataService as GroupsDataService } from '../../groups/services/data.service';
@Component({
selector: 'app-pxe-boot-files',
@ -33,14 +34,6 @@ export class PxeBootFilesComponent implements OnInit {
ogLiveOptions: any[] = [];
globalOgLive: string | null = null;
unitSearchText: string = '';
getFilteredUnits(): any[] {
if (!this.unitSearchText) {
return this.units;
}
const searchText = this.unitSearchText.toLowerCase();
return this.units.filter(unit => unit.displayName.toLowerCase().includes(searchText));
}
length: number = 0;
itemsPerPage: number = 10;
page: number = 0;
@ -56,7 +49,8 @@ export class PxeBootFilesComponent implements OnInit {
private configService: ConfigService,
private joyrideService: JoyrideService,
private route: ActivatedRoute,
private dialog: MatDialog
private dialog: MatDialog,
private groupsDataService: GroupsDataService
) {
this.baseUrl = this.configService.apiUrl;
this.taskForm = this.fb.group({
@ -111,25 +105,19 @@ export class PxeBootFilesComponent implements OnInit {
.filter((unit: any) => unit.type === 'classroom')
.map((unit: any) => ({
...unit,
displayName: this.buildFullDisplayName(unit, rawUnits)
path: this.groupsDataService.getOrganizationalUnitPath(unit, rawUnits)
}));
this.units.sort((a, b) => a.displayName.localeCompare(b.displayName));
this.units.sort((a, b) => {
const pathA = a.path || a.name;
const pathB = b.path || b.name;
return pathA.localeCompare(pathB);
});
},
error => console.error(this.translate.instant('errorFetchingOrganizationalUnits1'), error)
);
}
private buildFullDisplayName(unit: any, allUnits: any[]): string {
let hierarchy = unit.name;
let currentParent = unit.parent;
while (currentParent) {
hierarchy = `${currentParent.name} / ${hierarchy}`;
const fullParent = allUnits.find((u: any) => u['@id'] === currentParent['@id']);
currentParent = fullParent ? fullParent.parent : null;
}
return hierarchy;
}
fetchPxeTemplates(): void {
this.http.get<any>(`${this.baseUrl}/pxe-templates?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters })

View File

@ -3,18 +3,13 @@
<h2 mat-dialog-title>Añade clientes a {{data.subnetName}}</h2>
<mat-dialog-content>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'selectClassroomPrompt' | translate }}</mat-label>
<mat-select [formControl]="unitControl" (selectionChange)="loadChildUnits($event)" (opened)="unitSearchText = ''">
<div class="dropdown-search-wrapper">
<input type="text" class="dropdown-search-input" [(ngModel)]="unitSearchText" [ngModelOptions]="{standalone: true}" (keydown)="$event.stopPropagation()"
placeholder="{{ 'labelSearch' | translate }}">
</div>
@for (unit of getFilteredUnits(); track unit.id) {
<mat-option [value]="unit">{{ unit.displayName }}</mat-option>
}
</mat-select>
</mat-form-field>
<app-ou-select
class="full-width"
[formControl]="unitControl"
[units]="units"
[label]="'selectClassroomPrompt'"
(selectionChange)="loadChildUnits($event)">
</app-ou-select>
<div class="checkbox-group">
<label>{{ 'labelClients' | translate }}</label>

View File

@ -7,6 +7,7 @@ import { FormControl } from '@angular/forms';
import { ToastrService } from 'ngx-toastr';
import { OperationResultDialogComponent } from "../operation-result-dialog/operation-result-dialog.component";
import { ConfigService } from '@services/config.service';
import { DataService as GroupsDataService } from "../../groups/services/data.service";
@Component({
selector: 'app-add-clients-to-subnet',
@ -23,15 +24,7 @@ export class AddClientsToSubnetComponent implements OnInit {
unitControl = new FormControl();
childUnitControl = new FormControl();
allSelected: boolean = false;
unitSearchText: string = '';
getFilteredUnits(): any[] {
if (!this.unitSearchText) {
return this.units;
}
const searchText = this.unitSearchText.toLowerCase();
return this.units.filter(unit => unit.displayName.toLowerCase().includes(searchText));
}
constructor(
private http: HttpClient,
@ -39,6 +32,7 @@ export class AddClientsToSubnetComponent implements OnInit {
private toastService: ToastrService,
public dialog: MatDialog,
private configService: ConfigService,
private groupsDataService: GroupsDataService,
@Inject(MAT_DIALOG_DATA) public data: { subnetUuid: string, subnetName: string }
) {
this.baseUrl = this.configService.apiUrl;
@ -55,26 +49,20 @@ export class AddClientsToSubnetComponent implements OnInit {
const rawUnits = response['hydra:member'];
this.units = rawUnits.map((unit: any) => ({
...unit,
displayName: this.buildFullDisplayName(unit, rawUnits)
name: unit.name,
path: this.groupsDataService.getOrganizationalUnitPath(unit, rawUnits)
}));
this.units.sort((a, b) => a.displayName.localeCompare(b.displayName));
this.units.sort((a, b) => {
const pathA = a.path || a.name;
const pathB = b.path || b.name;
return pathA.localeCompare(pathB);
});
this.loading = false;
},
error => console.error(this.translate.instant('errorFetchingOrganizationalUnits1'), error)
);
}
private buildFullDisplayName(unit: any, allUnits: any[]): string {
let hierarchy = unit.name;
let currentParent = unit.parent;
while (currentParent) {
hierarchy = `${currentParent.name} / ${hierarchy}`;
const fullParent = allUnits.find((u: any) => u['@id'] === currentParent['@id']);
currentParent = fullParent ? fullParent.parent : null;
}
return hierarchy;
}
loadChildUnits(event: any) {
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${event.value.id}&exists[subnet]=false`).subscribe(

View File

@ -0,0 +1,11 @@
:host {
display: block;
width: 100%;
}
.full-width {
width: 100%;
}
.unit-name {
line-height: normal;
padding-top: 4px;
}

View File

@ -0,0 +1,21 @@
<mat-form-field class="full-width">
<mat-label>{{ label | translate }}</mat-label>
<mat-select [multiple]="multiple" [required]="required" [disabled]="disabled"
[(ngModel)]="value" (selectionChange)="onSelectionChange($event)" (opened)="onOpened()">
<mat-select-trigger>
{{ getSelectedName() }}
</mat-select-trigger>
<div class="dropdown-search-wrapper">
<input type="text" class="dropdown-search-input" [(ngModel)]="searchText" (keydown)="$event.stopPropagation()"
placeholder="{{ 'labelSearch' | translate }}">
</div>
@for (unit of filteredUnits; track (unit.id || unit['@id'])) {
<mat-option [value]="unit.id || unit['@id']">
<div class="unit-name">{{ unit.name }}</div>
@if (unit.path) {
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
}
</mat-option>
}
</mat-select>
</mat-form-field>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OuSelectComponent } from './ou-select.component';
describe('OuSelectComponent', () => {
let component: OuSelectComponent;
let fixture: ComponentFixture<OuSelectComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OuSelectComponent]
})
.compileComponents();
fixture = TestBed.createComponent(OuSelectComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,77 @@
import { Component, Input, Output, EventEmitter, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-ou-select',
templateUrl: './ou-select.component.html',
styleUrls: ['./ou-select.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OuSelectComponent),
multi: true
}
]
})
export class OuSelectComponent implements ControlValueAccessor {
@Input() units: any[] = [];
@Input() multiple = false;
@Input() required = false;
@Input() disabled = false;
@Input() label: string = 'labelOrganizationalUnit';
@Output() selectionChange = new EventEmitter<any>();
value: any = null;
searchText: string = '';
get filteredUnits() {
if (!this.searchText) {
return this.units;
}
const lower = this.searchText.toLowerCase();
return this.units.filter(unit =>
unit.name.toLowerCase().includes(lower) ||
(unit.path && unit.path.toLowerCase().includes(lower))
);
}
onOpened() {
this.searchText = '';
}
onChange = (val: any) => {};
onTouched = () => {};
writeValue(val: any): void {
this.value = val;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onSelectionChange(event: any) {
this.value = event.value;
this.onChange(this.value);
this.selectionChange.emit(event);
this.onTouched();
}
getSelectedName(): string {
if (this.multiple) {
if (!this.value || !Array.isArray(this.value)) return '';
return this.units.filter(u => this.value.includes(u.id || u['@id'])).map(u => u.name).join(', ');
} else {
return this.units.find(u => (u.id || u['@id']) === this.value)?.name || '';
}
}
}