Merge pull request 'develop' (#10) from develop into main
testing/ogGui-multibranch/pipeline/head This commit looks good Details
testing/ogGui-multibranch/pipeline/tag This commit looks good Details

Reviewed-on: #10
pull/12/head^2 opengnsys_devel-0.0.20
Manuel Aranda Rosales 2025-01-14 09:48:54 +01:00
commit bed68408d8
75 changed files with 1874 additions and 3364 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## [0.7.0] - 2024-12-10
### Refactored
- Refactored the group screen, removing the separate tabs for clients, advanced search, and organizational units.
- Added support for partitioning functionality in the client detail view.
## [0.6.1] - 2024-11-19
### Improved
@ -7,7 +13,6 @@
- Improve test coverage.
- New view for clients inside the classroom on the main page.
## [0.6.0] - 2024-11-19
### Added
@ -30,12 +35,8 @@
- Made predefined commands read-only to prevent accidental modifications.
- Simplified the task creation modal to enhance user experience.
- Adjusted the translation system to cover new elements and improve consistency (work in progress).
- New element view from clients on groups main view
- New element view from clients on groups main view.
### Fixed
- Resolved an issue that prevented editing software profiles correctly.
- Fixed a bug where newly created commands failed to execute in the commands section.
---

View File

@ -134,7 +134,8 @@
"src/assets"
],
"styles": [
"src/styles.css"
"src/styles.css",
"src/custom-theme.scss"
],
"scripts": []
}

View File

@ -40,50 +40,50 @@ import {
MainRepositoryViewComponent
} from "./components/repositories/main-repository-view/main-repository-view.component";
import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
import {MenusComponent} from "./components/menus/menus.component";
const routes: Routes = [
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
{
path: '',
component: MainLayoutComponent,
children: [
{ path: 'dashboard', component: DashboardComponent },
{ path: 'admin', component: AdminComponent },
{ path: 'users', component: UsersComponent },
{ path: 'env-vars', component: EnvVarsComponent },
{ path: 'user-groups', component: RolesComponent },
{ path: 'groups', component: GroupsComponent },
{ path: 'pxe-images', component: PXEimagesComponent },
{ path: 'pxe', component: PxeComponent },
{ path: 'pxe-boot-file', component: PxeBootFilesComponent },
{ path: 'ogboot-status', component: OgbootStatusComponent },
{ path: 'dhcp', component: OgdhcpComponent },
{ path: 'subnets', component: OgDhcpSubnetsComponent },
{ path: 'ogdhcp-status', component: StatusComponent },
{ path: 'commands', component: CommandsComponent },
{ path: 'commands-groups', component: CommandsGroupsComponent },
{ path: 'commands-task', component: CommandsTaskComponent },
{ path: 'commands-logs', component: TaskLogsComponent },
{ path: 'calendars', component: CalendarComponent },
{ 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: 'images', component: ImagesComponent },
{ path: 'repositories', component: RepositoriesComponent },
{ path: 'repository/:id', component: MainRepositoryViewComponent },
{ path: 'software', component: SoftwareComponent },
{ path: 'software-profiles', component: SoftwareProfileComponent },
{ path: 'operative-systems', component: OperativeSystemComponent },
],
},
{
path: 'auth',
component: AuthLayoutComponent,
children: [
{ path: 'login', component: LoginComponent },
],
},
{ path: '**', component: PageNotFoundComponent },
{ path: '', component: MainLayoutComponent,
children: [
{ path: 'dashboard', component: DashboardComponent },
{ path: 'admin', component: AdminComponent },
{ path: 'users', component: UsersComponent },
{ path: 'env-vars', component: EnvVarsComponent },
{ path: 'user-groups', component: RolesComponent },
{ path: 'groups', component: GroupsComponent },
{ path: 'pxe-images', component: PXEimagesComponent },
{ path: 'pxe', component: PxeComponent },
{ path: 'pxe-boot-file', component: PxeBootFilesComponent },
{ path: 'ogboot-status', component: OgbootStatusComponent },
{ path: 'dhcp', component: OgdhcpComponent },
{ path: 'subnets', component: OgDhcpSubnetsComponent },
{ path: 'ogdhcp-status', component: StatusComponent },
{ path: 'commands', component: CommandsComponent },
{ path: 'commands-groups', component: CommandsGroupsComponent },
{ path: 'commands-task', component: CommandsTaskComponent },
{ path: 'commands-logs', component: TaskLogsComponent },
{ path: 'calendars', component: CalendarComponent },
{ 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: 'images', component: ImagesComponent },
{ path: 'repositories', component: RepositoriesComponent },
{ path: 'repository/:id', component: MainRepositoryViewComponent },
{ path: 'software', component: SoftwareComponent },
{ path: 'software-profiles', component: SoftwareProfileComponent },
{ path: 'operative-systems', component: OperativeSystemComponent },
{ path: 'menus', component: MenusComponent },
],
},
{
path: 'auth',
component: AuthLayoutComponent,
children: [
{ path: 'login', component: LoginComponent },
],
},
{ path: '**', component: PageNotFoundComponent },
];
@NgModule({

View File

@ -95,10 +95,7 @@ import { CreateCommandGroupComponent } from './components/commands/commands-grou
import { DetailCommandGroupComponent } from './components/commands/commands-groups/detail-command-group/detail-command-group.component';
import { CreateTaskComponent } from './components/commands/commands-task/create-task/create-task.component';
import { DetailTaskComponent } from './components/commands/commands-task/detail-task/detail-task.component';
import { ClientTabViewComponent } from './components/groups/components/client-tab-view/client-tab-view.component';
import { AdvancedSearchComponent } from './components/groups/components/advanced-search/advanced-search.component';
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
import { OrganizationalUnitTabViewComponent } from './components/groups/components/organizational-unit-tab-view/organizational-unit-tab-view.component';
import { ServerInfoDialogComponent } from './components/ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component';
import { StatusComponent } from './components/ogdhcp/og-dhcp-subnets/status/status.component';
import {MatSliderModule} from '@angular/material/slider';
@ -124,6 +121,9 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component';
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';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
}
@ -181,10 +181,7 @@ export function HttpLoaderFactory(http: HttpClient) {
DetailCommandGroupComponent,
CreateTaskComponent,
DetailTaskComponent,
ClientTabViewComponent,
AdvancedSearchComponent,
TaskLogsComponent,
OrganizationalUnitTabViewComponent,
ServerInfoDialogComponent,
StatusComponent,
ClientMainViewComponent,
@ -206,6 +203,9 @@ export function HttpLoaderFactory(http: HttpClient) {
MainRepositoryViewComponent,
ExecuteCommandOuComponent,
EnvVarsComponent,
MenusComponent,
CreateMenuComponent,
CreateMultipleClientComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,

View File

@ -31,9 +31,11 @@
<ng-container *ngIf="column.columnDef !== 'readOnly'">
{{ column.cell(command) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'readOnly'">
<mat-chip *ngIf="command.readOnly" class="mat-chip-readonly-true"><mat-icon style="color:white;">check</mat-icon></mat-chip>
<mat-chip *ngIf="!command.readOnly" class="mat-chip-readonly-false"><mat-icon style="color:white;">close</mat-icon></mat-chip>
<mat-icon [color]="command[column.columnDef] ? 'primary' : 'warn'">
{{ command[column.columnDef] ? 'check_circle' : 'cancel' }}
</mat-icon>
</ng-container>
</td>
</ng-container>
@ -41,7 +43,6 @@
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<td mat-cell *matCellDef="let command" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
<button mat-icon-button color="info" (click)="executeCommand($event, command)"><mat-icon>play_arrow</mat-icon></button>
<button mat-icon-button color="info" (click)="viewDetails($event, command)"><mat-icon>visibility</mat-icon></button>
<button mat-icon-button color="primary" [disabled]="command.readOnly" (click)="editCommand($event, command)"><mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" [disabled]="command.readOnly" (click)="deleteCommand($event, command)"><mat-icon>delete</mat-icon></button>

View File

@ -50,7 +50,7 @@ export class CommandsComponent implements OnInit {
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/commands`;
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
private joyrideService: JoyrideService) {}
ngOnInit(): void {
@ -114,19 +114,6 @@ export class CommandsComponent implements OnInit {
});
}
executeCommand(event: MouseEvent, command: any): void {
this.dialog.open(ExecuteCommandComponent, {
width: '50%',
data: { commandData: command }
}).afterClosed().subscribe((result) => {
if (result) {
console.log('Comando ejecutado con éxito');
} else {
console.log('Ejecución de comando cancelada');
}
});
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
@ -147,5 +134,5 @@ export class CommandsComponent implements OnInit {
themeColor: '#3f51b5'
});
}
}

View File

@ -1,40 +1,10 @@
<h2 mat-dialog-title>{{ 'executeCommandTitle' | translate }}</h2>
<button mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu">
<mat-icon>terminal</mat-icon>
</button>
<mat-dialog-content class="form-container">
<form [formGroup]="form" class="command-form">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
<mat-select formControlName="unit">
<mat-option *ngFor="let unit of units" [value]="unit.uuid">{{ unit.name }}</mat-option>
</mat-select>
</mat-form-field>
<mat-menu #commandMenu="matMenu">
<button mat-menu-item [disabled]="command.disabled" *ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)">
{{ command.name }}
</button>
</mat-menu>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'subOrganizationalUnitLabel' | translate }}</mat-label>
<mat-select formControlName="childUnit">
<mat-option *ngFor="let child of childUnits" [value]="child.uuid">{{ child.name }}</mat-option>
</mat-select>
</mat-form-field>
<div class="checkbox-group">
<label>{{ 'clientsLabel' | translate }}</label>
<div *ngIf="clients.length > 0">
<mat-checkbox *ngFor="let client of clients"
(change)="toggleClientSelection(client.uuid)"
[checked]="form.get('clientSelection')?.value.includes(client.uuid)">
{{ client.name }}
</mat-checkbox>
</div>
<div *ngIf="clients.length === 0">
<p>{{ 'noClientsAvailable' | translate }}</p>
</div>
</div>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="closeModal()">{{ 'buttonCancel' | translate }}</button>
<button mat-button (click)="executeCommand()" [disabled]="!form.get('clientSelection')?.value.length">{{ 'buttonExecute' | translate }}</button>
</mat-dialog-actions>

View File

@ -10,11 +10,13 @@ import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/materia
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatTableModule } from '@angular/material/table';
import { MatSelectModule } from '@angular/material/select';
import { MatSelectModule } from '@angular/material/select';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { ToastrModule, ToastrService } from 'ngx-toastr';
import { DataService } from '../data.service';
import {MatIconModule} from "@angular/material/icon";
import {MatMenu, MatMenuModule} from "@angular/material/menu";
describe('ExecuteCommandComponent', () => {
let component: ExecuteCommandComponent;
@ -31,9 +33,11 @@ describe('ExecuteCommandComponent', () => {
MatInputModule,
MatCheckboxModule,
MatButtonModule,
MatMenuModule,
BrowserAnimationsModule,
MatTableModule,
MatSelectModule,
MatIconModule,
ToastrModule.forRoot(),
TranslateModule.forRoot()
],

View File

@ -1,7 +1,9 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {Component, Inject, Input, OnInit} 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';
import {Router} from "@angular/router";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-execute-command',
@ -9,92 +11,129 @@ import { FormBuilder, FormGroup } from '@angular/forms';
styleUrls: ['./execute-command.component.css']
})
export class ExecuteCommandComponent implements OnInit {
form: FormGroup;
units: any[] = [];
childUnits: any[] = [];
clients: any[] = [];
selectedClients: any[] = [];
@Input() clientData: any = {};
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
loading: boolean = true;
arrayCommands: any[] = [
{name: 'Enceder', slug: 'power-on', disabled: false},
{name: 'Apagar', slug: 'power-off', disabled: false},
{name: 'Reiniciar', slug: 'reboot', disabled: false},
{name: 'Iniciar Sesión', slug: 'login', disabled: true},
{name: 'Crear Image', slug: 'create-image', disabled: false},
{name: 'Deploy Image', slug: 'deploy-image', disabled: false},
{name: 'Eliminar Imagen Cache', slug: 'delete-image-cache', disabled: true},
{name: 'Particionar y Formatear', slug: 'partition', disabled: false},
{name: 'Inventario Software', slug: 'software-inventory', disabled: true},
{name: 'Inventario Hardware', slug: 'hardware-inventory', disabled: true},
{name: 'Ejecutar script', slug: 'run-script', disabled: true},
];
constructor(
private dialogRef: MatDialogRef<ExecuteCommandComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private dialog: MatDialog,
private http: HttpClient,
private fb: FormBuilder
private fb: FormBuilder,
private router: Router,
private toastService: ToastrService
) {
this.form = this.fb.group({
unit: [null],
childUnit: [null],
clientSelection: [[]]
});
}
ngOnInit(): void {
this.loadUnits();
this.form.get('unit')?.valueChanges.subscribe(value => this.onUnitChange(value));
this.form.get('childUnit')?.valueChanges.subscribe(value => this.onChildUnitChange(value));
this.clientData = this.clientData || {};
this.loadClient(this.clientData)
}
loadUnits(): void {
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`).subscribe(
response => {
this.units = response['hydra:member'].filter((unit: { type: string; }) => unit.type === 'organizational-unit');
loadClient = (uuid: string) => {
this.http.get<any>(`${this.baseUrl}${uuid}`).subscribe({
next: data => {
this.clientData = data;
this.loading = false;
},
error => console.error('Error fetching organizational units:', error)
error: error => {
console.error('Error al obtener el cliente:', error);
}
});
}
onCommandSelect(action: any): void {
if (action === 'partition') {
this.openPartitionAssistant();
}
if (action === 'create-image') {
this.openCreateImageAssistant();
}
if (action === 'deploy-image') {
this.openDeployImageAssistant();
}
if (action === 'reboot') {
this.rebootClient();
}
if (action === 'power-off') {
this.powerOffClient();
}
if (action === 'power-on') {
this.powerOnClient();
}
}
rebootClient(): void {
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/reboot`, {}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
},
error => {
this.toastService.error('Error de conexión con el cliente');
}
);
}
onUnitChange(unitId: string): void {
const unit = this.units.find(unit => unit.uuid === unitId);
this.childUnits = unit ? this.getAllChildren(unit) : [];
this.clients = [];
this.form.patchValue({ childUnit: null, clientSelection: [] });
}
getAllChildren(unit: any): any[] {
let allChildren = [];
if (unit.children && unit.children.length > 0) {
for (const child of unit.children) {
allChildren.push(child);
allChildren = allChildren.concat(this.getAllChildren(child));
}
}
return allChildren;
}
onChildUnitChange(childUnitId: string): void {
const childUnit = this.childUnits.find(unit => unit.uuid === childUnitId);
this.clients = childUnit && childUnit.clients ? childUnit.clients : [];
this.form.patchValue({ clientSelection: [] });
}
executeCommand(): void {
powerOnClient(): void {
const payload = {
clients: ['/clients/'+this.form.get('clientSelection')?.value]
};
this.http.post(`${this.baseUrl}/commands/${this.data.commandData.uuid}/execute`, payload)
.subscribe({
next: () => {
console.log('Comando ejecutado con éxito');
this.dialogRef.close(true);
},
error: (error) => {
console.error('Error al ejecutar el comando:', error);
}
});
}
closeModal(): void {
this.dialogRef.close(false);
}
toggleClientSelection(clientId: string): void {
const selectedClients = this.form.get('clientSelection')?.value;
if (selectedClients.includes(clientId)) {
this.form.get('clientSelection')?.setValue(selectedClients.filter((id: string) => id !== clientId));
} else {
this.form.get('clientSelection')?.setValue([...selectedClients, clientId]);
client: this.clientData['@id']
}
this.http.post(`${this.baseUrl}${this.clientData.repository['@id']}/wol`, payload).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
},
error => {
this.toastService.error('Error de conexión con el cliente');
}
);
}
powerOffClient(): void {
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/power-off`, {}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
},
error => {
this.toastService.error('Error de conexión con el cliente');
}
);
}
openPartitionAssistant(): void {
this.router.navigate([`/clients/${this.clientData.uuid}/partition-assistant`]).then(r => {
console.log('navigated', r);
});
}
openCreateImageAssistant(): void {
this.router.navigate([`/clients/${this.clientData.uuid}/create-image`]).then(r => {
console.log('navigated', r);
});
}
openDeployImageAssistant(): void {
this.router.navigate([`/clients/${this.clientData.uuid}/deploy-image`]).then(r => {
console.log('navigated', r);
});
}
}

View File

@ -1,333 +0,0 @@
.groupLists-container {
display: flex;
flex-wrap: wrap;
margin-bottom: 30px;
}
.search-container {
display: flex;
flex-grow: 1;
margin: 10px;
}
.card {
flex-grow: 1;
margin: 10px;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 100px;
padding: 10px;
}
.title {
margin-left: 10px;
}
.container {
display: flex;
flex-direction: column;
}
.header {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
}
.header mat-form-field {
width: 300px;
}
.main-content {
display: flex;
}
.filters {
padding: 20px;
display: flex;
flex-direction: column;
width: 300px;
}
.results {
width: 100%;
}
.result-card {
width: 100%;
max-width: 250px;
height: auto;
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
padding: 15px;
margin: 10px 10px;
}
.result-card.small-card {
width: 100%;
max-width: 180px;
padding: 10px;
margin: 10px 10px 10px 10px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.result-container {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
padding: 16px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
background-color: #fff;
max-width: 300px;
margin: 8px;
box-sizing: border-box;
}
.result-card {
&.card-og-live {
background-color: yellow; /* Verde */
color: white;
}
&.card-busy {
background-color: indianred; /* Naranja */
color: white;
}
&.card-windows {
background-color: cornflowerblue; /* Azul */
color: white;
}
&.card-linux {
background-color: mediumpurple; /* Púrpura */
color: white;
}
&.card-macos {
background-color: cornflowerblue; /* Rojo */
color: white;
}
&.card-off {
background-color: #9e9e9e; /* Gris */
color: white;
}
}
.result-container:hover {
transform: translateY(-4px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.result-title {
font-size: 1.25rem;
font-weight: bold;
margin: 8px 0;
text-align: center;
}
.result-content {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.9rem;
}
.result-type,
.result-ip,
.result-mac,
.result-status,
.result-internal-units,
.result-clients {
margin: 0;
padding: 0;
text-align: left;
}
.result-checkbox {
margin-bottom: 8px;
cursor: pointer;
}
.card-og-live {
background-color: #4caf50;
color: white;
}
.card-busy {
background-color: #f44336;
color: white;
}
.card-windows {
background-color: #2196f3;
color: white;
}
.card-linux {
background-color: #9c27b0;
color: white;
}
.card-macos {
background-color: #ff9800;
color: white;
}
.card-off {
background-color: #9e9e9e;
color: white;
}
.divider {
margin: 20px 0;
}
button {
margin-bottom: 10px;
}
.button-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.button-group button {
flex-grow: 1;
}
.red-card {
background-color: #f35f53;
color: white;
}
.green-card {
background-color: #4caf50;
color: white;
}
.view-mode-buttons {
padding: 20px;
}
.view-mode-buttons button.active {
font-weight: bold;
color: #3f51b5;
}
.result-card-list {
display: flex;
flex-direction: row;
align-items: center;
padding: 5px;
margin-bottom: 2px;
border: 1px solid #ddd;
}
.result-card-list mat-checkbox {
margin-right: 8px;
}
.result-card-list .result-title {
font-size: 14px;
font-weight: bold;
margin-right: 8px;
}
.result-card-list mat-card-content {
display: flex;
flex-direction: row;
gap: 8px;
font-size: 12px;
}
.no-results {
display: flex;
justify-content: center;
align-items: center;
height: 100%; /* Ajusta según el contenedor padre */
text-align: center;
}
.no-results p {
font-size: 1.5rem; /* Tamaño de fuente más grande */
font-weight: bold; /* Negrita para mejor visibilidad */
color: #555; /* Cambia el color según tu diseño */
}
.result-card-list p {
margin: 0;
}
.result-list {
height: auto;
}
@media (max-width: 600px) {
.result-container {
max-width: 100%;
}
}
/* Estilo general para la tarjeta */
.result-card {
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.result-card:hover {
transform: translateY(-4px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
/* Centrar contenido para clientes */
.centered-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.client-info p {
margin: 0.5rem 0;
font-size: 1rem;
color: #333;
}
.client-info .client-name {
font-size: 0.9rem;
color: #555;
}
.client-info .client-text {
font-size: 0.8rem;
color: #555;
}
.client-info strong {
font-weight: bold;
color: black;
}
.result-title {
font-size: 1.2rem;
font-weight: bold;
color: #333;
}
.client-image {
width: 100%;
height: auto;
}

View File

@ -1,192 +0,0 @@
<h2 class="title" i18n="@@searchTitle">Búsqueda avanzada</h2>
<div class="container">
<div class="header">
<mat-form-field>
<mat-label i18n="@@selectFilterLabel">Seleccione filtro</mat-label>
<mat-select (selectionChange)="loadSelectedFilter($event.value)">
<mat-option *ngFor="let savedFilter of savedFilterNames" [value]="savedFilter">
{{ savedFilter[0] }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-divider class="divider"></mat-divider>
<div class="view-mode-buttons" joyrideStep="viewModeStep" text="Elige cómo quieres ver los resultados: en cuadrícula o en lista.">
<button mat-button (click)="changeViewMode('grid')" [class.active]="viewMode === 'grid'">
<mat-icon>grid_view</mat-icon> Cuadrícula
</button>
<button mat-button (click)="changeViewMode('list')" [class.active]="viewMode === 'list'">
<mat-icon>list</mat-icon> Lista
</button>
<button mat-button (click)="toggleSelectAll()">
<mat-icon>checkbox</mat-icon> Seleccionar/Deseleccionar Todos
</button>
</div>
<div class="main-content">
<div class="filters">
<mat-form-field>
<mat-label i18n="@@selectOptionLabel">Selecciona una opción</mat-label>
<mat-select [(value)]="selectedFilter1" (selectionChange)="applyFilter()">
<mat-option value="ou" i18n="@@organizationalUnitsOption">Unidades organizativas</mat-option>
<mat-option value="client" i18n="@@clientsOption">Clientes</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label i18n="@@nameLabel">Nombre</mat-label>
<input matInput placeholder="Unidad organizativa" (input)="applyFilter()" [(ngModel)]="filterName"
i18n-placeholder="@@namePlaceholder">
</mat-form-field>
<ng-container *ngIf="selectedFilter1 === 'ou'">
<mat-form-field [disabled]="selectedFilter1 === 'ou'">
<mat-label i18n="@@unitTypeLabel">Tipo de unidad</mat-label>
<mat-select [(value)]="selectedFilter2" (selectionChange)="applyFilter()">
<mat-option value="organizational-unit" i18n="@@organizationalUnitOption">Unidad organizativa</mat-option>
<mat-option value="classroom-group" i18n="@@classroomsGroupOption">Grupos de aulas</mat-option>
<mat-option value="classroom" i18n="@@classroomOption">Aulas</mat-option>
<mat-option value="client-group" i18n="@@clientGroupOption">Grupos de clientes</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label i18n="@@floorLabel" class="temp_filter">Planta</mat-label>
<mat-select [(value)]="selectedFilter1">
<mat-option value="none" i18n="@@noneOption">Ninguno</mat-option>
<mat-option value="option1" i18n="@@option1">Planta 1</mat-option>
<mat-option value="option2" i18n="@@option2">Planta 2</mat-option>
<mat-option value="option3" i18n="@@option3">Planta 3</mat-option>
</mat-select>
</mat-form-field>
</ng-container>
<!-- FILTROS CLIENTES -->
<ng-container *ngIf="selectedFilter1 === 'client'">
<mat-form-field>
<mat-label i18n="@@selectAnotherOptionLabel" class="temp_filter">Sistema Operativo</mat-label>
<mat-select multiple [(value)]="selectedFilterOS">
<mat-option value="none" i18n="@@noneOption">Ninguno</mat-option>
<mat-option value="Windows 10 Education 1803 64 bits">Windows 10 Education 1803 64 bits</mat-option>
<mat-option value="Ubuntu 18.04.1 LTS 64 bits">Ubuntu 18.04.1 LTS 64 bits</mat-option>
<mat-option value="Ubuntu 16.04.4 LTS 64 bits">Ubuntu 16.04.4 LTS 64 bits</mat-option>
<mat-option value="DATA">RESTO DE OPCIONES TBI</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label i18n="@@selectStateLabel" class="temp_filter">Estado</mat-label>
<mat-select multiple [(value)]="selectedFilterStatus">
<mat-option value="off" i18n="@@offOption">off</mat-option>
<mat-option value="initializing" i18n="@@initializingOption">initializing</mat-option>
<mat-option value="oglive" i18n="@@ogliveOption">oglive</mat-option>
<mat-option value="busy" i18n="@@busyOption">busy</mat-option>
<mat-option value="linux" i18n="@@linuxOption">linux</mat-option>
<mat-option value="linux_session" i18n="@@linuxSessionOption">linux_session</mat-option>
<mat-option value="macos" i18n="@@macosOption">macos</mat-option>
<mat-option value="windows" i18n="@@windowsOption">windows</mat-option>
<mat-option value="windows_session" i18n="@@windowsSessionOption">windows_session</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label class="temp_filter">IP</mat-label>
<input matInput placeholder="Dírección IP" (input)="applyFilter()" i18n [(ngModel)]="filterIP">
</mat-form-field>
<mat-form-field class="example-full-width">
<mat-label class="temp_filter">MAC</mat-label>
<input matInput placeholder="Dírección IP" (input)="applyFilter()" i18n [(ngModel)]="filterMAC">
</mat-form-field>
</ng-container>
<div class="button-group">
<button mat-raised-button color="primary" (click)="saveFilters()" i18n="@@saveFiltersButton" joyrideStep="saveFiltersStep" text="Guarda tus filtros seleccionados para usarlos en el futuro.">Guardar Filtros</button>
<button mat-raised-button color="accent" (click)="sendActions()" i18n="@@sendFiltersButton" [disabled]="selectedElements.length === 0" joyrideStep="sendActionStep" text="Envía una acción a los elementos seleccionados.">Enviar Acción</button>
<button mat-flat-button color="primary" [disabled]="selectedElements.length === 0" (click)="onPxeBootFile()" joyrideStep="addPxeStep" text="Añade un archivo PXE a los elementos seleccionados.">Añadir fichero PXE</button>
<button mat-raised-button color="primary" [matMenuTriggerFor]="menu" [disabled]="selectedFilter1 === 'ou'">
Asistentes
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item [disabled]="selectedElements.length > 1 || !selectedElements.length" (click)="onCommandSelect('partition')">Asistente de particionado</button>
<button mat-menu-item [disabled]="selectedElements.length > 1 || !selectedElements.length" (click)="onCommandSelect('create-image')">Crear una imagen</button>
<button mat-menu-item [disabled]="selectedElements.length > 1 || !selectedElements.length" (click)="onCommandSelect('deploy-image')">Desplegar una imagen</button>
</mat-menu>
</div>
</div>
<div class="results">
<ng-container *ngIf="filteredResults && filteredResults.length > 0; else noResults">
<ng-container *ngIf="viewMode === 'grid'">
<mat-grid-list cols="7" rowHeight="1:1">
<mat-grid-tile *ngFor="let result of filteredResults">
<mat-card class="result-card small-card" [ngClass]="{
'card-og-live': result.status === 'og-live',
'card-busy': result.status === 'busy',
'card-windows': result.status === 'windows' || result.status === 'windows-session',
'card-linux': result.status === 'linux' || result.status === 'linux-session',
'card-macos': result.status === 'macos',
'card-off': result.status === 'off'
}">
<mat-checkbox
[(ngModel)]="result.selected"
(change)="onCheckboxChange($event, result.name, result['@id'])"
class="result-checkbox">
</mat-checkbox>
<mat-card-title *ngIf="result.type !== 'client'" class="result-title">{{ result.name }}</mat-card-title>
<mat-card-content *ngIf="result.type === 'client'" class="result-content centered-content" >
<div class="client-info">
<p class="client-name">{{ result.name }}</p>
<p class="client-text">{{ result.ip }}</p>
<p class="client-text"> {{ result.mac }}</p>
</div>
</mat-card-content>
<mat-card-content *ngIf="result.type !== 'client'" class="result-content">
<p i18n="@@internalUnits" class="result-internal-units">Unidades internas: {{ result.children.length }}</p>
<p i18n="@@clients" class="result-clients">Clientes: {{ result.clients.length }}</p>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</ng-container>
<ng-container *ngIf="viewMode === 'list'">
<div class="result-list" *ngFor="let result of filteredResults">
<mat-card class="result-card-list">
<mat-checkbox [(ngModel)]="result.selected" (change)="onCheckboxChange($event, result.name, result['@id'])" class="result-checkbox"></mat-checkbox>
<mat-card-title class="result-title">{{ result.name }}</mat-card-title>
<mat-card-content class="result-content">
<p class="result-type">{{ result.type !== 'client' ? result.type : '' }}</p>
<p class="result-ip" *ngIf="result.type === 'client'">{{ result.ip }}</p>
<p class="result-mac" *ngIf="result.type === 'client'">{{ result.mac }}</p>
<p class="result-status" *ngIf="result.type === 'client'">{{ result.status }}</p>
<p *ngIf="result.type !== 'client'" i18n="@@internalUnits" class="result-internal-units">
Unidades internas: {{ result.children.length }}
</p>
<p *ngIf="result.type !== 'client'" i18n="@@clients" class="result-clients">
Clientes: {{ result.clients.length }}
</p>
</mat-card-content>
</mat-card>
</div>
</ng-container>
<div class="paginator-container">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page"
[pageSizeOptions]="pageSizeOptions" (page)="onPageChange($event)">
</mat-paginator>
</div>
</ng-container>
<ng-template #noResults>
<div class="no-results">
<p i18n="@@noResultsMessage">No hay resultados para mostrar.</p>
</div>
</ng-template>
</div>
</div>
</div>

View File

@ -1,502 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { DataService } from '../../services/data.service';
import { ClientCollection, UnidadOrganizativa } from '../../model/model';
import { MatDialog } from '@angular/material/dialog';
import { CreateOrganizationalUnitComponent } from '../../shared/organizational-units/create-organizational-unit/create-organizational-unit.component';
import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component';
import { CreateClientComponent } from '../../shared/clients/create-client/create-client.component';
import { EditOrganizationalUnitComponent } from '../../shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component';
import { EditClientComponent } from '../../shared/clients/edit-client/edit-client.component';
import { ShowOrganizationalUnitComponent} from "../../shared/organizational-units/show-organizational-unit/show-organizational-unit.component";
import {ToastrService} from "ngx-toastr";
import {TreeViewComponent} from "../../shared/tree-view/tree-view.component";
import {MatBottomSheet} from "@angular/material/bottom-sheet";
import {LegendComponent} from "../../shared/legend/legend.component";
import { ClassroomViewDialogComponent } from '../../shared/classroom-view/classroom-view-modal';
import {HttpClient} from "@angular/common/http";
import {PageEvent} from "@angular/material/paginator";
import { SaveFiltersDialogComponent } from '../../shared/save-filters-dialog/save-filters-dialog.component';
import { AcctionsModalComponent } from '../../shared/acctions-modal/acctions-modal.component';
import {MatTableDataSource} from "@angular/material/table";
import {DatePipe} from "@angular/common";
import { Router } from '@angular/router';
import {
CreatePxeBootFileComponent
} from "../../../ogboot/pxe-boot-files/create-pxeBootFile/create-pxe-boot-file/create-pxe-boot-file.component";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-advanced-search',
templateUrl: './advanced-search.component.html',
styleUrl: './advanced-search.component.css'
})
export class AdvancedSearchComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
dataSource = new MatTableDataSource<any>();
organizationalUnits: UnidadOrganizativa[] = [];
selectedUnidad: UnidadOrganizativa | null = null;
selectedDetail: any | null = null;
children: any[] = [];
breadcrumb: string[] = [];
clientsData: any[] = [];
breadcrumbData: any[] = [];
loading:boolean = false;
loadingChildren:boolean = false;
searchTerm: string = '';
selectedFilter1: string = 'none';
selectedFilter2: string = 'none';
selectedFilterOS: string[] = [];
selectedFilterStatus: string[] = [];
filterIP: string = '';
filterMAC: string = '';
filterName: string = '';
filteredResults: any[] = [];
savedFilterNames: any[] = [];
length: number = 0;
itemsPerPage: number = 10;
page: number = 0;
pageSizeOptions: number[] = [5, 10, 25, 100];
selectedElements: any[] = [];
isAllSelected: boolean = false;
filters: { [key: string]: string } = {};
datePipe: DatePipe = new DatePipe('es-ES');
viewMode: 'grid' | 'list' = 'grid';
constructor(
private dataService: DataService,
public dialog: MatDialog,
private toastService: ToastrService,
private _bottomSheet: MatBottomSheet,
private http: HttpClient,
private router: Router,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
this.search();
this.getFilters();
}
changeViewMode(mode: 'grid' | 'list'): void {
this.viewMode = mode;
}
getFilters(): void {
this.dataService.getFilters().subscribe(
data => {
this.savedFilterNames = data.map((filter: any) => [filter.name, filter.uuid]);
},
error => {
console.error('Error fetching filters:', error);
}
);
}
search(): void {
this.loading = true;
this.dataService.getOrganizationalUnits(this.searchTerm).subscribe(
data => {
this.organizationalUnits = data;
this.loading = false;
},
error => {
console.error('Error fetching unidades organizativas', error);
this.loading = false;
}
);
}
onSelectUnidad(unidad: UnidadOrganizativa): void {
this.selectedUnidad = unidad;
this.selectedDetail = unidad;
this.breadcrumb = [unidad.name];
this.breadcrumbData = [unidad];
this.loadChildrenAndClients(unidad.id);
}
onSelectChild(child: any): void {
this.selectedDetail = child;
if (child.type !== 'client' && child.uuid && child.id) {
this.breadcrumb.push(child.name || child.name);
this.breadcrumbData.push(child);
this.loadChildrenAndClients(child.id);
}
}
navigateToBreadcrumb(index: number): void {
this.breadcrumb = this.breadcrumb.slice(0, index + 1);
const target = this.breadcrumbData[index];
this.breadcrumbData = this.breadcrumbData.slice(0, index + 1);
if (target.type === 'client') {
this.selectedDetail = target;
} else {
this.loadChildrenAndClients(target.id);
}
}
loadChildrenAndClients(id: string): void {
this.loadingChildren = true
this.dataService.getChildren(id).subscribe(
childrenData => {
this.dataService.getClients(id).subscribe(
clientsData => {
this.clientsData = clientsData;
const newChildren = [...childrenData, ...clientsData];
if (newChildren.length > 0) {
this.children = newChildren;
} else {
this.children = [];
}
this.loadingChildren = false
},
error => {
console.error('Error fetching clients', error);
this.clientsData = [];
this.children = [];
this.loadingChildren = false
}
);
},
error => {
console.error('Error fetching children', error);
this.children = [];
this.loadingChildren = false
}
);
}
addOU(event: MouseEvent, parent:any = null): void {
event.stopPropagation();
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px'});
dialogRef.afterClosed().subscribe(() => {
this.dataService.getOrganizationalUnits().subscribe(
data => {
this.organizationalUnits = data
this.loadChildrenAndClients(parent.id);
},
error => console.error('Error fetching unidades organizativas', error)
);
});
}
addClient(event: MouseEvent, organizationalUnit:any = null): void {
event.stopPropagation();
const dialogRef = this.dialog.open(CreateClientComponent, { data: { organizationalUnit }, width: '900px'});
dialogRef.afterClosed().subscribe(() => {
this.dataService.getOrganizationalUnits().subscribe(
data => {
this.organizationalUnits = data
this.loadChildrenAndClients(organizationalUnit.id);
},
error => console.error('Error fetching unidades organizativas', error)
);
});
}
onDeleteClick(event: MouseEvent, uuid: string, name: string, type: string): void {
event.stopPropagation();
if (type === 'client') {
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '400px',
data: { name }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.dataService.deleteElement(uuid, type).subscribe(
() => {
this.loadChildrenAndClients(this.selectedUnidad?.id || '');
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
error => console.error('Error fetching unidades organizativas', error)
);
this.openSnackBar(false, 'Entidad eliminada exitosamente')
},
error => {
console.error('Error deleting element', error)
this.openSnackBar(true, error.error['hydra:description'])
}
);
}
});
} else {
const dialogDeleteGroupRef = this.dialog.open(DeleteModalComponent, {
width: '400px',
data: { name }
});
dialogDeleteGroupRef.afterClosed().subscribe(result => {
if (result && result === 'delete') {
this.dataService.deleteElement(uuid, type).subscribe(
() => {
this.loadChildrenAndClients(this.selectedUnidad?.id || '');
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
error => console.error('Error fetching unidades organizativas', error)
);
this.openSnackBar(false, 'Entidad eliminada exitosamente')
},
error => {
console.error('Error deleting element', error)
this.openSnackBar(true, error.error['hydra:description'])
}
);
} else if (result && result === 'change') {
this.dataService.changeParent(uuid).subscribe(
() => {
this.loadChildrenAndClients(this.selectedUnidad?.id || '');
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
error => console.error('Error fetching unidades organizativas', error)
);
this.openSnackBar(false, 'Entidad eliminada exitosamente')
},
error => {
console.error('Error deleting element', error)
this.openSnackBar(true, error.error['hydra:description'])
}
);
}
});
}
}
onEditClick(event: MouseEvent, type: any, uuid: string): void {
event.stopPropagation();
if (type != "client") {
const dialogRef = this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px'});
} else {
console.log('Editar cliente');
const dialogRef = this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' } );
}
}
onShowClick(event: MouseEvent, data: any): void {
event.stopPropagation();
if (data.type != "client") {
const dialogRef = this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px'});
}
}
onTreeClick(event: MouseEvent, data: any): void {
event.stopPropagation();
if (data.type != "client") {
const dialogRef = this.dialog.open(TreeViewComponent, { data: { data }, width: '800px'});
}
}
openSnackBar(isError: boolean, message: string) {
if (isError) {
this.toastService.error(' Error al eliminar la entidad: ' + message, 'Error');
} else
this.toastService.success(message, 'Éxito');
}
openBottomSheet(): void {
this._bottomSheet.open(LegendComponent);
}
roomMap(): void {
if (this.selectedDetail && this.selectedDetail.type === 'classroom') {
const dialogRef = this.dialog.open(ClassroomViewDialogComponent, {
width: '90vw',
data: { clients: this.clientsData }
});
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
});
}
}
applyFilter() {
this.dataService.getFilteredResults(this.selectedFilter1, this.selectedFilter2, this.filterName, this.filterIP, this.filterMAC, this.page + 1, this.itemsPerPage)
.subscribe(
response => {
this.filteredResults = response.results;
this.length = response.total;
},
error => {
console.error('Error al obtener los resultados filtrados', error);
this.filteredResults = [];
}
);
}
onPageChange(event: PageEvent) {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.length = event.length;
this.applyFilter();
}
saveFilters() {
const dialogRef = this.dialog.open(SaveFiltersDialogComponent);
dialogRef.afterClosed().subscribe(result => {
if (result) {
const filters = {
name: result,
favourite: true,
filters: {
filter0: this.filterName,
filter1: this.selectedFilter1,
filter2: this.selectedFilter2,
filter3: this.selectedFilterOS,
filter4: this.selectedFilterStatus,
filter5: this.filterIP,
filter6: this.filterMAC,
}
};
this.http.post(`${this.baseUrl}/views`, filters).subscribe(response => {
console.log('Response from server:', response);
this.toastService.success('Se ha guardado el filtro correctamente');
}, error => {
console.error('Error:', error);
this.toastService.error(error);
});
}
});
}
loadSelectedFilter(savedFilter: any) {
const url = `${this.baseUrl}/views/` + savedFilter[1];
console.log('llamando a:', url);
this.dataService.getFilter(savedFilter[1]).subscribe(response => {
console.log('Response from server:', response.filters);
if (response) {
console.log('Filter1:', response.filters);
this.filterName = response.filters.filter0 || '';
this.selectedFilter1 = response.filters.filter1 || null;
this.selectedFilter2 = response.filters.filter2 || '';
this.selectedFilterOS = response.filters.filter3 || [];
this.selectedFilterStatus = response.filters.filter4 || [];
this.filterIP = response.filters.filter5 || '';
this.filterMAC = response.filters.filter6 || '';
this.applyFilter();
}
}, error => {
console.error('Error:', error);
});
}
onCheckboxChange(event: any, name: string, uuid: string) {
if (event.checked) {
if (!this.selectedElements.includes(uuid)) {
this.selectedElements.push(uuid);
}
} else {
const index = this.selectedElements.indexOf(uuid);
if (index > -1) {
this.selectedElements.splice(index, 1);
}
}
this.isAllSelected = this.filteredResults.every(result =>
this.selectedElements.includes(result['@id'])
);
}
toggleSelectAll() {
this.isAllSelected = !this.isAllSelected;
if (this.isAllSelected) {
this.selectedElements = this.filteredResults.map(result => result['@id']);
} else {
this.selectedElements = [];
}
this.filteredResults.forEach(result => {
result.selected = this.isAllSelected;
});
}
isSelected(name: string): boolean {
return this.selectedElements.includes(name);
}
sendActions() {
const dialogRef = this.dialog.open(AcctionsModalComponent, { data: { selectedElements: this.selectedElements }, width: '700px'});
}
onPxeBootFile(): void {
const dialog = this.dialog.open(CreatePxeBootFileComponent, { data: this.selectedElements, width: '400px' });
dialog.afterClosed().subscribe(() => {
this.dialog.closeAll();
});
}
onCommandSelect(action: any): void {
if (action === 'partition') {
this.openPartitionAssistant();
}
if (action === 'create-image') {
this.openCreateImageAssistant();
}
if (action === 'deploy-image') {
this.openDeployImageAssistant();
}
}
openPartitionAssistant(): void {
const client = this.selectedElements[0];
console.log(client)
this.router.navigate([`${client}/partition-assistant`]).then(r => {
console.log('navigated', r);
});
}
openCreateImageAssistant(): void {
const client = this.selectedElements[0];
this.router.navigate([`${client}/create-image`]).then(r => {
console.log('navigated', r);
});
}
openDeployImageAssistant(): void {
const client = this.selectedElements[0];
this.router.navigate([`${client}/deploy-image`]).then(r => {
console.log('navigated', r);
});
}
onDobleClick(event: MouseEvent, data: any, type: string): void {
if (type === 'client') {
this.router.navigate(['client', data]);
}
else {
}
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'title2Step',
'filterSelectionStep',
'viewModeStep',
'filtersStep',
'selectAllStep',
'saveFiltersStep',
'sendActionStep',
'addPxeStep',
'resultsStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -257,5 +257,7 @@
text-align: center;
}
.back-button {
margin-left: 10px;
}

View File

@ -12,6 +12,11 @@
</mat-menu>
</div>
<button mat-raised-button color="primary" (click)="navigateToGroups()" class="back-button">
<mat-icon>arrow_back</mat-icon>
{{ 'Back' | translate }}
</button>
<div *ngIf="loading" class="loading-container">
<mat-spinner></mat-spinner>
</div>

View File

@ -122,6 +122,11 @@ export class ClientMainViewComponent implements OnInit {
}
});
}
navigateToGroups() {
this.router.navigate(['/groups']);
}
updateGeneralData() {
this.generalData = [
{ property: 'Nombre', value: this.clientData?.name || '' },
@ -198,7 +203,7 @@ export class ClientMainViewComponent implements OnInit {
}
loadPartitions(): void {
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[partitionNumber]=ASC`).subscribe({
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[diskNumber, partitionNumber]=ASC`).subscribe({
next: data => {
this.dataSource = data['hydra:member'];
this.partitions = data['hydra:member'];

View File

@ -1,4 +1,5 @@
<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="subnets-button-row">
<button mat-flat-button color="primary" (click)="save()">Guardar y ejecutar</button>

View File

@ -16,7 +16,6 @@ import { ActivatedRoute } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { ToastrModule, ToastrService } from 'ngx-toastr';
import { of } from 'rxjs';
import { DataService } from '../../client-tab-view/data.service';
describe('CreateImageComponent', () => {
let component: CreateImageComponent;
@ -42,7 +41,6 @@ describe('CreateImageComponent', () => {
providers: [
FormBuilder,
ToastrService,
DataService,
provideHttpClient(),
provideHttpClientTesting(),
{

View File

@ -68,6 +68,7 @@ export class CreateImageComponent {
selectedImage: string | null = null;
selectedPartition: any = null;
name: string = '';
client: any = null;
dataSource = new MatTableDataSource<any>();
columns = [
{
@ -120,6 +121,7 @@ export class CreateImageComponent {
this.http.get(url).subscribe(
(response: any) => {
if (response.partitions) {
this.client = response;
this.clientName = response.name;
this.dataSource.data = response.partitions.filter((partition: any) => {
@ -145,6 +147,10 @@ export class CreateImageComponent {
);
}
back() {
this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
}
save(): void {
const payload = {
client: `/clients/${this.clientId}`,

View File

@ -1,4 +1,5 @@
<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="subnets-button-row">
<button mat-flat-button color="primary" (click)="save()">Guardar</button>
@ -54,9 +55,9 @@
</table>
<mat-divider></mat-divider>
<h3 *ngIf="isMethod('multicast')" class="input-group">Opciones multicast</h3>
<h3 *ngIf="isMethod('torrent')" class="input-group">Opciones torrent</h3>
<div *ngIf="isMethod('multicast')" class="input-group">
<h3 *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">Opciones multicast</h3>
<h3 *ngIf="isMethod('p2p')" class="input-group">Opciones torrent</h3>
<div *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">
<mat-form-field appearance="fill" class="input-field">
<mat-label>Puerto</mat-label>
<input matInput [(ngModel)]="mcastPort" name="mcastPort">
@ -92,7 +93,7 @@
</mat-form-field>
</div>
<div *ngIf="isMethod('torrent')" class="input-group">
<div *ngIf="isMethod('p2p')" class="input-group">
<mat-form-field appearance="fill" class="input-field">
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
<mat-select [(ngModel)]="p2pMode" name="p2pMode">

View File

@ -15,7 +15,6 @@ import { MatRadioModule } from '@angular/material/radio'; // Importar MatRadioMo
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { ToastrModule, ToastrService } from 'ngx-toastr';
import { DataService } from '../../client-tab-view/data.service';
import { provideRouter } from '@angular/router';
import { MatSelectModule } from '@angular/material/select';
@ -28,7 +27,7 @@ describe('DeployImageComponent', () => {
declarations: [DeployImageComponent],
imports: [
ReactiveFormsModule,
FormsModule,
FormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
@ -36,7 +35,7 @@ describe('DeployImageComponent', () => {
MatButtonModule,
MatTableModule,
MatDividerModule,
MatRadioModule,
MatRadioModule,
MatSelectModule,
BrowserAnimationsModule,
ToastrModule.forRoot(),
@ -45,7 +44,6 @@ describe('DeployImageComponent', () => {
providers: [
FormBuilder,
ToastrService,
DataService,
provideHttpClient(),
provideHttpClientTesting(),
provideRouter([]),

View File

@ -32,6 +32,7 @@ export class DeployImageComponent {
p2pMode: string = '';
p2pTime: Number = 0;
name: string = '';
client: any = null;
protected p2pModeOptions = [
{ name: 'Leecher', value: 'p2p-mode-leecher' },
@ -39,22 +40,25 @@ export class DeployImageComponent {
{ name: 'Seeder', value: 'p2p-mode-seeder' },
];
protected multicastModeOptions = [
{"name": 'Half duplex', "value": "half-duplex"},
{"name": 'Full duplex', "value": "full-duplex"},
{ name: 'Half duplex', value: "half"},
{ name: 'Full duplex', value: "full"},
];
allMethods = [
'multicast',
'uftp',
'udpcast',
'multicast-direct',
'unicast',
'unicast-direct',
'torrent'
'p2p'
];
updateCacheMethods = [
'uftp',
'udpcast',
'multicast',
'unicast',
'torrent'
'p2p'
];
dataSource = new MatTableDataSource<any>();
@ -116,6 +120,7 @@ export class DeployImageComponent {
this.http.get(url).subscribe(
(response: any) => {
if (response.partitions) {
this.client = response;
this.clientName = response.name;
this.dataSource.data = response.partitions.filter((partition: any) => {
return partition.partitionNumber !== 0;
@ -146,6 +151,10 @@ export class DeployImageComponent {
);
}
back() {
this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
}
save(): void {
if (!this.selectedImage) {
this.toastService.error('Debe seleccionar una imagen');
@ -177,8 +186,7 @@ export class DeployImageComponent {
this.http.post(`${this.baseUrl}${this.selectedImage}/deploy-image`, payload)
.subscribe({
next: (response) => {
this.toastService.success('Imagen creada exitosamente');
this.router.navigate(['/commmands-logs'])
this.toastService.success('Petición de despliegue enviada correctamente');
},
error: (error) => {
console.error('Error:', error);

View File

@ -162,3 +162,8 @@ button.remove-btn:hover {
align-content: center;
justify-self: center;
}
.disk-select {
padding: 20px;
margin: 10px auto;
}

View File

@ -1,21 +1,28 @@
<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="subnets-button-row">
<button mat-flat-button color="primary" (click)="save()">Ejecutar</button>
<button mat-flat-button color="primary" [disabled]="data.status === 'busy'" (click)="save()">Ejecutar</button>
</div>
</div>
<mat-divider></mat-divider>
<mat-dialog-content>
<div class="partition-assistant" *ngFor="let disk of disks; let i = index">
<div class="header">
<label for="disk-number-{{ i }}">Disco {{ disk.diskNumber }}:</label>
<span class="disk-size">Tamaño: {{ (disk.totalDiskSize / 1024).toFixed(2) }} GB</span>
</div>
<div class="disk-select">
<mat-form-field appearance="fill">
<mat-label>Seleccionar disco</mat-label>
<mat-select [(ngModel)]="selectedDiskNumber">
<mat-option *ngFor="let disk of disks" [value]="disk.diskNumber">
Disco {{ disk.diskNumber }} ({{ (disk.totalDiskSize / 1024).toFixed(2) }} GB)
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="partition-assistant" *ngIf="selectedDisk">
<div class="partition-bar">
<div
*ngFor="let partition of activePartitions(disk.diskNumber)"
*ngFor="let partition of activePartitions(selectedDisk.diskNumber)"
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
class="partition-segment"
>
@ -23,7 +30,7 @@
</div>
</div>
<div class="row">
<button mat-flat-button color="primary" (click)="addPartition(disk.diskNumber)">Añadir partición</button>
<button mat-flat-button color="primary" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
</div>
<div class="row">
@ -41,7 +48,7 @@
</tr>
</thead>
<tbody>
<ng-container *ngFor="let partition of disk.partitions; let j = index">
<ng-container *ngFor="let partition of selectedDisk.partitions; let j = index">
<tr *ngIf="!partition.removed">
<td>{{ partition.partitionNumber }}</td>
<td>
@ -62,21 +69,21 @@
<input
type="number"
[(ngModel)]="partition.size" required
(input)="updatePartitionSize(disk.diskNumber, j, partition.size)"
(input)="updatePartitionSize(selectedDisk.diskNumber, j, partition.size)"
/>
</td>
<td>
<input
type="number"
[(ngModel)]="partition.percentage"
(input)="updatePartitionSizeFromPercentage(disk.diskNumber, j, partition.percentage)"
(input)="updatePartitionSizeFromPercentage(selectedDisk.diskNumber, j, partition.percentage)"
/>
</td>
<td>
<input type="checkbox" [(ngModel)]="partition.format" />
</td>
<td>
<button (click)="removePartition(disk.diskNumber, partition)" class="remove-btn">X</button>
<button (click)="removePartition(selectedDisk.diskNumber, partition)" class="remove-btn">X</button>
</td>
</tr>
</ng-container>
@ -87,7 +94,7 @@
<div class="chart-container">
<ngx-charts-pie-chart
[view]="view"
[results]="disk.chartData"
[results]="selectedDisk.chartData"
[doughnut]="true"
>
</ngx-charts-pie-chart>
@ -96,5 +103,4 @@
</div>
</mat-dialog-content>
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>

View File

@ -35,6 +35,7 @@ export class PartitionAssistantComponent implements OnInit {
originalPartitions: any[] = [];
clientId: string | null = null;
newPartitions: any[] = [];
selectedDiskNumber: number | null = null;
updateRequests: any[] = [];
data: any = {};
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = [];
@ -57,6 +58,10 @@ export class PartitionAssistantComponent implements OnInit {
this.loadPartitions();
}
get selectedDisk():any {
return this.disks.find(disk => disk.diskNumber === this.selectedDiskNumber) || null;
}
loadPartitions() {
const url = `${this.baseUrl}/clients/${this.clientId}`;
this.http.get(url).subscribe(
@ -245,60 +250,62 @@ export class PartitionAssistantComponent implements OnInit {
return modifiedPartitions;
}
save() {
const invalidDisks = this.disks.filter(disk => {
const totalPartitionSize = disk.partitions.reduce((sum, partition) => sum + partition.size, 0);
return totalPartitionSize > disk.totalDiskSize;
});
back() {
this.router.navigate(['clients', this.data.uuid], { state: { clientData: this.data } });
}
console.log(invalidDisks);
if (invalidDisks.length > 0) {
this.errorMessage = 'El tamaño total de las particiones en uno o más discos excede el tamaño total del disco.';
save() {
if (!this.selectedDisk) {
this.errorMessage = 'Por favor selecciona un disco antes de guardar.';
return;
}
const totalPartitionSize = this.selectedDisk.partitions.reduce((sum: any, partition: { size: any; }) => sum + partition.size, 0);
if (totalPartitionSize > this.selectedDisk.totalDiskSize) {
this.errorMessage = 'El tamaño total de las particiones en el disco seleccionado excede el tamaño total del disco.';
return;
} else {
this.errorMessage = '';
}
const modifiedPartitions = this.getModifiedOrNewPartitions();
const modifiedPartitions = this.selectedDisk.partitions.filter((partition: { removed: any; format: any; }) => !partition.removed || partition.format);
if (modifiedPartitions.length === 0) {
this.errorMessage = 'No hay cambios para guardar.';
this.errorMessage = 'No hay cambios para guardar en el disco seleccionado.';
return;
}
modifiedPartitions.forEach(({ partition, diskNumber, partitionNumber }) => {
const payload = {
diskNumber: diskNumber,
partitionNumber: partitionNumber,
memoryUsage: partition.memoryUsage,
size: partition.size,
partitionCode: partition.partitionCode,
filesystem: partition.filesystem,
client: `/clients/${this.clientId}`,
uuid: partition.uuid,
removed: partition.removed || false,
format: partition.format || false,
};
const newPartitions = modifiedPartitions.map((partition: { partitionNumber: any; memoryUsage: any; size: any; partitionCode: any; filesystem: any; uuid: any; removed: any; format: any; }) => ({
diskNumber: this.selectedDisk.diskNumber,
partitionNumber: partition.partitionNumber,
memoryUsage: partition.memoryUsage,
size: partition.size,
partitionCode: partition.partitionCode,
filesystem: partition.filesystem,
client: `/clients/${this.clientId}`,
uuid: partition.uuid,
removed: partition.removed || false,
format: partition.format || false,
}));
this.newPartitions.push(payload);
});
if (this.newPartitions.length > 0) {
const bulkPayload = { partitions: this.newPartitions };
if (newPartitions.length > 0) {
const bulkPayload = { partitions: newPartitions };
this.http.post(this.apiUrl, bulkPayload).subscribe(
(response) => {
this.toastService.success('Particiones creadas exitosamente');
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
this.router.navigate(['/commands-logs']);
},
(error) => {
console.error('Error al crear las particiones:', error);
this.toastService.error('Error al crear las particiones');
this.toastService.error('Error al crear las particiones.');
}
);
}
}
removePartition(diskNumber: number, partition: Partition) {
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
if (disk) {

View File

@ -1,92 +0,0 @@
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 100px;
padding: 10px;
margin-top: 16px;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 5px;
box-sizing: border-box;
}
.search-string {
flex: 1;
padding: 5px;
}
.divider {
margin: 20px 0;
}
.mat-elevation-z8 {
margin-top: 30px;
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
.search-boolean {
flex: 1;
padding: 5px;
}
.search-select {
flex: 2;
padding: 5px;
}
button{
margin-left: 10px;
}
.client-info {
display: flex;
flex-direction: column;
gap: 3px;
margin: 5px;
}
.client-name {
font-size: 16px;
font-weight: bold;
}
.client-ip,
.client-mac {
font-size: 12px;
color: #666;
line-height: 1.5;
}
.chip-busy {
background-color: indianred !important;
color: black;
}
.chip-og-live {
background-color: yellow !important;
color: black;
}
.chip-windows,
.chip-windows-session,
.chip-macos {
background-color: cornflowerblue !important;
color: white;
}
.chip-linux,
.chip-linux-session {
background-color: mediumpurple !important;
color: white;
}
.chip-off {
background-color: darkgrey !important;
color: white;
}

View File

@ -1,124 +0,0 @@
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminImagesTitle" joyrideStep="clientTabtitleStep" text="En esta pantalla se podran administrar todos los clientes del sistema sin jerarquias.">{{ 'adminImagesTitle' | translate }}</h2>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="clientTabResetFiltersStep" text="Reinicia los filtros aplicados para mostrar todos los clientes.">{{ 'resetFiltersButton' | translate }}</button>
<button mat-flat-button color="primary" (click)="addClient($event)" joyrideStep="clientTabaddClientStep" text="Añade un nuevo cliente a la lista.">{{ 'addClientButton' | translate }}</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container" joyrideStep="clientTabsearchContainerStep" text="Filtra los clientes por nombre, IP, MAC o unidad organizativa.">
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">{{ 'searchClientNameLabel' | translate }}</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint i18n="@@searchHint">{{ 'searchHint' | translate }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">{{ 'searchIPLabel' | translate }}</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint i18n="@@searchHint">{{ 'searchHint' | translate }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">{{ 'searchMACLabel' | translate }}</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['mac']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint i18n="@@searchHint">{{ 'searchHint' | translate }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-select" >
<mat-label i18n="@@organizational-unit-label">{{ 'organizationalUnitLabel' | translate }}</mat-label>
<mat-select [(ngModel)]="filters['organizationalUnit.id']" (selectionChange)="search()">
<mat-option *ngFor="let unit of organizationalUnits" [value]="unit.id" >
{{ unit.name }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="!loading" class="loading-container">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="loading">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="clientTabtableStep" text="Lista de clientes filtrados por tus criterios de búsqueda.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let client" >
<ng-container *ngIf="column.columnDef === 'name'">
<div class="client-info">
<div class="client-name">{{ client.name }}</div>
<div class="client-ip">{{ client.ip }}</div>
<div class="client-mac">{{ client.mac }}</div>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef === 'status'">
<mat-chip [ngClass]="{
'chip-og-live': client.status === 'og-live',
'chip-busy': client.status === 'busy',
'chip-windows': client.status === 'windows' || client.status === 'windows-session',
'chip-linux': client.status === 'linux' || client.status === 'linux-session',
'chip-macos': client.status === 'macos',
'chip-off': client.status === 'off'
}">
{{ client.status }}
</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'status' && column.columnDef !== 'name'">
{{ column.cell(client) }}
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">
{{ 'columnActions' | translate }}
</th>
<td mat-cell *matCellDef="let client" style="text-align: center;" joyrideStep="clientTabactionsStep"
text="Acciones disponibles para cada cliente, como ver, editar o eliminar.">
<button
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
mat-icon-button color="primary"
(click)="getStatus(client)">
<mat-icon>sync</mat-icon>
</button>
<button
*ngIf="syncStatus && syncingClientId === client.uuid"
mat-icon-button color="primary">
<mat-spinner diameter="24"></mat-spinner>
</button>
<button mat-icon-button color="info" (click)="handleClientClick($event, client)">
<mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="onEditClick($event, client.uuid)" i18n="@@editImage">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="onDeleteClick($event, client)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div class="paginator-container" joyrideStep="clientTabpaginationStep" text="Navega entre las páginas de resultados utilizando el paginador.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"
[pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>
</div>

View File

@ -1,216 +0,0 @@
import { Component } from '@angular/core';
import {PageEvent} from "@angular/material/paginator";
import {DatePipe} from "@angular/common";
import {MatTableDataSource} from "@angular/material/table";
import {MatDialog} from "@angular/material/dialog";
import {ToastrService} from "ngx-toastr";
import {HttpClient} from "@angular/common/http";
import {DataService} from "./data.service";
import {EditClientComponent} from "../../shared/clients/edit-client/edit-client.component";
import {CreateClientComponent} from "../../shared/clients/create-client/create-client.component";
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import {ClientViewComponent} from "../../shared/client-view/client-view.component";
import { Router } from '@angular/router';
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-client-tab-view',
templateUrl: './client-tab-view.component.html',
styleUrl: './client-tab-view.component.css'
})
export class ClientTabViewComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
dataSource = new MatTableDataSource<any>();
length: number = 0;
loading:boolean = false;
syncStatus: boolean = false;
itemsPerPage: number = 10;
pageSizeOptions: number[] = [5, 10, 25, 100];
page: number = 0;
filters: { [key: string]: string } = {};
organizationalUnits: any[] = [];
datePipe: DatePipe = new DatePipe('es-ES');
syncingClientId: number | null = null;
private apiUrl = `${this.baseUrl}/clients`;
columns = [
{
columnDef: 'id',
header: 'ID',
cell: (client: any) => `${client.id}`
},
{
columnDef: 'name',
header: 'Nombre del cliente',
cell: (client: any) => `${client.name}`
},
{
columnDef: 'status',
header: 'Estado',
cell: (client: any) => `${client.status}`
},
{
columnDef: 'organizationalUnit',
header: 'Pertenece a',
cell: (client: any) => `${client.organizationalUnit?.name}`
},
{
columnDef: 'ogLive',
header: 'OgLive',
cell: (client: any) => `${client.ogLive?.name}`
},
{
columnDef: 'subnet',
header: 'Subred',
cell: (client: any) => `${client.subnet}`
},
{
columnDef: 'template',
header: 'Plantilla PXE',
cell: (client: any) => `${client.template?.name}`
},
];
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
constructor(
private dataService: DataService,
public dialog: MatDialog,
private toastService: ToastrService,
private http: HttpClient,
private router: Router,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
this.getClients();
this.loadOrganizationalUnits();
}
getClients() {
this.http.get<any>(`${this.apiUrl}?&page=${this.page + 1 }&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
(data) => {
this.dataSource.data = data['hydra:member'];
this.length = data['hydra:totalItems'];
},
(error) => {
console.error('Error fetching commands', error);
}
);
}
onEditClick(event: MouseEvent, uuid: string): void {
event.stopPropagation();
const dialogRef = this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' } );
dialogRef.afterClosed().subscribe(() => this.getClients());
}
addClient(event: MouseEvent, organizationalUnit:any = null): void {
event.stopPropagation();
const dialogRef = this.dialog.open(CreateClientComponent, { data: { organizationalUnit }, width: '900px'});
dialogRef.afterClosed().subscribe(() => {
this.getClients();
});
}
onDeleteClick(event: MouseEvent, client: any): void {
event.stopPropagation();
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '400px'
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.http.delete<void>(`${this.apiUrl}/${client.uuid}`).pipe(
catchError(error => {
this.toastService.error(error.error['hydra:description']);
return throwError(error);
})
).subscribe(() => {
this.toastService.success('Elemento eliminado correctamente');
this.getClients();
});
}
});
}
getStatus(client: any): void {
this.syncingClientId = client.uuid;
this.syncStatus = true;
this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
this.search();
this.syncStatus = false;
this.syncingClientId = null;
},
error => {
this.toastService.error('Error de conexión con el cliente');
this.syncStatus = false;
this.syncingClientId = null;
}
);
}
handleClientClick(event: MouseEvent, client: any): void {
event.stopPropagation();
this.router.navigate(['clients', client.uuid], { state: { clientData: client } });
}
resetFilters() {
this.loading = true;
this.filters = {};
this.getClients();
}
loadOrganizationalUnits() {
this.loading = true;
this.http.get<any>( `${this.baseUrl}/organizational-units?&page=1&itemsPerPage=10000`, {
params: {
'groups[]': ['organizational-unit:read:collection:short']
}
}).subscribe(
response => {
this.organizationalUnits = response['hydra:member'];
this.loading = false;
},
error => {
console.error('Error fetching parent units:', error);
this.loading = false;
}
);
}
search(): void {
this.loading = true;
this.getClients()
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.length = event.length;
this.getClients();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'clientTabtitleStep',
'clientTabResetFiltersStep',
'clientTabaddClientStep',
'clientTabsearchContainerStep',
'clientTabtableStep',
'clientTabactionsStep',
'clientTabpaginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -1,35 +0,0 @@
import { Injectable } from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DataService {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl = `${this.baseUrl}/organizational-units`;
private clientsUrl = `${this.baseUrl}/clients`;
constructor(private http: HttpClient) {}
getClients(itemsPerPage: number = 10, page: number = 1): Observable<any> {
return this.http.get<any>(this.clientsUrl, {
params: new HttpParams().set('itemsPerPage', itemsPerPage.toString()).set('page', page.toString())
})
.pipe(
map(response => {
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
return response['hydra:member']
} else {
throw new Error('Unexpected response format');
}
}),
catchError(error => {
console.error('Error fetching clients', error);
return throwError(error);
})
);
}
}

View File

@ -1,55 +0,0 @@
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 100px;
padding: 10px;
margin-top: 16px;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 5px;
box-sizing: border-box;
}
.search-string {
flex: 1;
padding: 5px;
}
.search-boolean {
flex: 1;
padding: 5px;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
.search-select {
flex: 2;
padding: 5px;
}
.mat-chip-success {
background-color: #4CAF50 !important;
color: white !important;
align-items: center;
vertical-align: middle;
}
.button-row{
display: flex;
gap: 10px;
}
.calendar-ico{
margin-top: 5px;
color: gray;
}

View File

@ -1,82 +0,0 @@
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" i18n="@@adminImagesTitle" joyrideStep="titleS4tep" text="Gestiona las unidades organizativas desde esta pantalla.">{{ 'resetFiltersButton' | translate }}</h2>
<div class="button-row">
<button mat-flat-button color="primary" (click)="resetFilters()" joyrideStep="resetFiltersStep" text="Reinicia los filtros aplicados para mostrar todas las unidades organizativas.">Reiniciar filtros</button>
<button mat-flat-button color="primary" (click)="addOrganizationalUnit($event)" joyrideStep="addOUStep" text="Añade una nueva unidad organizativa.">{{ 'addOUButton' | translate }}</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container" joyrideStep="searchContainerStep" text="Filtra las unidades organizativas por nombre o tipo.">
<mat-form-field appearance="fill" class="search-string">
<mat-label i18n="@@searchLabel">{{ 'searchLabelOu' | translate }}</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint i18n="@@searchHint">{{ 'searchHint' | translate }}</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-boolean">
<mat-label i18n="@@searchLabel">{{ 'typeLabel' | translate }}</mat-label>
<mat-select [(ngModel)]="filters['type']" (selectionChange)="search()" placeholder="Seleccionar opción">
<mat-option [value]="''">{{ 'allOption' | translate }}</mat-option>
<mat-option [value]="'organizational-unit'">{{ 'centerOption' | translate }}</mat-option>
<mat-option [value]="'classrooms-group'">{{ 'classroomsGroupOption' | translate }}</mat-option>
<mat-option [value]="'classroom'">{{ 'classroomOption' | translate }}</mat-option>
<mat-option [value]="'clients-group'">{{ 'clientsGroupOption' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="Aquí se muestran las unidades organizativas según los filtros aplicados.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let ou">
<ng-container *ngIf="column.columnDef !== 'available' && column.columnDef !== 'type'">
{{ column.cell(ou) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'available'">
<mat-chip *ngIf="ou.available" class="mat-chip-success"><mat-icon class="calendar-ico">event_available</mat-icon></mat-chip>
<mat-chip *ngIf="!ou.available" class="mat-chip-error"><mat-icon class="calendar-ico">event_busy</mat-icon></mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef === 'type'">
<mat-chip>{{ ou.type }}</mat-chip>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">{{ 'columnActions' | translate }}</th>
<td mat-cell *matCellDef="let ou" style="text-align: center;" joyrideStep="actionsStep" text="Usa estas opciones para ver, editar o eliminar una unidad organizativa.">
<button mat-icon-button color="info" (click)="onShowClick($event, ou)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" (click)="onEditClick($event, ou.uuid)" i18n="@@editImage"><mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="onDeleteClick($event, ou)"><mat-icon>delete</mat-icon></button>
<button mat-icon-button color="info" [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item [disabled]="ou.type !== 'classroom'" (click)="roomMap(ou)">
<span i18n="@@viewTreeMenu"> {{ 'roomMapOption' | translate }}</span>
</button>
<button mat-menu-item [disabled]="ou.type !== 'organizational-unit'" (click)="onTreeClick(ou)">
<span i18n="@@viewTreeMenu">{{ 'viewTreeMenu' | translate }}</span>
</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" joyrideStep="paginationStep" text="Navega entre las páginas de unidades organizativas con el paginador.">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"
[pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>

View File

@ -1,191 +0,0 @@
import { Component } from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {DatePipe} from "@angular/common";
import {DataService} from "../client-tab-view/data.service";
import {MatDialog} from "@angular/material/dialog";
import {ToastrService} from "ngx-toastr";
import {HttpClient} from "@angular/common/http";
import {EditClientComponent} from "../../shared/clients/edit-client/edit-client.component";
import {CreateClientComponent} from "../../shared/clients/create-client/create-client.component";
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
import {catchError} from "rxjs/operators";
import {throwError} from "rxjs";
import {
CreateOrganizationalUnitComponent
} from "../../shared/organizational-units/create-organizational-unit/create-organizational-unit.component";
import {
ShowOrganizationalUnitComponent
} from "../../shared/organizational-units/show-organizational-unit/show-organizational-unit.component";
import {
EditOrganizationalUnitComponent
} from "../../shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component";
import {ClassroomViewDialogComponent} from "../../shared/classroom-view/classroom-view-modal";
import {TreeViewComponent} from "../../shared/tree-view/tree-view.component";
import { JoyrideService } from 'ngx-joyride';
@Component({
selector: 'app-organizational-unit-tab-view',
templateUrl: './organizational-unit-tab-view.component.html',
styleUrl: './organizational-unit-tab-view.component.css'
})
export class OrganizationalUnitTabViewComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
dataSource = new MatTableDataSource<any>();
length: number = 0;
loading:boolean = false;
itemsPerPage: number = 10;
pageSizeOptions: number[] = [5, 10, 25, 100];
page: number = 0;
filters: { [key: string]: string } = {};
organizationalUnits: any[] = [];
datePipe: DatePipe = new DatePipe('es-ES');
private apiUrl = `${this.baseUrl}/organizational-units`;
columns = [
{
columnDef: 'id',
header: 'ID',
cell: (ou: any) => `${ou.id}`
},
{
columnDef: 'name',
header: 'Nombre',
cell: (ou: any) => `${ou.name}`
},
{
columnDef: 'type',
header: 'Tipo',
cell: (ou: any) => `${ou.type}`
},
{
columnDef: 'remoteCalendar',
header: 'Calendario',
cell: (ou: any) => `${ou.remoteCalendar ? ou.remoteCalendar.name : '-'}`
},
{
columnDef: 'available',
header: 'Disponible remotePC',
cell: (ou: any) => `${ou.available ? 'No' : 'Si'}`
},
{
columnDef: 'createdAt',
header: 'Fecha de creación',
cell: (ou: any) => `${this.datePipe.transform(ou.createdAt, 'dd/MM/yyyy hh:mm:ss')}`
}
];
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
constructor(
private dataService: DataService,
public dialog: MatDialog,
private toastService: ToastrService,
private http: HttpClient,
private joyrideService: JoyrideService
) {}
ngOnInit(): void {
this.getOrganizationalUnits();
}
roomMap(room: any): void {
console.log(room)
const dialogRef = this.dialog.open(ClassroomViewDialogComponent, {
width: '90vw',
data: { clients: room.clients }
});
}
onTreeClick(data: any): void {
const dialogRef = this.dialog.open(TreeViewComponent, { data: { data }, width: '800px'});
}
getOrganizationalUnits() {
this.http.get<any>(`${this.apiUrl}?&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
(data) => {
this.dataSource.data = data['hydra:member'];
this.length = data['hydra:totalItems'];
},
(error) => {
console.error('Error fetching ou', error);
}
);
}
onShowClick(event: MouseEvent, data: any): void {
event.stopPropagation();
if (data.type != "client") {
const dialogRef = this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px'});
}
}
onEditClick(event: MouseEvent, uuid: string): void {
event.stopPropagation();
const dialogRef = this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '700px'});
dialogRef.afterClosed().subscribe(() => this.getOrganizationalUnits());
}
addOrganizationalUnit(event: MouseEvent, organizationalUnit:any = null): void {
event.stopPropagation();
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { organizationalUnit }, width: '900px'});
dialogRef.afterClosed().subscribe(() => {
this.getOrganizationalUnits();
});
}
onDeleteClick(event: MouseEvent, client: any): void {
event.stopPropagation();
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '400px'
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.http.delete<void>(`${this.apiUrl}/${client.uuid}`).pipe(
catchError(error => {
this.toastService.error(error.error['hydra:description']);
return throwError(error);
})
).subscribe(() => {
this.toastService.success('Elemento eliminado correctamente');
this.getOrganizationalUnits();
});
}
});
}
resetFilters() {
this.loading = true;
this.filters = {};
this.getOrganizationalUnits();
}
search(): void {
this.loading = true;
this.getOrganizationalUnits()
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.length = event.length;
this.getOrganizationalUnits();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleS4tep',
'resetFiltersStep',
'addOUStep',
'searchContainerStep',
'tableStep',
'actionsStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -1,9 +1,3 @@
.card-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding: 20px;
}
.header-container {
display: flex;
@ -14,6 +8,12 @@
border-bottom: 1px solid #ddd;
}
.header-container-title {
flex-grow: 1;
text-align: left;
margin-left: 1em;
}
.groups-button-row {
display: flex;
gap: 15px;
@ -189,10 +189,6 @@ mat-tree {
padding: 10px;
}
button {
margin: 5px;
}
mat-tree mat-tree-node {
display: flex;
align-items: center;
@ -432,7 +428,7 @@ mat-tree mat-tree-node.disabled:hover {
flex-direction: column;
align-items: center;
text-align: center;
padding: 15px;
padding: 2px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
@ -447,10 +443,6 @@ mat-tree mat-tree-node.disabled:hover {
margin-bottom: 15px;
}
.client-details {
margin-top: 10px;
}
.client-name {
display: block;
font-size: 1.2em;
@ -471,8 +463,8 @@ button[mat-raised-button] {
.clients-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 8px; /* Espaciado reducido entre cards */
}
.clients-list {
@ -495,12 +487,6 @@ button[mat-raised-button] {
flex-direction: column;
}
.view-toggle-container {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.clients-grid {
display: flex;
flex-wrap: wrap;
@ -520,35 +506,16 @@ button[mat-raised-button] {
border-radius: 5px;
}
.client-card {
display: flex;
flex-direction: column;
align-items: center;
}
.client-image {
width: 50px;
height: 50px;
margin-bottom: 0.5rem;
}
.header-actions-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.back-button {
flex-shrink: 0;
}
.view-toggle-container {
display: flex;
gap: 1rem;
align-items: center;
}
.filters-container {
display: flex;
flex-wrap: wrap;
@ -584,3 +551,47 @@ button[mat-raised-button] {
align-items: center;
}
.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-image {
max-width: 100%;
height: auto;
margin-top: 10px;
margin-bottom: 8px;
}
.client-details {
margin-top: 4px;
}
.clients-view-header {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
align-items: center;
}
.clients-title-name {
font-size: x-large;
display: block;
padding: 1rem 1rem 1rem 15px;
}
.no-clients-info {
display: flex;
align-items: center;
gap: 10px;
margin-top: 1.5rem;
}

View File

@ -1,18 +1,24 @@
<!-- Header -->
<div class="header-container" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
{{ 'adminGroupsTitle' | translate }}
</h2>
<div class="header-container-title">
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
{{ 'adminGroupsTitle' | translate }}
</h2>
</div>
<div class="groups-button-row" joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}">
<button mat-flat-button color="primary" (click)="addOU($event)"
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
{{ 'newOrganizationalUnitButton' | translate }}
</button>
<button mat-flat-button color="primary" (click)="addClient($event)" matTooltipShowDelay="1000">
{{ 'newClientButton' | translate }}
</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>
</mat-menu>
<button mat-flat-button (click)="openBottomSheet()" joyrideStep="keyStep" text="{{ 'keyStepText' | translate }}"
matTooltipShowDelay="1000">
{{ 'legendButton' | translate }}
@ -20,7 +26,8 @@
</div>
</div>
<mat-expansion-panel *ngIf="isTreeViewActive" class="filters-panel">
<!-- 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>
@ -49,178 +56,131 @@
<mat-option value="group">{{ 'computerGroups' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</mat-expansion-panel>
<div *ngIf="!selectedUnidad; else detailsTemplate" class="card-container">
<mat-card *ngFor="let unidad of organizationalUnits"
[ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}"
(click)="onSelectUnidad(unidad)" class="unidad-card small-card">
<mat-card-header>
<mat-card-title>
<mat-icon>apartment</mat-icon> {{ unidad.name }}
</mat-card-title>
</mat-card-header>
<mat-card-actions>
<div class="button-container">
<button mat-raised-button color="primary" [matMenuTriggerFor]="menu" (click)="$event.stopPropagation()">
<mat-icon>menu</mat-icon>
{{ 'Menu' | translate }}
<!-- 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)">
<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>
<mat-icon class="node-icon {{ node.type }}">
{{
node.type === 'organizational-unit' ? 'apartment'
: node.type === 'classrooms-group' ? 'meeting_room'
: node.type === 'classroom' ? 'school'
: node.type === 'clients-group' ? 'lan'
: node.type === 'client' ? 'computer'
: 'group'
}}
</mat-icon>
<span>{{ node.name }}</span>
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onNodeClick(node)">
<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)">
<button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle"></button>
<mat-icon style="color: green;">
{{
node.type === 'organizational-unit' ? 'apartment'
: node.type === 'classrooms-group' ? 'meeting_room'
: node.type === 'classroom' ? 'school'
: node.type === 'clients-group' ? 'lan'
: node.type === 'client' ? 'computer'
: 'group'
}}
</mat-icon>
<span>{{ node.name }}</span>
<ng-container *ngIf="node.type === 'client'">
<span> - IP: {{ node.ip }}</span>
</ng-container>
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onNodeClick(node)">
<mat-icon>more_vert</mat-icon>
</button>
</mat-tree-node>
</mat-tree>
</div>
<mat-divider [vertical]="true"></mat-divider>
<!-- Tree node actions -->
<mat-menu restoreFocus=false #commandMenu="matMenu">
<button mat-menu-item *ngFor="let command of commands" (click)="executeCommand(command, selectedNode)">
<span>{{ command.name }}</span>
</button>
</mat-menu>
<mat-menu #menuNode="matMenu">
<button mat-menu-item (click)="onShowDetailsClick($event, selectedNode)">
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
<span>{{ 'viewUnitMenu' | translate }}</span>
</button>
<button *ngIf="selectedNode?.type === 'classroom'" mat-menu-item (click)="onRoomMap(selectedNode)">
<mat-icon>map</mat-icon>
<span>{{ 'roomMap' | translate }}</span>
</button>
<button mat-menu-item (click)="addClient($event, selectedNode)">
<mat-icon>add</mat-icon>
<span>{{ 'addClientMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="addOU($event, selectedNode)">
<mat-icon>playlist_add</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>
</button>
</mat-menu>
<!-- Clients view -->
<div class="clients-container">
<div class="clients-view-header">
<span class="clients-title-name">{{ 'clients' | translate }}
<strong>{{ selectedNode?.name }}</strong>
</span>
<div class="view-type-container">
<button mat-button color="primary" (click)="toggleView('card')" [disabled]="currentView === 'card'">
<mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
</button>
<button mat-button color="primary" (click)="toggleView('list')" [disabled]="currentView === 'list'">
<mat-icon>list</mat-icon> {{ 'Vista Lista' | translate }}
</button>
</div>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="onEditClick($event, unidad.type, unidad.uuid)">
<mat-icon matTooltip="{{ 'editUnitTooltip' | translate }}" matTooltipHideDelay="0">edit</mat-icon>
<span>{{ 'editUnitMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="onShowDetailsClick($event, unidad)">
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
<span>{{ 'viewUnitMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="addOU($event, unidad)">
<mat-icon matTooltip="{{ 'addInternalUnitTooltip' | translate }}" matTooltipHideDelay="0">add_home_work</mat-icon>
<span>{{ 'addInternalUnitMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="addClient($event, unidad)">
<mat-icon matTooltip="{{ 'addClientTooltip' | translate }}" matTooltipHideDelay="0">devices</mat-icon>
<span>{{ 'addClientMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="onTreeClick($event, unidad)">
<mat-icon matTooltip="{{ 'viewTreeTooltip' | translate }}" matTooltipHideDelay="0">account_tree</mat-icon>
<span>{{ 'viewTreeMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="onDeleteClick($event, unidad)">
<mat-icon>delete</mat-icon>
<span>{{ 'delete' | translate }}</span>
</button>
</mat-menu>
</mat-card-actions>
</mat-card>
</div>
<ng-template #detailsTemplate>
<div class="header-actions-container">
<button mat-raised-button color="primary" (click)="clearSelection()" class="back-button">
<mat-icon>arrow_back</mat-icon>
{{ 'Back' | translate }}
</button>
<div class="view-toggle-container" *ngIf="selectedDetail">
<button mat-button color="primary" (click)="toggleView('card')" [disabled]="currentView === 'card'">
<mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
</button>
<button mat-button color="primary" (click)="toggleView('list')" [disabled]="currentView === 'list'">
<mat-icon>list</mat-icon> {{ 'Vista Lista' | translate }}
</button>
</div>
</div>
<div class="main-container">
<div class="tree-container">
<h2>{{ selectedUnidad?.name }}</h2>
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
<mat-tree-node [ngClass]="{'selected-node': node === selectedNode}" *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>
<mat-icon class="node-icon {{ node.type }}">
{{
node.type === 'organizational-unit' ? 'apartment'
: node.type === 'classrooms-group' ? 'meeting_room'
: node.type === 'classroom' ? 'school'
: node.type === 'clients-group' ? 'lan'
: node.type === 'client' ? 'computer'
: 'group'
}}
</mat-icon>
<span>{{ node.name }}</span>
<button mat-icon-button [matMenuTriggerFor]="menu" (click)="setSelectedNode(node); $event.stopPropagation()">
<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)">
<button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle"></button>
<mat-icon style="color: green;">
{{
node.type === 'organizational-unit' ? 'apartment'
: node.type === 'classrooms-group' ? 'meeting_room'
: node.type === 'classroom' ? 'school'
: node.type === 'clients-group' ? 'lan'
: node.type === 'client' ? 'computer'
: 'group'
}}
</mat-icon>
<span>{{ node.name }}</span>
<ng-container *ngIf="node.type === 'client'">
<span> - IP: {{ node.ip }}</span>
</ng-container>
<button mat-icon-button [matMenuTriggerFor]="menu" (click)="setSelectedNode(node)">
<mat-icon>more_vert</mat-icon>
</button>
</mat-tree-node>
</mat-tree>
</div>
<mat-divider [vertical]="true"></mat-divider>
<mat-menu restoreFocus=false #commandMenu="matMenu">
<button mat-menu-item *ngFor="let command of commands" (click)="executeCommand(command, selectedNode)">
<span>{{ command.name }}</span>
</button>
</mat-menu>
<mat-menu #menu="matMenu">
<button *ngIf="selectedNode?.type === 'classroom'" mat-menu-item [matMenuTriggerFor]="commandMenu" (click)="fetchCommands()">
<mat-icon>play_arrow</mat-icon>
<span>{{ 'executeCommand' | translate }}</span>
</button>
<button mat-menu-item (click)="onShowDetailsClick($event, selectedNode)">
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
<span>{{ 'viewUnitMenu' | translate }}</span>
</button>
<button *ngIf="selectedNode?.type === 'classroom'" mat-menu-item (click)="onRoomMap(selectedNode)">
<mat-icon>map</mat-icon>
<span>{{ 'roomMap' | translate }}</span>
</button>
<button mat-menu-item (click)="addClient($event, selectedNode)">
<mat-icon>add</mat-icon>
<span>{{ 'addClientMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="addOU($event, selectedNode)">
<mat-icon>playlist_add</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)="onDeleteClick($event, selectedNode)">
<mat-icon>delete</mat-icon>
<span>{{ 'delete' | translate }}</span>
</button>
</mat-menu>
<div class="clients-container" *ngIf="(selectedClients.data?.length || 0) > 0">
<h3>{{ 'clients' | translate }} {{ selectedNode?.name ? ('del ' + selectedNode?.name) : '' }}</h3>
<div *ngIf="(selectedClients.data?.length || 0) > 0; else noClientsTemplate">
<!-- Cards view -->
<div class="clients-grid" *ngIf="currentView === 'card'">
<div *ngFor="let client of selectedClients.data" class="client-item">
<div class="client-card">
<img src="assets/images/client.png" alt="Client Icon" class="client-image" />
<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>
<div class="flex">
<mat-chip [ngClass]="{
'chip-og-live': client.status === 'og-live',
'chip-busy': client.status === 'busy',
'chip-windows': client.status === 'windows' || client.status === 'windows-session',
'chip-linux': client.status === 'linux' || client.status === 'linux-session',
'chip-macos': client.status === 'macos',
'chip-off': client.status === 'off'
}">
{{ client.status || 'off' }}
<span class="client-ip">{{ client.mac }}</span>
</mat-chip>
<div class="action-icons">
<button
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
mat-icon-button color="primary"
(click)="getStatus(client)">
(click)="getStatus(client, selectedNode)">
<mat-icon>sync</mat-icon>
</button>
@ -229,28 +189,46 @@
mat-icon-button color="primary">
<mat-spinner diameter="24"></mat-spinner>
</button>
</div>
<button mat-raised-button color="primary" [matMenuTriggerFor]="clientMenu">Acciones</button>
<mat-menu #clientMenu="matMenu">
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
<mat-icon>edit</mat-icon>
<span>{{ 'edit' | translate }}</span>
</button>
<button mat-menu-item (click)="onShowClientDetail($event, client)">
<button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)">
<mat-icon>visibility</mat-icon>
<span>{{ 'viewDetails' | translate }}</span>
</button>
<button mat-menu-item (click)="onDeleteClick($event, client, selectedNode)">
<mat-icon>delete</mat-icon>
<span>{{ 'delete' | translate }}</span>
</button>
</mat-menu>
<app-execute-command [clientData]="client['@id']"></app-execute-command>
</div>
</div>
</div>
</div>
</div>
<!-- List view -->
<div class="clients-table" *ngIf="currentView === 'list'">
<table mat-table matSort [dataSource]="selectedClients" class="mat-elevation-z8">
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
<td mat-cell *matCellDef="let client">
<img
[src]="'assets/images/ordenador_' + client.status + '.png'"
alt="Client Icon"
class="client-image" />
</td>
</ng-container>
<ng-container matColumnDef="sync">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'sync' | translate }} </th>
<td mat-cell *matCellDef="let client">
<button
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
mat-icon-button color="primary"
(click)="getStatus(client, selectedNode)">
<mat-icon>sync</mat-icon>
</button>
<button
*ngIf="syncStatus && syncingClientId === client.uuid"
mat-icon-button color="primary">
<mat-spinner diameter="24"></mat-spinner>
</button>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
<td mat-cell *matCellDef="let client">
@ -263,39 +241,9 @@
</ng-container>
<ng-container matColumnDef="oglive">
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
<td mat-cell *matCellDef="let client"> {{ client.ogLive?.name }} </td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
<td mat-cell *matCellDef="let client">
<mat-chip [ngClass]="{
'chip-og-live': client.status === 'og-live',
'chip-busy': client.status === 'busy',
'chip-windows': client.status === 'windows' || client.status === 'windows-session',
'chip-linux': client.status === 'linux' || client.status === 'linux-session',
'chip-macos': client.status === 'macos',
'chip-off': client.status === 'off'
}">
{{ client.status || 'off' }}
</mat-chip>
<button
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
mat-icon-button color="primary"
(click)="getStatus(client)">
<mat-icon>sync</mat-icon>
</button>
<button
*ngIf="syncStatus && syncingClientId === client.uuid"
mat-icon-button color="primary">
<mat-spinner diameter="24"></mat-spinner>
</button>
</td>
<td mat-cell *matCellDef="let client"> {{ (client.ogLive?.filename || '').slice(0, 15) }}{{ (client.ogLive?.filename?.length > 15) ? '...' : '' }} </td>
</ng-container>
<ng-container matColumnDef="maintenace">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'maintenance' | translate }} </th>
<td mat-cell *matCellDef="let client"> {{ client.maintenance }} </td>
@ -314,28 +262,14 @@
<td mat-cell *matCellDef="let client"> {{ client.parentName }} </td>
</ng-container>
<ng-container matColumnDef="actions">
<ng-container matColumnDef="actions" >
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'actions' | translate }} </th>
<td mat-cell *matCellDef="let client">
<button mat-icon-button [matMenuTriggerFor]="clientMenu">
<mat-icon>more_vert</mat-icon>
</button>
<app-execute-command [clientData]="client['@id']"></app-execute-command>
<mat-menu #clientMenu="matMenu">
<mat-menu restoreFocus=false #commandMenu="matMenu" xPosition="before">
<button mat-menu-item *ngFor="let command of commands" (click)="executeClientCommand(command, client)">
<span>{{ command.name }}</span>
</button>
</mat-menu>
<button mat-menu-item [matMenuTriggerFor]="commandMenu" (click)="fetchCommands()">
<mat-icon>play_arrow</mat-icon>
<span>{{ 'executeCommand' | translate }}</span>
</button>
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
<mat-icon>edit</mat-icon>
<span>{{ 'edit' | translate }}</span>
@ -358,4 +292,17 @@
</div>
</div>
</div>
<!-- No clients view -->
<ng-template #noClientsTemplate>
<div *ngIf="isLoadingClients" class="loading-container">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="!isLoadingClients" class="no-clients-info">
<mat-icon>error_outline</mat-icon>
<span>{{ 'noClients' | translate }}</span>
</div>
</ng-template>
</div>

View File

@ -22,10 +22,8 @@ import { MatTabsModule } from '@angular/material/tabs';
import { MatCardModule } from '@angular/material/card';
import { TranslateModule } from '@ngx-translate/core';
import { JoyrideModule } from 'ngx-joyride';
import { AdvancedSearchComponent } from './components/advanced-search/advanced-search.component';
import { ClientTabViewComponent } from './components/client-tab-view/client-tab-view.component';
import { OrganizationalUnitTabViewComponent } from './components/organizational-unit-tab-view/organizational-unit-tab-view.component';
import { MatMenuModule } from '@angular/material/menu';
import { MatTreeModule } from '@angular/material/tree';
describe('GroupsComponent', () => {
let component: GroupsComponent;
@ -33,7 +31,7 @@ describe('GroupsComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [GroupsComponent, AdvancedSearchComponent, ClientTabViewComponent, OrganizationalUnitTabViewComponent],
declarations: [GroupsComponent],
imports: [
HttpClientTestingModule,
ToastrModule.forRoot(),
@ -56,6 +54,7 @@ describe('GroupsComponent', () => {
MatListModule,
MatCardModule,
MatMenuModule,
MatTreeModule,
TranslateModule.forRoot(),
JoyrideModule.forRoot(),
],
@ -98,19 +97,4 @@ describe('GroupsComponent', () => {
component.getFilters();
expect(component.getFilters).toHaveBeenCalled();
});
it('should call onTabChange method', () => {
spyOn(component, 'onTabChange');
const event = { index: 2 } as any;
component.onTabChange(event);
expect(component.onTabChange).toHaveBeenCalledWith(event);
});
it('should call onSelectUnidad method', () => {
spyOn(component, 'onSelectUnidad');
const unidad = { id: '1', name: 'Test' } as any;
component.onSelectUnidad(unidad);
expect(component.onSelectUnidad).toHaveBeenCalledWith(unidad);
});
});

View File

@ -18,13 +18,12 @@ import { EditClientComponent } from './shared/clients/edit-client/edit-client.co
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 { ClientTabViewComponent } from './components/client-tab-view/client-tab-view.component';
import { OrganizationalUnitTabViewComponent } from './components/organizational-unit-tab-view/organizational-unit-tab-view.component';
import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component';
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
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";
enum NodeType {
OrganizationalUnit = 'organizational-unit',
@ -45,6 +44,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
selectedUnidad: UnidadOrganizativa | null = null;
selectedDetail: UnidadOrganizativa | null = null;
loading = false;
isLoadingClients: boolean = false;
searchTerm = '';
treeControl: FlatTreeControl<FlatNode>;
treeFlattener: MatTreeFlattener<TreeNode, FlatNode>;
@ -63,7 +63,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
syncingClientId: string | null = null;
private originalTreeData: TreeNode[] = [];
displayedColumns: string[] = ['name', 'oglive', 'status', 'maintenace', 'subnet', 'pxeTemplate', 'parentName', 'actions'];
displayedColumns: string[] = ['status','sync', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
private _sort!: MatSort;
private _paginator!: MatPaginator;
@ -83,8 +83,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.selectedClients.paginator = this._paginator;
}
}
@ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent;
@ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent;
private subscriptions: Subscription = new Subscription();
@ -116,6 +114,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.search();
this.getFilters();
this.updateGridCols();
this.loadOrganizationalUnits();
window.addEventListener('resize', this.updateGridCols);
this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => {
@ -135,6 +134,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
}
private transformer = (node: TreeNode, level: number): FlatNode => ({
id: node.id,
name: node.name,
type: node.type,
level,
@ -144,6 +144,40 @@ 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;
}
@ -158,14 +192,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.selectedDetail = null;
this.selectedClients.data = [];
this.isTreeViewActive = false;
}
onTabChange(event: MatTabChangeEvent): void {
if (event.index === 2) {
this.clientTabComponent.search();
} else if (event.index === 3) {
this.organizationalUnitTabComponent.search();
}
this.selectedNode = null;
}
getFilters(): void {
@ -212,38 +239,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
);
}
onSelectUnidad(unidad: UnidadOrganizativa): void {
this.selectedUnidad = unidad;
this.selectedDetail = unidad;
this.selectedClients.data = this.collectAllClients(unidad);
this.selectedClientsOriginal = [...this.selectedClients.data];
this.loadChildrenAndClients(unidad.id).then((fullData) => {
const treeData = this.convertToTreeData(fullData);
this.treeDataSource.data = treeData[0]?.children || [];
});
this.isTreeViewActive = true;
console.log('Selected unidad:', unidad);
}
private collectAllClients(node: UnidadOrganizativa): Client[] {
let clients = (node.clients || []).map(client => ({
...client,
parentName: node.name
}));
if (node.children) {
node.children.forEach((child) => {
clients = clients.concat(this.collectAllClients(child).map(client => ({
...client,
parentName: client.parentName || ''
})));
});
}
return clients;
}
private async loadChildrenAndClients(id: string): Promise<UnidadOrganizativa> {
try {
const childrenData = await this.dataService.getChildren(id).toPromise();
@ -268,6 +263,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
private convertToTreeData(data: UnidadOrganizativa): TreeNode[] {
const processNode = (node: UnidadOrganizativa): TreeNode => ({
id: node.id,
name: node.name,
type: node.type,
'@id': node['@id'],
@ -279,34 +275,23 @@ export class GroupsComponent implements OnInit, OnDestroy {
onNodeClick(node: TreeNode): void {
console.log('Node clicked:', node);
this.selectedNode = node;
this.fetchClientsForNode(node);
}
private fetchClientsForNode(node: TreeNode): void {
if (node.hasClients && node['@id']) {
this.subscriptions.add(
this.http.get<{ clients: Client[] }>(`${this.baseUrl}${node['@id']}`).subscribe(
(data) => {
const clientsWithParentName = (data.clients || []).map(client => ({
...client,
parentName: node.name
}));
this.selectedClients.data = clientsWithParentName;
this.selectedClients._updateChangeSubscription();
if (this._paginator) {
this._paginator.firstPage();
}
},
(error) => {
console.error('Error fetching clients:', error);
}
)
);
} else {
this.selectedClients.data = [];
this.selectedClients._updateChangeSubscription();
}
console.log('Node:', node);
this.isLoadingClients = true;
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({
next: (response) => {
this.selectedClients.data = response['hydra:member'];
this.isLoadingClients = false;
},
error: () => {
this.isLoadingClients = false;
}
});
}
getNodeIcon(node: TreeNode): string {
@ -351,7 +336,27 @@ export class GroupsComponent implements OnInit, OnDestroy {
});
}
addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
event.stopPropagation();
const dialogRef = this.dialog.open(CreateMultipleClientComponent, {
data: { organizationalUnit },
width: '900px',
});
dialogRef.afterClosed().subscribe(() => {
this.refreshOrganizationalUnits();
if (organizationalUnit && organizationalUnit['@id']) {
this.refreshClientsForNode(organizationalUnit);
}
});
}
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) => {
@ -362,6 +367,15 @@ export class GroupsComponent implements OnInit, OnDestroy {
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);
}
});
});
});
}
},
@ -370,6 +384,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
);
}
onEditNode(event: MouseEvent, node: TreeNode | null): void {
event.stopPropagation();
const uuid = node ? this.extractUuid(node['@id']) : null;
@ -478,10 +493,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
}
}
executeClientCommand(command: Command, client: Client): void {
this.toastr.success(`Ejecutando comando: ${command.name} en ${client.name}`);
}
onShowClientDetail(event: MouseEvent, client: Client): void {
event.stopPropagation();
this.router.navigate(['clients', client.uuid], { state: { clientData: client } });
@ -511,7 +522,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
iniciarTour(): void {
this.joyrideService.startTour({
steps: ['groupsTitleStepText', 'addStep', 'keyStep', 'unitStep', 'elementsStep', 'tabsStep'],
steps: ['groupsTitleStepText', 'filtersPanelStep', 'addStep', 'keyStep', 'tabsStep'],
showPrevButton: true,
themeColor: '#3f51b5',
});
@ -563,7 +574,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.selectedNode = node;
}
getStatus(client: Client): void {
getStatus(client: Client, node: any): void {
if (!client.uuid || !client['@id']) return;
this.syncingClientId = client.uuid;
@ -573,14 +584,15 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe(
() => {
this.toastr.success('Cliente actualizado correctamente');
this.search();
this.syncStatus = false;
this.syncingClientId = null;
this.search()
},
() => {
this.toastr.error('Error de conexión con el cliente');
this.syncStatus = false;
this.syncingClientId = null;
this.search()
}
)
);

View File

@ -60,6 +60,7 @@ export interface ClientCollection {
}
export interface TreeNode {
id?: string
name: string;
type: string;
'@id'?: string;
@ -70,6 +71,7 @@ export interface TreeNode {
}
export interface FlatNode {
id?: string;
name: string;
type: string;
level: number;

View File

@ -1,161 +1,63 @@
.create-client-container {
h1 {
text-align: center;
font-family: 'Roboto', sans-serif;
font-weight: 400;
color: #3f51b5;
margin-bottom: 20px;
}
.network-form {
display: flex;
flex-direction: column;
padding: 16px;
font-family: Arial, sans-serif;
font-size: 14px;
align-items: center;
gap: 15px;
}
h1, h3, h4 {
margin: 0 0 16px;
color: #333;
font-weight: 600;
}
h1 {
font-size: 20px;
}
h3 {
font-size: 18px;
}
h4 {
font-size: 16px;
margin-top: 16px;
}
.inputs-container {
display: flex;
gap: 24px;
margin-top: 16px;
.form-field {
width: 100%;
margin-top: 10px;
}
.mat-dialog-content {
flex: 1;
background-color: #f9f9f9;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
min-width: 600px;
max-width: 90vw;
width: 800px;
padding: 50px;
}
.create-multiple-client-container {
flex: 1;
background-color: #f9f9f9;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
button {
text-transform: none;
font-size: 16px;
font-weight: 500;
}
.client-form {
.mat-slide-toggle {
margin-top: 20px;
}
mat-option .unit-name {
display: block;
}
mat-option .unit-path {
display: block;
font-size: 0.8em;
color: gray;
}
.loading-spinner {
display: block;
margin: 0 auto;
align-items: center;
justify-content: center;
}
.create-client-container {
position: relative;
}
.grid-form {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.form-field {
width: 100%;
}
.mat-form-field {
width: 100%;
}
.scrollable-table {
max-height: 200px;
overflow-y: auto;
margin-top: 16px;
border: 1px solid #ddd;
border-radius: 8px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
text-align: left;
padding: 8px;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f1f1f1;
font-weight: bold;
}
tr:hover {
background-color: #f9f9f9;
}
button {
margin-right: 8px;
}
button:last-child {
margin-right: 0;
}
.mat-dialog-actions {
margin-top: 16px;
display: flex;
justify-content: space-between;
}
button.mat-raised-button {
text-transform: none;
font-weight: 600;
}
.loading-spinner {
margin: 16px auto;
display: block;
}
.toggle-button {
background: none;
border: none;
color: #007BFF;
cursor: pointer;
font-size: 14px;
text-decoration: underline;
}
.toggle-button:hover {
text-decoration: none;
}
.mat-divider {
margin: 0 16px;
}
.upload-container {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
input[type="file"] {
display: none;
}
@media (max-width: 768px) {
.inputs-container {
flex-direction: column;
gap: 16px;
}
.mat-dialog-content, .create-multiple-client-container {
padding: 12px;
}
.scrollable-table {
max-height: 150px;
}
}

View File

@ -1,136 +1,111 @@
<div class="create-client-container mat-elevation-z4">
<h1>{{ 'addClientTitle' | translate }}s</h1>
<div class="inputs-container">
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
<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']">
<div class="unit-name">{{ unit.name }}</div>
</mat-option>
</mat-select>
</mat-form-field>
</form>
<div class="create-client-container">
<h1 mat-dialog-title i18n="@@add-client-dialog-title">Añadir Cliente</h1>
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
<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']">
<div class="unit-name">{{ unit.name }}</div>
<div class="unit-path">{{ unit.path }}</div>
</mat-option>
</mat-select>
</mat-form-field>
<div *ngIf="!isSingleClientForm; else singleClientForm">
<h3>Añadir múltiples clientes</h3>
<div class="upload-container">
<button mat-raised-button color="primary" (click)="fileInput.click()">Subir fichero</button>
<input #fileInput type="file" (change)="onFileUpload($event)" accept="*" hidden>
<p>o añadelos manualmente:</p>
<div *ngIf="showTextarea">
<textarea #textarea matInput placeholder="Ejemplo: host bbaa-it1-11 { hardware ethernet a0:48:1c:8a:f1:5b; fixed-address 172.17.69.11; };" rows="20" cols="100"></textarea>
<button mat-raised-button color="primary" (click)="onTextarea(textarea.value)">cargar</button>
</div>
</div>
<mat-form-field class="form-field">
<mat-label i18n="@@name-label">Nombre</mat-label>
<input matInput formControlName="name">
</mat-form-field>
<h4 *ngIf="uploadedClients.length > 0">Clientes importados:</h4>
<div 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> Nombre </th>
<td mat-cell *matCellDef="let client"> {{ client.name }} </td>
</ng-container>
<mat-form-field class="form-field">
<mat-label i18n="@@oglive-label">OgLive</mat-label>
<mat-select formControlName="ogLive">
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
{{ oglive.filename }}
</mat-option>
</mat-select>
</mat-form-field>
<ng-container matColumnDef="ip">
<th mat-header-cell *matHeaderCellDef> IP </th>
<td mat-cell *matCellDef="let client"> {{ client.ip }} </td>
</ng-container>
<mat-form-field class="form-field">
<mat-label i18n="@@serial-number-label">Número de Serie</mat-label>
<input matInput formControlName="serialNumber">
</mat-form-field>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</div>
<mat-form-field class="form-field">
<mat-label i18n="@@netiface-label">Interfaz de red</mat-label>
<mat-select formControlName="netiface">
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
{{ type.name }}
</mat-option>
</mat-select>
</mat-form-field>
<!-- Añadir uon cliente -->
<mat-form-field class="form-field">
<mat-label i18n="@@net-driver-label">Controlador de red</mat-label>
<mat-select formControlName="netDriver">
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
{{ type.name }}
</mat-option>
</mat-select>
</mat-form-field>
<ng-template #singleClientForm>
<h3>Añadir un cliente</h3>
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
<mat-form-field class="form-field">
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
<input matInput formControlName="name">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label i18n="@@mac-label">MAC</mat-label>
<mat-hint i18n="@@mac-hint">Ejemplo: 00:11:22:33:44:55</mat-hint>
<input matInput formControlName="mac">
<mat-error i18n="@@mac-error">Formato de MAC inválido. Ejemplo válido: 00:11:22:33:44:55</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
<mat-select formControlName="ogLive">
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
{{ oglive.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label i18n="@@ip-label">Dirección IP</mat-label>
<mat-hint i18n="@@ip-hint">Ejemplo: 127.0.0.1</mat-hint>
<input matInput formControlName="ip">
<mat-error i18n="@@ip-error">Formato de dirección IP inválido. Ejemplo válido: 127.0.0.1</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'serialNumberLabel' | translate }}</mat-label>
<input matInput formControlName="serialNumber">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label i18n="@@oglive-label">Plantilla PXE</mat-label>
<mat-select formControlName="template">
<mat-option *ngFor="let template of templates" [value]="template['@id']">
{{ template.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'netifaceLabel' | translate }}</mat-label>
<mat-select formControlName="netiface">
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
{{ type.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label i18n="@@hardware-profile-label">Perfil de Hardware</mat-label>
<mat-select formControlName="hardwareProfile">
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">
{{ unit.description }}
</mat-option>
</mat-select>
<mat-error i18n="@@hardware-profile-error">Formato de URL inválido.</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'netDriverLabel' | translate }}</mat-label>
<mat-select formControlName="netDriver">
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
{{ type.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label i18n="@@hardware-profile-label">Repositorio</mat-label>
<mat-select formControlName="repository">
<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>{{ 'macLabel' | translate }}</mat-label>
<mat-hint>{{ 'macHint' | translate }}</mat-hint>
<input matInput formControlName="mac">
<mat-error>{{ 'macError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'ipLabel' | translate }}</mat-label>
<mat-hint>{{ 'ipHint' | translate }}</mat-hint>
<input matInput formControlName="ip">
<mat-error>{{ 'ipError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
<mat-select formControlName="template">
<mat-option *ngFor="let template of templates" [value]="template['@id']">
{{ template.name }}
</mat-option>
</mat-select>
</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>{{ 'hardwareProfileError' | translate }}</mat-error>
</mat-form-field>
</form>
</ng-template>
</div>
<mat-form-field class="form-field">
<mat-label>{{ 'menuLabel' | translate }}</mat-label>
<mat-select formControlName="menu">
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
{{ menu.name }}
</mat-option>
</mat-select>
<mat-error>{{ 'menuError' | translate }}</mat-error>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions align="end">
<button mat-button (click)="toggleClientForm()">
{{ isSingleClientForm ? 'Añadir múltiples clientes' : 'Añadir un único cliente' }}
</button>
<button mat-button color="warn" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
<button mat-button color="primary" (click)="onSubmit()">{{ 'addButton' | translate }}</button>
<button mat-button (click)="onNoClick()" i18n="@@cancel-button">Cancelar</button>
<button mat-button [disabled]="!clientForm.valid" (click)="onSubmit()" i18n="@@add-button">Añadir</button>
</div>
</div>

View File

@ -5,7 +5,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ToastrService } from 'ngx-toastr';
import { DataService } from '../../../services/data.service';
import * as Papa from 'papaparse';
@Component({
selector: 'app-create-client',
@ -18,12 +17,11 @@ export class CreateClientComponent implements OnInit {
parentUnits: any[] = [];
hardwareProfiles: any[] = [];
ogLives: any[] = [];
menus: any[] = [];
templates: any[] = [];
uploadedClients: any[] = [];
repositories: any[] = [];
loading: boolean = false;
displayedColumns: string[] = ['name', 'ip'];
isSingleClientForm: boolean = false;
showTextarea: boolean = true;
protected netifaceTypes = [
{ name: 'Eth0', value: 'eth0' },
{ name: 'Eth1', value: 'eth1' },
@ -49,6 +47,8 @@ export class CreateClientComponent implements OnInit {
this.loadHardwareProfiles();
this.loadOgLives();
this.loadPxeTemplates();
this.loadRepositories();
this.loadMenus()
}
initForm(): void {
@ -67,7 +67,9 @@ export class CreateClientComponent implements OnInit {
hardwareProfile: [
this.data.organizationalUnit?.networkSettings?.hardwareProfile?.['@id'] || null
],
ogLive: [null]
ogLive: [null],
repository: [null],
menu: [null]
});
}
@ -126,117 +128,47 @@ export class CreateClientComponent implements OnInit {
);
}
onFileUpload(event: any): void {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
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) {
clients.push({
name: match[1],
mac: match[2],
ip: match[3]
});
}
if (clients.length > 0) {
this.uploadedClients = clients;
this.toastService.success('Archivo cargado correctamente, los datos están listos para enviarse.', 'Éxito');
this.showTextarea = false;
} else {
this.toastService.error('No se encontraron datos válidos', 'Error');
this.showTextarea = true;
}
};
reader.readAsText(file);
}
loadMenus(): void {
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe(
response => {
this.menus = response['hydra:member'];
},
error => {
console.error('Error fetching menus:', error);
}
);
}
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) {
clients.push({
name: match[1],
mac: match[2],
ip: match[3]
});
}
if (clients.length > 0) {
this.uploadedClients = clients;
this.toastService.success('Datos cargados correctamente, los datos están listos para enviarse.', 'Éxito');
this.showTextarea = false;
} else {
this.toastService.error('No se encontraron datos válidos', 'Error');
this.showTextarea = true;
}
loadRepositories(): void {
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe(
response => {
this.repositories = response['hydra:member'];
},
error => {
console.error('Error fetching ogLives:', error);
}
);
}
onSubmit(): void {
if (this.isSingleClientForm) {
if (this.clientForm.valid) {
const formData = this.clientForm.value;
console.log('Form data:', formData);
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
response => {
this.toastService.success('Cliente creado exitosamente', 'Éxito');
this.dialogRef.close(response);
},
error => {
console.error('Error durante POST:', error);
this.toastService.error('Error al crear el cliente', 'Error');
}
);
}
} else {
if (this.uploadedClients.length > 0) {
this.uploadedClients.forEach(client => {
const formData = {
organizationalUnit: this.clientForm.value.organizationalUnit || null,
name: client.name || null,
mac: client.mac || null,
ip: client.ip || null,
template: this.clientForm.value.template || null,
hardwareProfile: this.clientForm.value.hardwareProfile || null,
ogLive: this.clientForm.value.ogLive || null,
serialNumber: null,
netiface: null,
netDriver: null
};
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
response => {
this.toastService.success(`Cliente ${client.name} creado exitosamente`, 'Éxito');
},
error => {
console.error(`Error al crear el cliente ${client.name}:`, error);
this.toastService.error(`Error al crear el cliente ${client.name}`, 'Error');
}
);
});
this.uploadedClients = [];
this.dialogRef.close();
} else {
this.toastService.error('No hay clientes cargados para añadir', 'Error');
}
if (this.clientForm.valid) {
const formData = this.clientForm.value;
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
response => {
this.toastService.success('Cliente creado exitosamente', 'Éxito');
this.dialogRef.close(response);
},
error => {
this.toastService.error('Error al crear el cliente', 'Error');
}
);
}
}
toggleClientForm(): void {
this.isSingleClientForm = !this.isSingleClientForm;
}
onNoClick(): void {
this.dialogRef.close();
}

View File

@ -0,0 +1,158 @@
.create-client-container {
display: flex;
flex-direction: column;
padding: 16px;
}
h1, h3, h4 {
margin: 0 0 16px;
color: #333;
font-weight: 600;
}
h1 {
font-size: 20px;
}
h3 {
font-size: 18px;
}
h4 {
font-size: 16px;
margin-top: 16px;
}
.inputs-container {
display: flex;
gap: 24px;
margin-top: 16px;
}
.mat-dialog-content {
flex: 1;
background-color: #f9f9f9;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
min-width: 600px;
max-width: 90vw;
width: 800px;
}
.create-multiple-client-container {
flex: 1;
background-color: #f9f9f9;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.client-form {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.form-field {
width: 100%;
}
.mat-form-field {
width: 100%;
}
.scrollable-table {
max-height: 200px;
overflow-y: auto;
margin-top: 16px;
border: 1px solid #ddd;
border-radius: 8px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
text-align: left;
padding: 8px;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f1f1f1;
font-weight: bold;
}
tr:hover {
background-color: #f9f9f9;
}
button {
margin-right: 8px;
}
button:last-child {
margin-right: 0;
}
.mat-dialog-actions {
margin-top: 16px;
display: flex;
justify-content: space-between;
}
button.mat-raised-button {
text-transform: none;
font-weight: 600;
}
.loading-spinner {
margin: 16px auto;
display: block;
}
.toggle-button {
background: none;
border: none;
color: #007BFF;
cursor: pointer;
font-size: 14px;
text-decoration: underline;
}
.toggle-button:hover {
text-decoration: none;
}
.mat-divider {
margin: 0 16px;
}
.upload-container {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
input[type="file"] {
display: none;
}
@media (max-width: 768px) {
.inputs-container {
flex-direction: column;
gap: 16px;
}
.mat-dialog-content, .create-multiple-client-container {
padding: 12px;
}
.scrollable-table {
max-height: 150px;
}
}

View File

@ -0,0 +1,58 @@
<div class="create-client-container">
<h1 mat-dialog-title i18n="@@add-client-dialog-title">Añadir multiples clientes</h1>
<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>{{ 'organizationalUnitLabel' | translate }}</mat-label>
<mat-select (selectionChange)="setOrganizationalUnit($event)">
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
<div class="unit-name">{{ unit.name }}</div>
</mat-option>
</mat-select>
</mat-form-field>
</form>
<div>
<div class="upload-container">
<button mat-raised-button color="primary" (click)="fileInput.click()">Subir fichero</button>
<input #fileInput type="file" (change)="onFileUpload($event)" accept="*" hidden>
<p>o añadelos manualmente:</p>
<div *ngIf="showTextarea">
<textarea #textarea matInput placeholder="Ejemplo: host bbaa-it1-11 { hardware ethernet a0:48:1c:8a:f1:5b; fixed-address 172.17.69.11; };" rows="20" cols="100"></textarea>
<button mat-raised-button color="primary" (click)="onTextarea(textarea.value)">Previsualizar</button>
</div>
</div>
<h4 *ngIf="uploadedClients.length > 0">Clientes importados:</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> Nombre </th>
<td mat-cell *matCellDef="let client"> {{ client.name }} </td>
</ng-container>
<ng-container matColumnDef="ip">
<th mat-header-cell *matHeaderCellDef> IP </th>
<td mat-cell *matCellDef="let client"> {{ client.ip }} </td>
</ng-container>
<ng-container matColumnDef="mac">
<th mat-header-cell *matHeaderCellDef> Mac </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>
<div mat-dialog-actions align="end">
<button mat-button color="warn" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
<button mat-button color="primary" [disabled]="!organizationalUnit" (click)="onSubmit()">{{ 'saveButton' | translate }}</button>
</div>
</div>

View File

@ -0,0 +1,138 @@
import {Component, Inject, OnInit, Optional} from '@angular/core';
import {MatDialogRef} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http";
import {MatSnackBar} from "@angular/material/snack-bar";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-create-multiple-client',
templateUrl: './create-multiple-client.component.html',
styleUrl: './create-multiple-client.component.css'
})
export class CreateMultipleClientComponent implements OnInit{
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
parentUnits: any[] = [];
uploadedClients: any[] = [];
loading: boolean = false;
displayedColumns: string[] = ['name', 'ip', 'mac'];
showTextarea: boolean = true;
organizationalUnit: any;
constructor(
@Optional() private dialogRef: MatDialogRef<CreateMultipleClientComponent>,
private http: HttpClient,
private snackBar: MatSnackBar,
private toastService: ToastrService
) {}
ngOnInit(): void {
this.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.loading = false;
},
error => {
console.error('Error fetching parent units:', error);
this.loading = false;
}
);
}
setOrganizationalUnit(organizationalUnit: any): void {
console.log('Organizational unit selected:', organizationalUnit.value);
this.organizationalUnit = organizationalUnit.value;
}
onFileUpload(event: any): void {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
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) {
clients.push({
name: match[1],
mac: match[2],
ip: match[3]
});
}
if (clients.length > 0) {
this.uploadedClients = clients;
this.toastService.success('Archivo cargado correctamente, los datos están listos para enviarse.', 'Éxito');
this.showTextarea = false;
} else {
this.toastService.error('No se encontraron datos válidos', 'Error');
this.showTextarea = true;
}
};
reader.readAsText(file);
}
}
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) {
clients.push({
name: match[1],
mac: match[2],
ip: match[3]
});
}
if (clients.length > 0) {
this.uploadedClients = clients;
this.toastService.success('Datos cargados correctamente, los datos están listos para enviarse.', 'Éxito');
this.showTextarea = false;
} else {
this.toastService.error('No se encontraron datos válidos', 'Error');
this.showTextarea = true;
}
}
onSubmit(): void {
if (this.uploadedClients.length > 0) {
this.uploadedClients.forEach(client => {
const formData = {
organizationalUnit: this.organizationalUnit,
name: client.name || null,
mac: client.mac || null,
ip: client.ip || null,
};
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
response => {
this.toastService.success(`Cliente ${client.name} creado exitosamente`, 'Éxito');
},
error => {
this.toastService.error(error.error['hydra:description'], `Error al crear el cliente ${client.name}`);
}
);
});
this.uploadedClients = [];
this.dialogRef.close();
} else {
this.toastService.error('No hay clientes cargados para añadir', 'Error');
}
}
onNoClick(): void {
this.dialogRef.close();
}
}

View File

@ -21,7 +21,7 @@
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
<mat-select formControlName="ogLive">
<mat-option *ngFor="let ogLive of ogLives" [value]="ogLive['@id']">
{{ ogLive.name }}
{{ ogLive.filename }}
</mat-option>
</mat-select>
</mat-form-field>
@ -88,6 +88,15 @@
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'menuLabel' | translate }}</mat-label>
<mat-select formControlName="menu">
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
{{ menu.name }}
</mat-option>
</mat-select>
<mat-error>{{ 'menuError' | translate }}</mat-error>
</mat-form-field>
</form>
</div>

View File

@ -19,6 +19,7 @@ export class EditClientComponent {
repositories: any[] = [];
ogLives: any[] = [];
templates: any[] = [];
menus: any[] = [];
isEditMode: boolean;
protected netifaceTypes = [
{ "name": 'Eth0', "value": "eth0" },
@ -50,6 +51,7 @@ export class EditClientComponent {
this.loadOgLives();
this.loadPxeTemplates()
this.loadRepositories();
this.loadMenus()
this.clientForm = this.fb.group({
organizationalUnit: [null, Validators.required],
name: ['', Validators.required],
@ -62,6 +64,7 @@ export class EditClientComponent {
hardwareProfile: null,
ogLive: null,
repository: null,
menu: null,
});
}
@ -102,6 +105,19 @@ export class EditClientComponent {
);
}
loadMenus(): void {
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe(
response => {
this.menus = response['hydra:member'];
},
error => {
console.error('Error fetching menus:', error);
}
);
}
loadRepositories(): void {
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
@ -146,6 +162,7 @@ export class EditClientComponent {
repository: data.repository ? data.repository['@id'] : null,
ogLive: data.ogLive ? data.ogLive['@id'] : null,
template: data.template ? data.template['@id'] : null,
menu: data.menu ? data.menu['@id'] : null,
});
this.loading = false;
},

View File

@ -9,7 +9,7 @@
<mat-form-field class="form-field">
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of types" [value]="type">
<mat-option *ngFor="let type of filteredTypes" [value]="type">
{{ typeTranslations[type] }}
</mat-option>
</mat-select>
@ -88,7 +88,7 @@
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
<mat-select formControlName="oglive" (selectionChange)="onOgLiveChange($event)">
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
{{ oglive.name }}
{{ oglive.filename }}
</mat-option>
</mat-select>
</mat-form-field>

View File

@ -29,10 +29,9 @@ export class CreateOrganizationalUnitComponent implements OnInit {
{ name: 'Peer', value: 'p2p-mode-peer' },
{ name: 'Seeder', value: 'p2p-mode-seeder' },
];
multicastModeOptions: { name: string, value: string }[] = [
{ name: 'Modo 1', value: 'mode1' },
{ name: 'Modo 2', value: 'mode2' },
{ name: 'Modo 3', value: 'mode3' },
protected multicastModeOptions = [
{"name": 'Half duplex', "value": "half"},
{"name": 'Full duplex', "value": "full"},
];
parentUnits: any[] = [];
hardwareProfiles: any[] = [];
@ -41,6 +40,7 @@ export class CreateOrganizationalUnitComponent implements OnInit {
repositories: any[] = [];
selectedCalendarUuid: string | null = null;
@Output() unitAdded = new EventEmitter();
constructor(
@ -97,6 +97,10 @@ export class CreateOrganizationalUnitComponent implements OnInit {
this.loadRepositories();
}
get filteredTypes(): string[] {
return this.generalFormGroup.get('parent')?.value ? this.types.filter(type => type !== 'organizational-unit') : this.types;
}
loadParentUnits() {
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
this.http.get<any>(url).subscribe(

View File

@ -8,7 +8,7 @@
<mat-form-field class="form-field">
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of types" [value]="type">{{ type }}</mat-option>
<mat-option *ngFor="let type of filteredTypes" [value]="type">{{ type }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
@ -85,7 +85,7 @@
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
{{ oglive.name }}
{{ oglive.filename }}
</mat-option>
</mat-select>
</mat-form-field>

View File

@ -31,8 +31,8 @@ export class EditOrganizationalUnitComponent implements OnInit {
{"name": 'Seeder', "value": "p2p-mode-seeder"},
];
protected multicastModeOptions = [
{"name": 'Half duplex', "value": "half-duplex"},
{"name": 'Full duplex', "value": "full-duplex"},
{"name": 'Half duplex', "value": "half"},
{"name": 'Full duplex', "value": "full"},
];
@Output() unitAdded = new EventEmitter();
calendars: any;
@ -98,6 +98,10 @@ export class EditOrganizationalUnitComponent implements OnInit {
this.loadRepositories();
}
get filteredTypes(): string[] {
return this.generalFormGroup.get('parent')?.value ? this.types.filter(type => type !== 'organizational-unit') : this.types;
}
loadParentUnits() {
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;

View File

@ -0,0 +1,41 @@
.dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
}
.form-container {
display: flex;
flex-direction: row; /* Alinear elementos horizontalmente */
justify-content: space-between;
gap: 24px;
align-items: flex-start; /* Para alinear superiormente */
}
.menu-form {
flex: 1; /* El formulario ocupa el espacio restante */
display: flex;
flex-direction: column;
gap: 16px;
}
.form-field {
width: 100%;
}
.iframe-container {
flex: 1; /* El iframe ocupa la otra mitad del espacio */
display: flex;
justify-content: center;
align-items: flex-start;
max-width: 50%; /* Limitar ancho máximo */
}
.iframe {
width: 100%;
height: 600px;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
}

View File

@ -0,0 +1,46 @@
<h2 mat-dialog-title>{{ menuId ? 'Editar' : 'Añadir' }} menú</h2>
<mat-dialog-content class="dialog-content">
<div class="form-container">
<!-- Formulario -->
<form [formGroup]="menuForm" (ngSubmit)="save()" class="menu-form">
<mat-form-field appearance="fill" class="form-field">
<mat-label>Nombre del menú</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field appearance="fill" class="form-field">
<span matPrefix>{{baseUrl}}/menu/</span>
<mat-label>Url pública</mat-label>
<input matInput formControlName="publicUrl" name="description">
</mat-form-field>
<mat-form-field appearance="fill" class="form-field">
<mat-label>Resolución</mat-label>
<input matInput formControlName="resolution" name="resolution">
</mat-form-field>
<mat-form-field appearance="fill" class="form-field">
<mat-label>Comentarios</mat-label>
<input matInput formControlName="comments" name="comments">
</mat-form-field>
<mat-checkbox
formControlName="isDefault"
name="isDefault"
>
{{ 'defaultMenuLabel' | translate }}
</mat-checkbox>
</form>
<!-- Iframe -->
<div class="iframe-container" *ngIf="safeUrl">
<iframe [src]="safeUrl" class="iframe"></iframe>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end" class="dialog-actions">
<button mat-button (click)="close()">Cancelar</button>
<button mat-button color="primary" (click)="save()">Guardar</button>
</mat-dialog-actions>

View File

@ -0,0 +1,110 @@
import {Component, Inject, OnInit} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ToastrService} from "ngx-toastr";
import {DataService} from "../data.service";
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-create-menu',
templateUrl: './create-menu.component.html',
styleUrl: './create-menu.component.css'
})
export class CreateMenuComponent implements OnInit{
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
menuForm: FormGroup<any>;
menuId: string | null = null;
softwareProfiles: any[] = [];
safeUrl: SafeResourceUrl | null = null;
constructor(
private fb: FormBuilder,
private http: HttpClient,
public dialogRef: MatDialogRef<CreateMenuComponent>,
private toastService: ToastrService,
private dataService: DataService,
private sanitizer: DomSanitizer,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.menuForm = this.fb.group({
name: ['', Validators.required],
publicUrl: ['', Validators.required],
resolution: ['', Validators.required],
isDefault: [false],
comments: [''],
});
}
updateSafeUrl(): void {
const url = `${this.baseUrl}/menu/${this.menuForm.get('publicUrl')?.value}`;
if (url) {
this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url);
} else {
this.safeUrl = null;
}
}
ngOnInit() {
if (this.data) {
this.load()
}
}
load(): void {
this.dataService.getMenu(this.data).subscribe({
next: (response) => {
this.menuForm = this.fb.group({
name: [response.name, Validators.required],
publicUrl: [response.publicUrl, Validators.required],
resolution: [response.resolution, Validators.required],
comments: [response.comments],
isDefault: [response.isDefault],
});
this.menuId = response['@id'];
this.updateSafeUrl()
},
error: (err) => {
console.error('Error fetching remote calendar:', err);
}
});
}
save(): void {
const payload = {
name: this.menuForm.value.name,
publicUrl: this.menuForm.value.publicUrl,
resolution: this.menuForm.value.resolution,
comments: this.menuForm.value.comments,
isDefault: this.menuForm.value.isDefault,
};
if (this.menuId) {
this.http.put(`${this.baseUrl}${this.menuId}`, payload).subscribe(
(response) => {
this.toastService.success('Menu editado correctamente');
this.dialogRef.close();
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
console.error('Error al editar la imagen', error);
}
);
} else {
this.http.post(`${this.baseUrl}/menus`, payload).subscribe(
(response) => {
this.toastService.success('Menu añadido correctamente');
this.dialogRef.close();
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
console.error('Error al añadir el menu', error);
}
);
}
}
close(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,54 @@
import { Injectable } from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DataService {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl = `${this.baseUrl}/menus?page=1&itemsPerPage=1000`;
constructor(private http: HttpClient) {}
getMenus(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
const params = new HttpParams({ fromObject: filters });
return this.http.get<any>(this.apiUrl, { params }).pipe(
map(response => {
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
return {
data: response['hydra:member'],
totalItems: response['hydra:totalItems']
}
} else {
throw new Error('Unexpected response format');
}
}),
catchError(error => {
console.error('Error fetching commands', error);
return throwError(error);
})
);
}
getMenu(id: string): Observable<any> {
return this.http.get<any>(`${this.baseUrl}${id}`).pipe(
map(response => {
if (response.name) {
return response;
} else {
throw new Error('Unexpected response format');
}
}),
catchError(error => {
console.error('Error fetching menus', error);
return throwError(error);
})
);
}
}

View File

@ -0,0 +1,83 @@
.title {
font-size: 24px;
}
.images-button-row {
display: flex;
justify-content: flex-start;
margin-top: 16px;
}
.divider {
margin: 20px 0;
}
.lists-container {
padding: 16px;
}
.imagesLists-container {
flex: 1;
}
.card.unidad-card {
height: 100%;
box-sizing: border-box;
}
.image-container {
display: flex;
align-items: center;
margin-bottom: 16px;
border-bottom: 1px solid rgba(122, 122, 122, 0.555);
}
.image-container h4 {
margin: 0;
flex: 1;
}
.image-name{
cursor: pointer;
}
table {
width: 100%;
margin-top: 50px;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 5px;
box-sizing: border-box;
}
.search-string {
flex: 2;
padding: 5px;
}
.search-boolean {
flex: 1;
padding: 5px;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
}

View File

@ -0,0 +1,56 @@
<div class="header-container">
<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 menus exitentes.">Administrar menús</h2>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="addMenu()" joyrideStep="addStep" text="Utiliza este botón para añadir un nuevo menu.">Añadir menú</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-label>Buscar nombre de menú</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>
</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 menu" >
<ng-container *ngIf="column.columnDef !== 'isDefault'">
{{ column.cell(menu) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'isDefault'">
<mat-icon [color]="menu[column.columnDef] ? 'primary' : 'warn'">
{{ menu[column.columnDef] ? 'check_circle' : 'cancel' }}
</mat-icon>
</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 menu" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editMenu($event, menu)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="deleteMenu($event, menu)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</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>

View File

@ -0,0 +1,46 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MenusComponent } from './menus.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { JoyrideModule } from 'ngx-joyride';
import { ToastrModule } from 'ngx-toastr';
describe('MenusComponent', () => {
let component: MenusComponent;
let fixture: ComponentFixture<MenusComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MenusComponent],
imports: [ HttpClientTestingModule, ToastrModule.forRoot(), BrowserAnimationsModule, MatDividerModule, MatFormFieldModule, MatInputModule, MatIconModule, MatButtonModule, MatTableModule, MatPaginatorModule, MatTooltipModule, FormsModule, ReactiveFormsModule, MatProgressSpinnerModule, MatDialogModule, MatSelectModule, MatTabsModule, MatAutocompleteModule, MatListModule, MatCardModule, MatMenuModule, TranslateModule.forRoot(), JoyrideModule.forRoot(), ],
})
.compileComponents();
fixture = TestBed.createComponent(MenusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,144 @@
import {Component, OnInit} from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {DatePipe} from "@angular/common";
import {MatDialog} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import {JoyrideService} from "ngx-joyride";
import {Router} from "@angular/router";
import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delete-modal.component";
import {CreateMenuComponent} from "./create-menu/create-menu.component";
@Component({
selector: 'app-menus',
templateUrl: './menus.component.html',
styleUrl: './menus.component.css'
})
export class MenusComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
dataSource = new MatTableDataSource<any>();
length: number = 0;
itemsPerPage: number = 10;
page: number = 0;
loading: boolean = false;
filters: { [key: string]: string } = {};
datePipe: DatePipe = new DatePipe('es-ES');
columns = [
{
columnDef: 'id',
header: 'Id',
cell: (menu: any) => `${menu.id}`
},
{
columnDef: 'name',
header: 'Nombre de menú',
cell: (menu: any) => `${menu.name}`
},
{
columnDef: 'publicUrl',
header: 'Url pública',
cell: (menu: any) => `${this.baseUrl}/menu/${menu.publicUrl}`
},
{
columnDef: 'isDefault',
header: 'Por defecto',
cell: (menu: any) => `${menu.isDefault}`
},
{
columnDef: 'resolution',
header: 'Resolución',
cell: (menu: any) => `${menu.resolution}`
},
{
columnDef: 'createdAt',
header: 'Fecha de creación',
cell: (menu: any) => `${this.datePipe.transform(menu.createdAt, 'dd/MM/yyyy hh:mm:ss')}`
}
];
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/menus`;
constructor(
public dialog: MatDialog,
private http: HttpClient,
private toastService: ToastrService,
private joyrideService: JoyrideService,
private router: Router
) {}
ngOnInit(): void {
this.search();
}
addMenu(): void {
const dialogRef = this.dialog.open(CreateMenuComponent, {
width: '1000px'
});
dialogRef.afterClosed().subscribe(() => {
this.search();
});
}
search(): void {
this.loading = true;
this.http.get<any>(`${this.apiUrl}?page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
data => {
this.dataSource.data = data['hydra:member'];
this.length = data['hydra:totalItems'];
this.loading = false;
},
error => {
console.error('Error fetching images', error);
this.loading = false;
}
);
}
editMenu(event: MouseEvent, menu: any): void {
event.stopPropagation();
this.dialog.open(CreateMenuComponent, {
width: '1200px',
data: menu['@id']
}).afterClosed().subscribe(() => this.search());
}
deleteMenu(event: MouseEvent, menu: any): void {
event.stopPropagation();
this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: menu.name },
}).afterClosed().subscribe((result) => {
if (result) {
this.http.delete(`${this.apiUrl}/${menu.uuid}`).subscribe({
next: () => {
this.toastService.success('Menu eliminado con éxito');
this.search();
},
error: (error) => {
console.error('Error al eliminar el menú:', error);
}
});
}
});
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.length = event.length;
this.search();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'addStep',
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -1,9 +1,5 @@
<h2 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Añadir' }} imagen ogLive</h2>
<mat-dialog-content>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Nombre</mat-label>
<input matInput [(ngModel)]="name">
</mat-form-field>
<mat-spinner class="spinner" *ngIf="loading"></mat-spinner>
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">

View File

@ -10,7 +10,6 @@ import { ToastrService } from 'ngx-toastr';
})
export class CreatePXEImageComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
name: string = '';
downloads: any[] = [];
selectedDownload: any;
isEditMode: boolean = false;
@ -28,7 +27,6 @@ export class CreatePXEImageComponent implements OnInit {
this.fetchDownloads();
if (this.data) {
this.isEditMode = true;
this.name = this.data.name;
this.selectedDownload = this.data.downloadUrl;
this.imageId = this.data.uuid; // Assuming UUID is used for edit
}
@ -55,7 +53,6 @@ export class CreatePXEImageComponent implements OnInit {
submitForm(): void {
const payload = {
name: this.name,
downloadUrl: this.selectedDownload.url
};

View File

@ -88,7 +88,7 @@
<button mat-icon-button color="info" (click)="showOgLive($event, image)">
<mat-icon>{{ 'viewIcon' | translate }}</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="editImage(image)">
<button mat-icon-button disabled color="primary" (click)="editImage(image)">
<mat-icon>{{ 'editIcon' | translate }}</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteImage(image)">

View File

@ -40,9 +40,9 @@ export class PXEimagesComponent implements OnInit {
cell: (user: any) => `${user.id}`
},
{
columnDef: 'name',
header: 'Nombre de imagen',
cell: (user: any) => `${user.name}`
columnDef: 'filename',
header: 'Og Live',
cell: (user: any) => `${user.filename}`
},
{
columnDef: 'isDefault',

View File

@ -128,5 +128,4 @@ export class RepositoriesComponent {
themeColor: '#3f51b5'
});
}
}

View File

@ -151,17 +151,10 @@
</span>
</mat-list-item>
<mat-list-item class="disabled" matTooltip="{{ 'TOOLTIP_MENUS' | translate }}" matTooltipShowDelay="1000">
<mat-list-item routerLink="/menus" matTooltip="{{ 'TOOLTIP_MENUS' | translate }}" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">list</mat-icon>
<span>{{ 'menus' | translate }}</span>
</span>
</mat-list-item>
<mat-list-item class="disabled" matTooltip="{{ 'TOOLTIP_SEARCH' | translate }}" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">search</mat-icon>
<span>{{ 'search' | translate }}</span>
</span>
</mat-list-item>
</mat-nav-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

View File

@ -211,6 +211,7 @@
"mcastPortLabel": "Multicast Port",
"mcastModeLabel": "Multicast Mode",
"menuUrlLabel": "Menu URL",
"menuLabel": "Menu",
"hardwareProfileLabel": "Hardware Profile",
"urlFormatError": "Invalid URL format.",
"validationToggle": "Validation",
@ -445,5 +446,9 @@
"maintenance": "Maintenance",
"subnet": "Subnet",
"parent": "Parent",
"adminUsersTitle": "Manage users"
"adminUsersTitle": "Manage users",
"filtersPanelStepText": "Use these filters to search or load configurations.",
"organizationalUnitsStepText": "List of Organizational Units. Click on one to view details.",
"defaultMenuLabel": "Main menu",
"noClients": "No clients"
}

View File

@ -363,6 +363,7 @@
"selectOptionPlaceholder": "Selecciona una opción",
"yesOption": "Sí",
"noOption": "No",
"menuLabel": "Menu",
"actionsColumn": "Acciones",
"createServerButton": "Crear servidor",
"labelName": "Nombre",
@ -447,5 +448,9 @@
"maintenance": "Mantenimiento",
"subnet": "Subred",
"parent": "Padre",
"adminUsersTitle": "Administrar usuarios"
"adminUsersTitle": "Administrar usuarios",
"filtersPanelStepText": "Utiliza estos filtros para buscar o cargar configuraciones.",
"organizationalUnitsStepText": "Lista de Unidades Organizacionales. Haz clic en una para ver detalles.",
"defaultMenuLabel": "Menú por defecto",
"noClients": "No hay clientes"
}

View File

@ -1,535 +1,94 @@
<?xml version="1.0"?>
<testsuite name="Chrome Headless 131.0.0.0 (Linux x86_64)" package="" timestamp="2024-11-19T13:35:34" id="0" hostname="alvaro-Latitude-3420" tests="38" errors="0" failures="32" time="0.486">
<testsuite name="Chrome Headless 131.0.0.0 (Linux x86_64)" package="" timestamp="2024-12-09T10:14:38" id="0" hostname="alvaro-Latitude-3420" tests="80" errors="0" failures="0" time="2.257">
<properties>
<property name="browser.fullName" value="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/131.0.0.0 Safari/537.36"/>
</properties>
<testcase name="CreateOperativeSystemComponent should create" time="0.071" classname="CreateOperativeSystemComponent"/>
<testcase name="AddClientsToPxeComponent should create" time="0.016" classname="AddClientsToPxeComponent">
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'AddClientsToPxeComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
error properties: Object({ code: -302 })
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
at AddClientsToPxeComponent_Template (ng:///AddClientsToPxeComponent.js:70:9)
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
</failure>
</testcase>
<testcase name="ClientsComponent should create" time="0.011" classname="ClientsComponent">
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'ClientsComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
error properties: Object({ code: -302 })
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
at ClientsComponent_Template (ng:///ClientsComponent.js:77:9)
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
</failure>
</testcase>
<testcase name="SoftwareComponent should create" time="0.019" classname="SoftwareComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.SoftwareComponent_Factory [as factory] (ng:///SoftwareComponent/ɵfac.js:6:52)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="OgDhcpSubnetsComponent should create" time="0.02" classname="OgDhcpSubnetsComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.OgDhcpSubnetsComponent_Factory [as factory] (ng:///OgDhcpSubnetsComponent/ɵfac.js:6:7)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="CreateCommandComponent should create" time="0.012" classname="CreateCommandComponent">
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'CreateCommandComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
error properties: Object({ code: -302 })
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
at CreateCommandComponent_Template (ng:///CreateCommandComponent.js:15:9)
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
</failure>
</testcase>
<testcase name="RepositoriesComponent should create" time="0.012" classname="RepositoriesComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[HttpClient -&gt; HttpClient]:
NullInjectorError: No provider for HttpClient!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'HttpClient', 'HttpClient' ] })
NullInjectorError: NullInjectorError: No provider for HttpClient!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.RepositoriesComponent_Factory [as factory] (ng:///RepositoriesComponent/ɵfac.js:5:7)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="PXEimagesComponent should create" time="0.024" classname="PXEimagesComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.PXEimagesComponent_Factory [as factory] (ng:///PXEimagesComponent/ɵfac.js:6:52)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="ExecuteCommandComponent should create" time="0.008" classname="ExecuteCommandComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[MatDialogRef -&gt; MatDialogRef]:
NullInjectorError: No provider for MatDialogRef!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'MatDialogRef', 'MatDialogRef' ] })
NullInjectorError: NullInjectorError: No provider for MatDialogRef!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.ExecuteCommandComponent_Factory [as factory] (ng:///ExecuteCommandComponent/ɵfac.js:4:51)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="OperativeSystemComponent should create" time="0.012" classname="OperativeSystemComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.OperativeSystemComponent_Factory [as factory] (ng:///OperativeSystemComponent/ɵfac.js:6:52)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="PxeBootFilesComponent should create" time="0.015" classname="PxeBootFilesComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.PxeBootFilesComponent_Factory [as factory] (ng:///PxeBootFilesComponent/ɵfac.js:6:7)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="ServerInfoDialogComponent should create" time="0.008" classname="ServerInfoDialogComponent"/>
<testcase name="EnvVarsComponent should create" time="0.005" classname="EnvVarsComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[HttpClient -&gt; HttpClient]:
NullInjectorError: No provider for HttpClient!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'HttpClient', 'HttpClient' ] })
NullInjectorError: NullInjectorError: No provider for HttpClient!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.EnvVarsComponent_Factory [as factory] (ng:///EnvVarsComponent/ɵfac.js:4:44)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="StatusComponent should create" time="0.013" classname="StatusComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.StatusComponent_Factory [as factory] (ng:///StatusComponent/ɵfac.js:5:7)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="SoftwareProfileComponent should create" time="0.011" classname="SoftwareProfileComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.SoftwareProfileComponent_Factory [as factory] (ng:///SoftwareProfileComponent/ɵfac.js:6:52)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="CalendarComponent should create" time="0.013" classname="CalendarComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.CalendarComponent_Factory [as factory] (ng:///CalendarComponent/ɵfac.js:6:52)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="AdminComponent el primer botón debería tener el routerLink correcto" time="0.008" classname="AdminComponent">
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'AdminComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
error properties: Object({ code: -302 })
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
at AdminComponent_Template (ng:///AdminComponent.js:13:9)
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
</failure>
</testcase>
<testcase name="AdminComponent el segundo botón debería tener el routerLink correcto" time="0.003" classname="AdminComponent">
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'AdminComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
error properties: Object({ code: -302 })
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
at AdminComponent_Template (ng:///AdminComponent.js:13:9)
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
</failure>
</testcase>
<testcase name="AdminComponent debería contener dos botones" time="0.002" classname="AdminComponent">
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'AdminComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
error properties: Object({ code: -302 })
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
at AdminComponent_Template (ng:///AdminComponent.js:13:9)
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
</failure>
</testcase>
<testcase name="AdminComponent debería crear el componente" time="0.002" classname="AdminComponent">
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'AdminComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
error properties: Object({ code: -302 })
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
at AdminComponent_Template (ng:///AdminComponent.js:13:9)
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
</failure>
</testcase>
<testcase name="AdminComponent el segundo botón debería tener el texto &quot;Roles&quot;" time="0.002" classname="AdminComponent">
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'AdminComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
error properties: Object({ code: -302 })
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
at AdminComponent_Template (ng:///AdminComponent.js:13:9)
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
</failure>
</testcase>
<testcase name="AdminComponent el primer botón debería tener el texto &quot;Usuarios&quot;" time="0.002" classname="AdminComponent">
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'AdminComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
error properties: Object({ code: -302 })
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
at AdminComponent_Template (ng:///AdminComponent.js:13:9)
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
</failure>
</testcase>
<testcase name="AppComponent should create the app" time="0.002" classname="AppComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[TranslateService -&gt; TranslateStore -&gt; TranslateStore]:
NullInjectorError: No provider for TranslateStore!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'TranslateService', 'TranslateStore', 'TranslateStore' ] })
NullInjectorError: NullInjectorError: No provider for TranslateStore!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at injectInjectorOnly (http://localhost:9876/_karma_webpack_/vendor.js:76056:36)
at Module.ɵɵinject (http://localhost:9876/_karma_webpack_/vendor.js:76062:59)
at Object.TranslateService_Factory [as factory] (http://localhost:9876/_karma_webpack_/vendor.js:163478:94)
at http://localhost:9876/_karma_webpack_/vendor.js:78093:35
at runInInjectorProfilerContext (http://localhost:9876/_karma_webpack_/vendor.js:75827:5)
at R3Injector.hydrate (http://localhost:9876/_karma_webpack_/vendor.js:78092:11)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77966:23)
</failure>
</testcase>
<testcase name="UsersComponent should create" time="0.008" classname="UsersComponent">
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'UsersComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
error properties: Object({ code: -302 })
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
at UsersComponent_Template (ng:///UsersComponent.js:133:9)
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
</failure>
</testcase>
<testcase name="PxeComponent should create the component" time="0.019" classname="PxeComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.PxeComponent_Factory [as factory] (ng:///PxeComponent/ɵfac.js:6:7)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="DeployImageComponent should create" time="0.017" classname="DeployImageComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[HttpClient -&gt; HttpClient]:
NullInjectorError: No provider for HttpClient!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'HttpClient', 'HttpClient' ] })
NullInjectorError: NullInjectorError: No provider for HttpClient!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.DeployImageComponent_Factory [as factory] (ng:///DeployImageComponent/ɵfac.js:4:48)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="OgdhcpComponent should create" time="0.003" classname="OgdhcpComponent"/>
<testcase name="CreateSoftwareProfileComponent should create" time="0.054" classname="CreateSoftwareProfileComponent"/>
<testcase name="OgbootStatusComponent should create the component" time="0.013" classname="OgbootStatusComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.OgbootStatusComponent_Factory [as factory] (ng:///OgbootStatusComponent/ɵfac.js:5:7)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="LoginComponent should create" time="0.009" classname="LoginComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[TranslateService -&gt; TranslateStore -&gt; TranslateStore]:
NullInjectorError: No provider for TranslateStore!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'TranslateService', 'TranslateStore', 'TranslateStore' ] })
NullInjectorError: NullInjectorError: No provider for TranslateStore!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at injectInjectorOnly (http://localhost:9876/_karma_webpack_/vendor.js:76056:36)
at Module.ɵɵinject (http://localhost:9876/_karma_webpack_/vendor.js:76062:59)
at Object.TranslateService_Factory [as factory] (http://localhost:9876/_karma_webpack_/vendor.js:163478:94)
at http://localhost:9876/_karma_webpack_/vendor.js:78093:35
at runInInjectorProfilerContext (http://localhost:9876/_karma_webpack_/vendor.js:75827:5)
at R3Injector.hydrate (http://localhost:9876/_karma_webpack_/vendor.js:78092:11)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77966:23)
</failure>
</testcase>
<testcase name="RolesComponent should create" time="0.009" classname="RolesComponent">
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'RolesComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
error properties: Object({ code: -302 })
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
at RolesComponent_Template (ng:///RolesComponent.js:138:9)
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
</failure>
</testcase>
<testcase name="CreateSoftwareComponent should create" time="0.017" classname="CreateSoftwareComponent"/>
<testcase name="CommandsComponent should create" time="0.012" classname="CommandsComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.CommandsComponent_Factory [as factory] (ng:///CommandsComponent/ɵfac.js:6:7)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="CreateRepositoryComponent should create" time="0.005" classname="CreateRepositoryComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[HttpClient -&gt; HttpClient]:
NullInjectorError: No provider for HttpClient!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'HttpClient', 'HttpClient' ] })
NullInjectorError: NullInjectorError: No provider for HttpClient!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.CreateRepositoryComponent_Factory [as factory] (ng:///CreateRepositoryComponent/ɵfac.js:5:7)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="DashboardComponent should create the component" time="0.003" classname="DashboardComponent"/>
<testcase name="CommandsTaskComponent should create" time="0.011" classname="CommandsTaskComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -&gt; JoyrideService]:
NullInjectorError: No provider for JoyrideService!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
NullInjectorError: NullInjectorError: No provider for JoyrideService!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.CommandsTaskComponent_Factory [as factory] (ng:///CommandsTaskComponent/ɵfac.js:6:7)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="CreateImageComponent should create" time="0.001" classname="CreateImageComponent">
<failure type="">Error: Unexpected "CreateImageComponent" found in the "declarations" array of the "TestBed.configureTestingModule" call, "CreateImageComponent" is marked as standalone and can't be declared in any NgModule - did you intend to import it instead (by adding it to the "imports" array)?
at http://localhost:9876/_karma_webpack_/vendor.js:115668:15
at Array.forEach (&lt;anonymous&gt;)
at assertNoStandaloneComponents (http://localhost:9876/_karma_webpack_/vendor.js:115664:9)
at TestBedCompiler.configureTestingModule (http://localhost:9876/_karma_webpack_/vendor.js:115738:7)
at TestBedImpl.configureTestingModule (http://localhost:9876/_karma_webpack_/vendor.js:116819:19)
at TestBedImpl.configureTestingModule (http://localhost:9876/_karma_webpack_/vendor.js:116648:33)
at UserContext.&lt;anonymous&gt; (http://localhost:9876/_karma_webpack_/main.js:3164:70)
at Generator.next (&lt;anonymous&gt;)
at asyncGeneratorStep (http://localhost:9876/_karma_webpack_/vendor.js:203145:17)
at _next (http://localhost:9876/_karma_webpack_/vendor.js:203159:9)
</failure>
</testcase>
<testcase name="MainRepositoryViewComponent should create" time="0.014" classname="MainRepositoryViewComponent">
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[HttpClient -&gt; HttpClient]:
NullInjectorError: No provider for HttpClient!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'HttpClient', 'HttpClient' ] })
NullInjectorError: NullInjectorError: No provider for HttpClient!
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
at NodeInjectorFactory.MainRepositoryViewComponent_Factory [as factory] (ng:///MainRepositoryViewComponent/ɵfac.js:5:7)
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
</failure>
</testcase>
<testcase name="CreateSoftwareComponent should create" time="0.12" classname="CreateSoftwareComponent"/>
<testcase name="CommandsComponent should create" time="0.104" classname="CommandsComponent"/>
<testcase name="OgbootStatusComponent should create the component" time="0.054" classname="OgbootStatusComponent"/>
<testcase name="PxeComponent should have a defined currentPage" time="0.069" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined itemsPerPage" time="0.041" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined component" time="0.035" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined loading" time="0.037" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined page" time="0.036" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined selectedElements" time="0.032" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined pageSizeOptions" time="0.034" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined selectedItem" time="0.031" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined length" time="0.029" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined datePipe" time="0.028" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined dataSource" time="0.029" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined baseUrl" time="0.029" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined alertMessage" time="0.025" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined pxeTemplates" time="0.027" classname="PxeComponent"/>
<testcase name="PxeComponent should have a defined filters" time="0.029" classname="PxeComponent"/>
<testcase name="PxeComponent should create the component" time="0.025" classname="PxeComponent"/>
<testcase name="AdminComponent debería renderizar dos botones" time="0.012" classname="AdminComponent"/>
<testcase name="AdminComponent debería tener un botón con routerLink a &quot;/users&quot;" time="0.005" classname="AdminComponent"/>
<testcase name="AdminComponent debería aplicar la clase &quot;fab-button&quot; a ambos botones" time="0.005" classname="AdminComponent"/>
<testcase name="AdminComponent debería crear el componente" time="0.006" classname="AdminComponent"/>
<testcase name="CreateOperativeSystemComponent should create" time="0.017" classname="CreateOperativeSystemComponent"/>
<testcase name="PxeBootFilesComponent should create" time="0.048" classname="PxeBootFilesComponent"/>
<testcase name="CalendarComponent should create" time="0.05" classname="CalendarComponent"/>
<testcase name="CreateRepositoryComponent should create" time="0.018" classname="CreateRepositoryComponent"/>
<testcase name="PXEimagesComponent should create" time="0.077" classname="PXEimagesComponent"/>
<testcase name="SoftwareComponent should create" time="0.055" classname="SoftwareComponent"/>
<testcase name="CreateCommandComponent should create" time="0.033" classname="CreateCommandComponent"/>
<testcase name="AppComponent should default to Spanish if no language is saved in localStorage" time="0.01" classname="AppComponent"/>
<testcase name="AppComponent should create the app" time="0.003" classname="AppComponent"/>
<testcase name="AppComponent should set language to Spanish in sessionStorage on ngOnInit" time="0.002" classname="AppComponent"/>
<testcase name="AppComponent should have as title 'ogWebconsole'" time="0.003" classname="AppComponent"/>
<testcase name="AppComponent should set the language from localStorage on creation" time="0.003" classname="AppComponent"/>
<testcase name="DeployImageComponent should create" time="0.08" classname="DeployImageComponent"/>
<testcase name="CreateSoftwareProfileComponent should create" time="0.085" classname="CreateSoftwareProfileComponent"/>
<testcase name="OgDhcpSubnetsComponent should create" time="0.078" classname="OgDhcpSubnetsComponent"/>
<testcase name="OgDhcpSubnetsComponent should call syncSubnets and handle success" time="0.044" classname="OgDhcpSubnetsComponent"/>
<testcase name="SoftwareProfileComponent should create" time="0.051" classname="SoftwareProfileComponent"/>
<testcase name="RolesComponent should create" time="0.021" classname="RolesComponent"/>
<testcase name="RolesComponent should initialize the dataSource" time="0.005" classname="RolesComponent"/>
<testcase name="RolesComponent should have a defined columns array" time="0.005" classname="RolesComponent"/>
<testcase name="RolesComponent should have a default itemsPerPage value" time="0.007" classname="RolesComponent"/>
<testcase name="EnvVarsComponent should create" time="0.019" classname="EnvVarsComponent"/>
<testcase name="MainRepositoryViewComponent should create" time="0.071" classname="MainRepositoryViewComponent"/>
<testcase name="StatusComponent should create" time="0.022" classname="StatusComponent"/>
<testcase name="CommandsTaskComponent should create" time="0.039" classname="CommandsTaskComponent"/>
<testcase name="LoginComponent should add rotating class to the logo when loading" time="0.025" classname="LoginComponent"/>
<testcase name="LoginComponent should create" time="0.01" classname="LoginComponent"/>
<testcase name="LoginComponent should add &quot;invalid&quot; class to username input if it is invalid and touched" time="0.011" classname="LoginComponent"/>
<testcase name="LoginComponent should show a success toast message" time="0.01" classname="LoginComponent"/>
<testcase name="LoginComponent should not add rotating class to the logo when not loading" time="0.01" classname="LoginComponent"/>
<testcase name="LoginComponent should show error message on login failure" time="0.01" classname="LoginComponent"/>
<testcase name="LoginComponent should toggle password visibility when clicking the button" time="0.01" classname="LoginComponent"/>
<testcase name="LoginComponent should disable the login button if username or password is missing" time="0.01" classname="LoginComponent"/>
<testcase name="LoginComponent should change language to English and save to localStorage" time="0.009" classname="LoginComponent"/>
<testcase name="LoginComponent should call onLogin and navigate on successful login" time="0.012" classname="LoginComponent"/>
<testcase name="LoginComponent should enable the login button if username and password are present" time="0.01" classname="LoginComponent"/>
<testcase name="LoginComponent should change language to Spanish and save to localStorage" time="0.01" classname="LoginComponent"/>
<testcase name="LoginComponent should show an error toast message" time="0.01" classname="LoginComponent"/>
<testcase name="ExecuteCommandComponent should create" time="0.025" classname="ExecuteCommandComponent"/>
<testcase name="CreateImageComponent should create" time="0.025" classname="CreateImageComponent"/>
<testcase name="OgdhcpComponent should create" time="0.005" classname="OgdhcpComponent"/>
<testcase name="DashboardComponent should create the component" time="0.006" classname="DashboardComponent"/>
<testcase name="GroupsComponent should call getFilters on ngOnInit" time="0.141" classname="GroupsComponent"/>
<testcase name="GroupsComponent should call search on ngOnInit" time="0.011" classname="GroupsComponent"/>
<testcase name="GroupsComponent should call getFilters method" time="0.014" classname="GroupsComponent"/>
<testcase name="GroupsComponent should call onSelectUnidad method" time="0.009" classname="GroupsComponent"/>
<testcase name="GroupsComponent should call onTabChange method" time="0.011" classname="GroupsComponent"/>
<testcase name="GroupsComponent should create" time="0.013" classname="GroupsComponent"/>
<testcase name="GroupsComponent should call search method" time="0.011" classname="GroupsComponent"/>
<testcase name="ServerInfoDialogComponent should create" time="0.013" classname="ServerInfoDialogComponent"/>
<testcase name="UsersComponent should call search on init" time="0.032" classname="UsersComponent"/>
<testcase name="UsersComponent should have default values for pagination" time="0.006" classname="UsersComponent"/>
<testcase name="UsersComponent should create" time="0.008" classname="UsersComponent"/>
<testcase name="UsersComponent should define displayedColumns" time="0.007" classname="UsersComponent"/>
<testcase name="UsersComponent should initialize the dataSource" time="0.007" classname="UsersComponent"/>
<testcase name="OperativeSystemComponent should create" time="0.041" classname="OperativeSystemComponent"/>
<testcase name="RepositoriesComponent should create" time="0.037" classname="RepositoriesComponent"/>
<system-out>
<![CDATA[
<![CDATA[Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error fetching organizational units:', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/organizational-units?type=classroom&page=1&itemsPerPage=50', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/organizational-units?type=classroom&page=1&itemsPerPage=50: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
,Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error al sincronizar', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives/sync', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives/sync: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
,Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error fetching images', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
,Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error fetching og lives', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
]]>
</system-out>
<system-err/>