Merge pull request 'develop' (#10) from develop into main
Reviewed-on: #10pull/12/head^2 opengnsys_devel-0.0.20
13
CHANGELOG.md
|
@ -1,5 +1,11 @@
|
||||||
# Changelog
|
# 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
|
## [0.6.1] - 2024-11-19
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
|
@ -7,7 +13,6 @@
|
||||||
- Improve test coverage.
|
- Improve test coverage.
|
||||||
- New view for clients inside the classroom on the main page.
|
- New view for clients inside the classroom on the main page.
|
||||||
|
|
||||||
|
|
||||||
## [0.6.0] - 2024-11-19
|
## [0.6.0] - 2024-11-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -30,12 +35,8 @@
|
||||||
- Made predefined commands read-only to prevent accidental modifications.
|
- Made predefined commands read-only to prevent accidental modifications.
|
||||||
- Simplified the task creation modal to enhance user experience.
|
- Simplified the task creation modal to enhance user experience.
|
||||||
- Adjusted the translation system to cover new elements and improve consistency (work in progress).
|
- 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
|
### Fixed
|
||||||
- Resolved an issue that prevented editing software profiles correctly.
|
- Resolved an issue that prevented editing software profiles correctly.
|
||||||
- Fixed a bug where newly created commands failed to execute in the commands section.
|
- Fixed a bug where newly created commands failed to execute in the commands section.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -134,7 +134,8 @@
|
||||||
"src/assets"
|
"src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.css"
|
"src/styles.css",
|
||||||
|
"src/custom-theme.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,11 +40,10 @@ import {
|
||||||
MainRepositoryViewComponent
|
MainRepositoryViewComponent
|
||||||
} from "./components/repositories/main-repository-view/main-repository-view.component";
|
} from "./components/repositories/main-repository-view/main-repository-view.component";
|
||||||
import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
|
import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
|
||||||
|
import {MenusComponent} from "./components/menus/menus.component";
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
||||||
{
|
{ path: '', component: MainLayoutComponent,
|
||||||
path: '',
|
|
||||||
component: MainLayoutComponent,
|
|
||||||
children: [
|
children: [
|
||||||
{ path: 'dashboard', component: DashboardComponent },
|
{ path: 'dashboard', component: DashboardComponent },
|
||||||
{ path: 'admin', component: AdminComponent },
|
{ path: 'admin', component: AdminComponent },
|
||||||
|
@ -74,6 +73,7 @@ const routes: Routes = [
|
||||||
{ path: 'software', component: SoftwareComponent },
|
{ path: 'software', component: SoftwareComponent },
|
||||||
{ path: 'software-profiles', component: SoftwareProfileComponent },
|
{ path: 'software-profiles', component: SoftwareProfileComponent },
|
||||||
{ path: 'operative-systems', component: OperativeSystemComponent },
|
{ path: 'operative-systems', component: OperativeSystemComponent },
|
||||||
|
{ path: 'menus', component: MenusComponent },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 { 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 { CreateTaskComponent } from './components/commands/commands-task/create-task/create-task.component';
|
||||||
import { DetailTaskComponent } from './components/commands/commands-task/detail-task/detail-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 { 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 { 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 { StatusComponent } from './components/ogdhcp/og-dhcp-subnets/status/status.component';
|
||||||
import {MatSliderModule} from '@angular/material/slider';
|
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 { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||||
import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component';
|
import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component';
|
||||||
import { MatSortModule } from '@angular/material/sort';
|
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) {
|
export function HttpLoaderFactory(http: HttpClient) {
|
||||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||||
}
|
}
|
||||||
|
@ -181,10 +181,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||||
DetailCommandGroupComponent,
|
DetailCommandGroupComponent,
|
||||||
CreateTaskComponent,
|
CreateTaskComponent,
|
||||||
DetailTaskComponent,
|
DetailTaskComponent,
|
||||||
ClientTabViewComponent,
|
|
||||||
AdvancedSearchComponent,
|
|
||||||
TaskLogsComponent,
|
TaskLogsComponent,
|
||||||
OrganizationalUnitTabViewComponent,
|
|
||||||
ServerInfoDialogComponent,
|
ServerInfoDialogComponent,
|
||||||
StatusComponent,
|
StatusComponent,
|
||||||
ClientMainViewComponent,
|
ClientMainViewComponent,
|
||||||
|
@ -206,6 +203,9 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||||
MainRepositoryViewComponent,
|
MainRepositoryViewComponent,
|
||||||
ExecuteCommandOuComponent,
|
ExecuteCommandOuComponent,
|
||||||
EnvVarsComponent,
|
EnvVarsComponent,
|
||||||
|
MenusComponent,
|
||||||
|
CreateMenuComponent,
|
||||||
|
CreateMultipleClientComponent
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
imports: [BrowserModule,
|
imports: [BrowserModule,
|
||||||
|
|
|
@ -31,9 +31,11 @@
|
||||||
<ng-container *ngIf="column.columnDef !== 'readOnly'">
|
<ng-container *ngIf="column.columnDef !== 'readOnly'">
|
||||||
{{ column.cell(command) }}
|
{{ column.cell(command) }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="column.columnDef === 'readOnly'">
|
<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-icon [color]="command[column.columnDef] ? 'primary' : 'warn'">
|
||||||
<mat-chip *ngIf="!command.readOnly" class="mat-chip-readonly-false"><mat-icon style="color:white;">close</mat-icon></mat-chip>
|
{{ command[column.columnDef] ? 'check_circle' : 'cancel' }}
|
||||||
|
</mat-icon>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -41,7 +43,6 @@
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
<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 }}">
|
<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="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="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>
|
<button mat-icon-button color="warn" [disabled]="command.readOnly" (click)="deleteCommand($event, command)"><mat-icon>delete</mat-icon></button>
|
||||||
|
|
|
@ -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 {
|
onPageChange(event: any): void {
|
||||||
this.page = event.pageIndex;
|
this.page = event.pageIndex;
|
||||||
this.itemsPerPage = event.pageSize;
|
this.itemsPerPage = event.pageSize;
|
||||||
|
|
|
@ -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">
|
<mat-menu #commandMenu="matMenu">
|
||||||
<form [formGroup]="form" class="command-form">
|
<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>{{ '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-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>
|
|
||||||
|
|
|
@ -15,6 +15,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||||
import { DataService } from '../data.service';
|
import { DataService } from '../data.service';
|
||||||
|
import {MatIconModule} from "@angular/material/icon";
|
||||||
|
import {MatMenu, MatMenuModule} from "@angular/material/menu";
|
||||||
|
|
||||||
describe('ExecuteCommandComponent', () => {
|
describe('ExecuteCommandComponent', () => {
|
||||||
let component: ExecuteCommandComponent;
|
let component: ExecuteCommandComponent;
|
||||||
|
@ -31,9 +33,11 @@ describe('ExecuteCommandComponent', () => {
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatMenuModule,
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
MatTableModule,
|
MatTableModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
|
MatIconModule,
|
||||||
ToastrModule.forRoot(),
|
ToastrModule.forRoot(),
|
||||||
TranslateModule.forRoot()
|
TranslateModule.forRoot()
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { Component, Inject, OnInit } from '@angular/core';
|
import {Component, Inject, Input, OnInit} from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
|
import {Router} from "@angular/router";
|
||||||
|
import {ToastrService} from "ngx-toastr";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-execute-command',
|
selector: 'app-execute-command',
|
||||||
|
@ -9,92 +11,129 @@ import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
styleUrls: ['./execute-command.component.css']
|
styleUrls: ['./execute-command.component.css']
|
||||||
})
|
})
|
||||||
export class ExecuteCommandComponent implements OnInit {
|
export class ExecuteCommandComponent implements OnInit {
|
||||||
form: FormGroup;
|
@Input() clientData: any = {};
|
||||||
units: any[] = [];
|
|
||||||
childUnits: any[] = [];
|
|
||||||
clients: any[] = [];
|
|
||||||
selectedClients: any[] = [];
|
|
||||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
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(
|
constructor(
|
||||||
private dialogRef: MatDialogRef<ExecuteCommandComponent>,
|
private dialog: MatDialog,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
|
||||||
private http: HttpClient,
|
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 {
|
ngOnInit(): void {
|
||||||
this.loadUnits();
|
this.clientData = this.clientData || {};
|
||||||
this.form.get('unit')?.valueChanges.subscribe(value => this.onUnitChange(value));
|
this.loadClient(this.clientData)
|
||||||
this.form.get('childUnit')?.valueChanges.subscribe(value => this.onChildUnitChange(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadUnits(): void {
|
loadClient = (uuid: string) => {
|
||||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`).subscribe(
|
this.http.get<any>(`${this.baseUrl}${uuid}`).subscribe({
|
||||||
response => {
|
next: data => {
|
||||||
this.units = response['hydra:member'].filter((unit: { type: string; }) => unit.type === 'organizational-unit');
|
this.clientData = data;
|
||||||
|
this.loading = false;
|
||||||
},
|
},
|
||||||
error => console.error('Error fetching organizational units:', error)
|
error: error => {
|
||||||
);
|
console.error('Error al obtener el cliente:', error);
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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 {
|
onCommandSelect(action: any): void {
|
||||||
this.dialogRef.close(false);
|
if (action === 'partition') {
|
||||||
|
this.openPartitionAssistant();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleClientSelection(clientId: string): void {
|
if (action === 'create-image') {
|
||||||
const selectedClients = this.form.get('clientSelection')?.value;
|
this.openCreateImageAssistant();
|
||||||
if (selectedClients.includes(clientId)) {
|
}
|
||||||
this.form.get('clientSelection')?.setValue(selectedClients.filter((id: string) => id !== clientId));
|
|
||||||
} else {
|
if (action === 'deploy-image') {
|
||||||
this.form.get('clientSelection')?.setValue([...selectedClients, clientId]);
|
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');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
powerOnClient(): void {
|
||||||
|
const payload = {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>
|
|
|
@ -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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -257,5 +257,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,11 @@
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</div>
|
</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">
|
<div *ngIf="loading" class="loading-container">
|
||||||
<mat-spinner></mat-spinner>
|
<mat-spinner></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -122,6 +122,11 @@ export class ClientMainViewComponent implements OnInit {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigateToGroups() {
|
||||||
|
this.router.navigate(['/groups']);
|
||||||
|
}
|
||||||
|
|
||||||
updateGeneralData() {
|
updateGeneralData() {
|
||||||
this.generalData = [
|
this.generalData = [
|
||||||
{ property: 'Nombre', value: this.clientData?.name || '' },
|
{ property: 'Nombre', value: this.clientData?.name || '' },
|
||||||
|
@ -198,7 +203,7 @@ export class ClientMainViewComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPartitions(): void {
|
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 => {
|
next: data => {
|
||||||
this.dataSource = data['hydra:member'];
|
this.dataSource = data['hydra:member'];
|
||||||
this.partitions = data['hydra:member'];
|
this.partitions = data['hydra:member'];
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
|
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
||||||
<h2 class="title" i18n="@@subnetsTitle">Crear Imagen desde {{ clientName }}</h2>
|
<h2 class="title" i18n="@@subnetsTitle">Crear Imagen desde {{ clientName }}</h2>
|
||||||
<div class="subnets-button-row">
|
<div class="subnets-button-row">
|
||||||
<button mat-flat-button color="primary" (click)="save()">Guardar y ejecutar</button>
|
<button mat-flat-button color="primary" (click)="save()">Guardar y ejecutar</button>
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { ActivatedRoute } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { DataService } from '../../client-tab-view/data.service';
|
|
||||||
|
|
||||||
describe('CreateImageComponent', () => {
|
describe('CreateImageComponent', () => {
|
||||||
let component: CreateImageComponent;
|
let component: CreateImageComponent;
|
||||||
|
@ -42,7 +41,6 @@ describe('CreateImageComponent', () => {
|
||||||
providers: [
|
providers: [
|
||||||
FormBuilder,
|
FormBuilder,
|
||||||
ToastrService,
|
ToastrService,
|
||||||
DataService,
|
|
||||||
provideHttpClient(),
|
provideHttpClient(),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
{
|
{
|
||||||
|
|
|
@ -68,6 +68,7 @@ export class CreateImageComponent {
|
||||||
selectedImage: string | null = null;
|
selectedImage: string | null = null;
|
||||||
selectedPartition: any = null;
|
selectedPartition: any = null;
|
||||||
name: string = '';
|
name: string = '';
|
||||||
|
client: any = null;
|
||||||
dataSource = new MatTableDataSource<any>();
|
dataSource = new MatTableDataSource<any>();
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
|
@ -120,6 +121,7 @@ export class CreateImageComponent {
|
||||||
this.http.get(url).subscribe(
|
this.http.get(url).subscribe(
|
||||||
(response: any) => {
|
(response: any) => {
|
||||||
if (response.partitions) {
|
if (response.partitions) {
|
||||||
|
this.client = response;
|
||||||
this.clientName = response.name;
|
this.clientName = response.name;
|
||||||
|
|
||||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
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 {
|
save(): void {
|
||||||
const payload = {
|
const payload = {
|
||||||
client: `/clients/${this.clientId}`,
|
client: `/clients/${this.clientId}`,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
|
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
||||||
<h2 class="title" i18n="@@subnetsTitle">Desplegar imagen en {{ clientName }}</h2>
|
<h2 class="title" i18n="@@subnetsTitle">Desplegar imagen en {{ clientName }}</h2>
|
||||||
<div class="subnets-button-row">
|
<div class="subnets-button-row">
|
||||||
<button mat-flat-button color="primary" (click)="save()">Guardar</button>
|
<button mat-flat-button color="primary" (click)="save()">Guardar</button>
|
||||||
|
@ -54,9 +55,9 @@
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<h3 *ngIf="isMethod('multicast')" class="input-group">Opciones multicast</h3>
|
<h3 *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">Opciones multicast</h3>
|
||||||
<h3 *ngIf="isMethod('torrent')" class="input-group">Opciones torrent</h3>
|
<h3 *ngIf="isMethod('p2p')" class="input-group">Opciones torrent</h3>
|
||||||
<div *ngIf="isMethod('multicast')" class="input-group">
|
<div *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">
|
||||||
<mat-form-field appearance="fill" class="input-field">
|
<mat-form-field appearance="fill" class="input-field">
|
||||||
<mat-label>Puerto</mat-label>
|
<mat-label>Puerto</mat-label>
|
||||||
<input matInput [(ngModel)]="mcastPort" name="mcastPort">
|
<input matInput [(ngModel)]="mcastPort" name="mcastPort">
|
||||||
|
@ -92,7 +93,7 @@
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="isMethod('torrent')" class="input-group">
|
<div *ngIf="isMethod('p2p')" class="input-group">
|
||||||
<mat-form-field appearance="fill" class="input-field">
|
<mat-form-field appearance="fill" class="input-field">
|
||||||
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
|
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
|
||||||
<mat-select [(ngModel)]="p2pMode" name="p2pMode">
|
<mat-select [(ngModel)]="p2pMode" name="p2pMode">
|
||||||
|
|
|
@ -15,7 +15,6 @@ import { MatRadioModule } from '@angular/material/radio'; // Importar MatRadioMo
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||||
import { DataService } from '../../client-tab-view/data.service';
|
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
|
||||||
|
@ -45,7 +44,6 @@ describe('DeployImageComponent', () => {
|
||||||
providers: [
|
providers: [
|
||||||
FormBuilder,
|
FormBuilder,
|
||||||
ToastrService,
|
ToastrService,
|
||||||
DataService,
|
|
||||||
provideHttpClient(),
|
provideHttpClient(),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
|
|
|
@ -32,6 +32,7 @@ export class DeployImageComponent {
|
||||||
p2pMode: string = '';
|
p2pMode: string = '';
|
||||||
p2pTime: Number = 0;
|
p2pTime: Number = 0;
|
||||||
name: string = '';
|
name: string = '';
|
||||||
|
client: any = null;
|
||||||
|
|
||||||
protected p2pModeOptions = [
|
protected p2pModeOptions = [
|
||||||
{ name: 'Leecher', value: 'p2p-mode-leecher' },
|
{ name: 'Leecher', value: 'p2p-mode-leecher' },
|
||||||
|
@ -39,22 +40,25 @@ export class DeployImageComponent {
|
||||||
{ name: 'Seeder', value: 'p2p-mode-seeder' },
|
{ name: 'Seeder', value: 'p2p-mode-seeder' },
|
||||||
];
|
];
|
||||||
protected multicastModeOptions = [
|
protected multicastModeOptions = [
|
||||||
{"name": 'Half duplex', "value": "half-duplex"},
|
{ name: 'Half duplex', value: "half"},
|
||||||
{"name": 'Full duplex', "value": "full-duplex"},
|
{ name: 'Full duplex', value: "full"},
|
||||||
];
|
];
|
||||||
|
|
||||||
allMethods = [
|
allMethods = [
|
||||||
'multicast',
|
'uftp',
|
||||||
|
'udpcast',
|
||||||
'multicast-direct',
|
'multicast-direct',
|
||||||
'unicast',
|
'unicast',
|
||||||
'unicast-direct',
|
'unicast-direct',
|
||||||
'torrent'
|
'p2p'
|
||||||
];
|
];
|
||||||
|
|
||||||
updateCacheMethods = [
|
updateCacheMethods = [
|
||||||
|
'uftp',
|
||||||
|
'udpcast',
|
||||||
'multicast',
|
'multicast',
|
||||||
'unicast',
|
'unicast',
|
||||||
'torrent'
|
'p2p'
|
||||||
];
|
];
|
||||||
|
|
||||||
dataSource = new MatTableDataSource<any>();
|
dataSource = new MatTableDataSource<any>();
|
||||||
|
@ -116,6 +120,7 @@ export class DeployImageComponent {
|
||||||
this.http.get(url).subscribe(
|
this.http.get(url).subscribe(
|
||||||
(response: any) => {
|
(response: any) => {
|
||||||
if (response.partitions) {
|
if (response.partitions) {
|
||||||
|
this.client = response;
|
||||||
this.clientName = response.name;
|
this.clientName = response.name;
|
||||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||||
return partition.partitionNumber !== 0;
|
return partition.partitionNumber !== 0;
|
||||||
|
@ -146,6 +151,10 @@ export class DeployImageComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
back() {
|
||||||
|
this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
|
||||||
|
}
|
||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
if (!this.selectedImage) {
|
if (!this.selectedImage) {
|
||||||
this.toastService.error('Debe seleccionar una imagen');
|
this.toastService.error('Debe seleccionar una imagen');
|
||||||
|
@ -177,8 +186,7 @@ export class DeployImageComponent {
|
||||||
this.http.post(`${this.baseUrl}${this.selectedImage}/deploy-image`, payload)
|
this.http.post(`${this.baseUrl}${this.selectedImage}/deploy-image`, payload)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.toastService.success('Imagen creada exitosamente');
|
this.toastService.success('Petición de despliegue enviada correctamente');
|
||||||
this.router.navigate(['/commmands-logs'])
|
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
|
|
|
@ -162,3 +162,8 @@ button.remove-btn:hover {
|
||||||
align-content: center;
|
align-content: center;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disk-select {
|
||||||
|
padding: 20px;
|
||||||
|
margin: 10px auto;
|
||||||
|
}
|
||||||
|
|
|
@ -1,21 +1,28 @@
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
|
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
||||||
<h2 class="title" i18n="@@subnetsTitle">Asistente de particionado</h2>
|
<h2 class="title" i18n="@@subnetsTitle">Asistente de particionado</h2>
|
||||||
<div class="subnets-button-row">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
<div class="partition-assistant" *ngFor="let disk of disks; let i = index">
|
<div class="disk-select">
|
||||||
<div class="header">
|
<mat-form-field appearance="fill">
|
||||||
<label for="disk-number-{{ i }}">Disco {{ disk.diskNumber }}:</label>
|
<mat-label>Seleccionar disco</mat-label>
|
||||||
<span class="disk-size">Tamaño: {{ (disk.totalDiskSize / 1024).toFixed(2) }} GB</span>
|
<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>
|
||||||
|
|
||||||
|
<div class="partition-assistant" *ngIf="selectedDisk">
|
||||||
<div class="partition-bar">
|
<div class="partition-bar">
|
||||||
<div
|
<div
|
||||||
*ngFor="let partition of activePartitions(disk.diskNumber)"
|
*ngFor="let partition of activePartitions(selectedDisk.diskNumber)"
|
||||||
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
|
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
|
||||||
class="partition-segment"
|
class="partition-segment"
|
||||||
>
|
>
|
||||||
|
@ -23,7 +30,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<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>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -41,7 +48,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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">
|
<tr *ngIf="!partition.removed">
|
||||||
<td>{{ partition.partitionNumber }}</td>
|
<td>{{ partition.partitionNumber }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -62,21 +69,21 @@
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
[(ngModel)]="partition.size" required
|
[(ngModel)]="partition.size" required
|
||||||
(input)="updatePartitionSize(disk.diskNumber, j, partition.size)"
|
(input)="updatePartitionSize(selectedDisk.diskNumber, j, partition.size)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
[(ngModel)]="partition.percentage"
|
[(ngModel)]="partition.percentage"
|
||||||
(input)="updatePartitionSizeFromPercentage(disk.diskNumber, j, partition.percentage)"
|
(input)="updatePartitionSizeFromPercentage(selectedDisk.diskNumber, j, partition.percentage)"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" [(ngModel)]="partition.format" />
|
<input type="checkbox" [(ngModel)]="partition.format" />
|
||||||
</td>
|
</td>
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -87,7 +94,7 @@
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<ngx-charts-pie-chart
|
<ngx-charts-pie-chart
|
||||||
[view]="view"
|
[view]="view"
|
||||||
[results]="disk.chartData"
|
[results]="selectedDisk.chartData"
|
||||||
[doughnut]="true"
|
[doughnut]="true"
|
||||||
>
|
>
|
||||||
</ngx-charts-pie-chart>
|
</ngx-charts-pie-chart>
|
||||||
|
@ -96,5 +103,4 @@
|
||||||
</div>
|
</div>
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
|
||||||
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>
|
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>
|
||||||
|
|
|
@ -35,6 +35,7 @@ export class PartitionAssistantComponent implements OnInit {
|
||||||
originalPartitions: any[] = [];
|
originalPartitions: any[] = [];
|
||||||
clientId: string | null = null;
|
clientId: string | null = null;
|
||||||
newPartitions: any[] = [];
|
newPartitions: any[] = [];
|
||||||
|
selectedDiskNumber: number | null = null;
|
||||||
updateRequests: any[] = [];
|
updateRequests: any[] = [];
|
||||||
data: any = {};
|
data: any = {};
|
||||||
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = [];
|
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = [];
|
||||||
|
@ -57,6 +58,10 @@ export class PartitionAssistantComponent implements OnInit {
|
||||||
this.loadPartitions();
|
this.loadPartitions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get selectedDisk():any {
|
||||||
|
return this.disks.find(disk => disk.diskNumber === this.selectedDiskNumber) || null;
|
||||||
|
}
|
||||||
|
|
||||||
loadPartitions() {
|
loadPartitions() {
|
||||||
const url = `${this.baseUrl}/clients/${this.clientId}`;
|
const url = `${this.baseUrl}/clients/${this.clientId}`;
|
||||||
this.http.get(url).subscribe(
|
this.http.get(url).subscribe(
|
||||||
|
@ -245,31 +250,35 @@ export class PartitionAssistantComponent implements OnInit {
|
||||||
return modifiedPartitions;
|
return modifiedPartitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
back() {
|
||||||
const invalidDisks = this.disks.filter(disk => {
|
this.router.navigate(['clients', this.data.uuid], { state: { clientData: this.data } });
|
||||||
const totalPartitionSize = disk.partitions.reduce((sum, partition) => sum + partition.size, 0);
|
}
|
||||||
return totalPartitionSize > disk.totalDiskSize;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(invalidDisks);
|
save() {
|
||||||
if (invalidDisks.length > 0) {
|
if (!this.selectedDisk) {
|
||||||
this.errorMessage = 'El tamaño total de las particiones en uno o más discos excede el tamaño total del disco.';
|
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;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this.errorMessage = '';
|
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) {
|
if (modifiedPartitions.length === 0) {
|
||||||
this.errorMessage = 'No hay cambios para guardar.';
|
this.errorMessage = 'No hay cambios para guardar en el disco seleccionado.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modifiedPartitions.forEach(({ partition, diskNumber, partitionNumber }) => {
|
const newPartitions = modifiedPartitions.map((partition: { partitionNumber: any; memoryUsage: any; size: any; partitionCode: any; filesystem: any; uuid: any; removed: any; format: any; }) => ({
|
||||||
const payload = {
|
diskNumber: this.selectedDisk.diskNumber,
|
||||||
diskNumber: diskNumber,
|
partitionNumber: partition.partitionNumber,
|
||||||
partitionNumber: partitionNumber,
|
|
||||||
memoryUsage: partition.memoryUsage,
|
memoryUsage: partition.memoryUsage,
|
||||||
size: partition.size,
|
size: partition.size,
|
||||||
partitionCode: partition.partitionCode,
|
partitionCode: partition.partitionCode,
|
||||||
|
@ -278,27 +287,25 @@ export class PartitionAssistantComponent implements OnInit {
|
||||||
uuid: partition.uuid,
|
uuid: partition.uuid,
|
||||||
removed: partition.removed || false,
|
removed: partition.removed || false,
|
||||||
format: partition.format || false,
|
format: partition.format || false,
|
||||||
};
|
}));
|
||||||
|
|
||||||
this.newPartitions.push(payload);
|
if (newPartitions.length > 0) {
|
||||||
});
|
const bulkPayload = { partitions: newPartitions };
|
||||||
|
|
||||||
if (this.newPartitions.length > 0) {
|
|
||||||
const bulkPayload = { partitions: this.newPartitions };
|
|
||||||
|
|
||||||
this.http.post(this.apiUrl, bulkPayload).subscribe(
|
this.http.post(this.apiUrl, bulkPayload).subscribe(
|
||||||
(response) => {
|
(response) => {
|
||||||
this.toastService.success('Particiones creadas exitosamente');
|
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
|
||||||
this.router.navigate(['/commands-logs']);
|
this.router.navigate(['/commands-logs']);
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('Error al crear las particiones:', error);
|
console.error('Error al crear las particiones:', error);
|
||||||
this.toastService.error('Error al crear las particiones');
|
this.toastService.error('Error al crear las particiones.');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
removePartition(diskNumber: number, partition: Partition) {
|
removePartition(diskNumber: number, partition: Partition) {
|
||||||
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
|
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
|
||||||
if (disk) {
|
if (disk) {
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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>
|
|
|
@ -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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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>
|
|
|
@ -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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,9 +1,3 @@
|
||||||
.card-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-container {
|
.header-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -14,6 +8,12 @@
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-container-title {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.groups-button-row {
|
.groups-button-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
|
@ -189,10 +189,6 @@ mat-tree {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
mat-tree mat-tree-node {
|
mat-tree mat-tree-node {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -432,7 +428,7 @@ mat-tree mat-tree-node.disabled:hover {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 15px;
|
padding: 2px;
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,10 +443,6 @@ mat-tree mat-tree-node.disabled:hover {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-details {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-name {
|
.client-name {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
|
@ -471,8 +463,8 @@ button[mat-raised-button] {
|
||||||
|
|
||||||
.clients-grid {
|
.clients-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
gap: 16px;
|
gap: 8px; /* Espaciado reducido entre cards */
|
||||||
}
|
}
|
||||||
|
|
||||||
.clients-list {
|
.clients-list {
|
||||||
|
@ -495,12 +487,6 @@ button[mat-raised-button] {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-toggle-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clients-grid {
|
.clients-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -520,35 +506,16 @@ button[mat-raised-button] {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-image {
|
.client-image {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-toggle-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters-container {
|
.filters-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -584,3 +551,47 @@ button[mat-raised-button] {
|
||||||
align-items: center;
|
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;
|
||||||
|
}
|
|
@ -1,18 +1,24 @@
|
||||||
|
<!-- Header -->
|
||||||
<div class="header-container" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
|
<div class="header-container" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
|
||||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||||
<mat-icon>help</mat-icon>
|
<mat-icon>help</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<h2 class="title" joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
<div class="header-container-title">
|
||||||
|
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
||||||
{{ 'adminGroupsTitle' | translate }}
|
{{ 'adminGroupsTitle' | translate }}
|
||||||
</h2>
|
</h2>
|
||||||
|
</div>
|
||||||
<div class="groups-button-row" joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}">
|
<div class="groups-button-row" joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}">
|
||||||
<button mat-flat-button color="primary" (click)="addOU($event)"
|
<button mat-flat-button color="primary" (click)="addOU($event)"
|
||||||
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
|
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
|
||||||
{{ 'newOrganizationalUnitButton' | translate }}
|
{{ 'newOrganizationalUnitButton' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button mat-flat-button color="primary" (click)="addClient($event)" matTooltipShowDelay="1000">
|
<button mat-flat-button color="primary" [matMenuTriggerFor]="menuClients">{{ 'newClientButton' | translate }}</button>
|
||||||
{{ 'newClientButton' | translate }}
|
<mat-menu #menuClients="matMenu">
|
||||||
</button>
|
<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 }}"
|
<button mat-flat-button (click)="openBottomSheet()" joyrideStep="keyStep" text="{{ 'keyStepText' | translate }}"
|
||||||
matTooltipShowDelay="1000">
|
matTooltipShowDelay="1000">
|
||||||
{{ 'legendButton' | translate }}
|
{{ 'legendButton' | translate }}
|
||||||
|
@ -20,7 +26,8 @@
|
||||||
</div>
|
</div>
|
||||||
</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-expansion-panel-header>
|
||||||
<mat-panel-title>{{ 'filters' | translate }}</mat-panel-title>
|
<mat-panel-title>{{ 'filters' | translate }}</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
|
@ -49,74 +56,13 @@
|
||||||
<mat-option value="group">{{ 'computerGroups' | translate }}</mat-option>
|
<mat-option value="group">{{ 'computerGroups' | translate }}</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
|
||||||
<div *ngIf="!selectedUnidad; else detailsTemplate" class="card-container">
|
<!-- Unit details view-->
|
||||||
<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 }}
|
|
||||||
</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="main-container">
|
||||||
|
<!-- Tree view -->
|
||||||
<div class="tree-container">
|
<div class="tree-container">
|
||||||
<h2>{{ selectedUnidad?.name }}</h2>
|
|
||||||
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
||||||
<mat-tree-node [ngClass]="{'selected-node': node === selectedNode}" *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick(node)">
|
<mat-tree-node [ngClass]="{'selected-node': node === selectedNode}" *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick(node)">
|
||||||
<button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable" [ngClass]="{'disabled-toggle': !node.expandable}">
|
<button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable" [ngClass]="{'disabled-toggle': !node.expandable}">
|
||||||
|
@ -133,7 +79,7 @@
|
||||||
}}
|
}}
|
||||||
</mat-icon>
|
</mat-icon>
|
||||||
<span>{{ node.name }}</span>
|
<span>{{ node.name }}</span>
|
||||||
<button mat-icon-button [matMenuTriggerFor]="menu" (click)="setSelectedNode(node); $event.stopPropagation()">
|
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onNodeClick(node)">
|
||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-tree-node>
|
</mat-tree-node>
|
||||||
|
@ -153,24 +99,22 @@
|
||||||
<ng-container *ngIf="node.type === 'client'">
|
<ng-container *ngIf="node.type === 'client'">
|
||||||
<span> - IP: {{ node.ip }}</span>
|
<span> - IP: {{ node.ip }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<button mat-icon-button [matMenuTriggerFor]="menu" (click)="setSelectedNode(node)">
|
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onNodeClick(node)">
|
||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-tree-node>
|
</mat-tree-node>
|
||||||
</mat-tree>
|
</mat-tree>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-divider [vertical]="true"></mat-divider>
|
<mat-divider [vertical]="true"></mat-divider>
|
||||||
|
|
||||||
|
<!-- Tree node actions -->
|
||||||
<mat-menu restoreFocus=false #commandMenu="matMenu">
|
<mat-menu restoreFocus=false #commandMenu="matMenu">
|
||||||
<button mat-menu-item *ngFor="let command of commands" (click)="executeCommand(command, selectedNode)">
|
<button mat-menu-item *ngFor="let command of commands" (click)="executeCommand(command, selectedNode)">
|
||||||
<span>{{ command.name }}</span>
|
<span>{{ command.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
<mat-menu #menu="matMenu">
|
<mat-menu #menuNode="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)">
|
<button mat-menu-item (click)="onShowDetailsClick($event, selectedNode)">
|
||||||
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
|
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
|
||||||
<span>{{ 'viewUnitMenu' | translate }}</span>
|
<span>{{ 'viewUnitMenu' | translate }}</span>
|
||||||
|
@ -191,36 +135,52 @@
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
<span>{{ 'edit' | translate }}</span>
|
<span>{{ 'edit' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-menu-item (click)="selectedNode && onTreeClick($event, selectedNode)">
|
||||||
|
<mat-icon matTooltip="{{ 'viewTreeTooltip' | translate }}" matTooltipHideDelay="0">account_tree</mat-icon>
|
||||||
|
<span>{{ 'viewTreeMenu' | translate }}</span>
|
||||||
|
</button>
|
||||||
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)">
|
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span>{{ 'delete' | translate }}</span>
|
<span>{{ 'delete' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
<div class="clients-container" *ngIf="(selectedClients.data?.length || 0) > 0">
|
|
||||||
<h3>{{ 'clients' | translate }} {{ selectedNode?.name ? ('del ' + selectedNode?.name) : '' }}</h3>
|
<!-- 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="(selectedClients.data?.length || 0) > 0; else noClientsTemplate">
|
||||||
|
<!-- Cards view -->
|
||||||
<div class="clients-grid" *ngIf="currentView === 'card'">
|
<div class="clients-grid" *ngIf="currentView === 'card'">
|
||||||
<div *ngFor="let client of selectedClients.data" class="client-item">
|
<div *ngFor="let client of selectedClients.data" class="client-item">
|
||||||
<div class="client-card">
|
<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">
|
<div class="client-details">
|
||||||
<span class="client-name">{{ client.name }}</span>
|
<span class="client-name">{{ client.name }}</span>
|
||||||
<span class="client-ip">{{ client.ip }}</span>
|
<span class="client-ip">{{ client.ip }}</span>
|
||||||
<div class="flex">
|
<span class="client-ip">{{ client.mac }}</span>
|
||||||
<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>
|
<div class="action-icons">
|
||||||
<button
|
<button
|
||||||
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
|
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
|
||||||
mat-icon-button color="primary"
|
mat-icon-button color="primary"
|
||||||
(click)="getStatus(client)">
|
(click)="getStatus(client, selectedNode)">
|
||||||
<mat-icon>sync</mat-icon>
|
<mat-icon>sync</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -229,28 +189,46 @@
|
||||||
mat-icon-button color="primary">
|
mat-icon-button color="primary">
|
||||||
<mat-spinner diameter="24"></mat-spinner>
|
<mat-spinner diameter="24"></mat-spinner>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<button mat-raised-button color="primary" [matMenuTriggerFor]="clientMenu">Acciones</button>
|
<button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)">
|
||||||
<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)">
|
|
||||||
<mat-icon>visibility</mat-icon>
|
<mat-icon>visibility</mat-icon>
|
||||||
<span>{{ 'viewDetails' | translate }}</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item (click)="onDeleteClick($event, client, selectedNode)">
|
<app-execute-command [clientData]="client['@id']"></app-execute-command>
|
||||||
<mat-icon>delete</mat-icon>
|
|
||||||
<span>{{ 'delete' | translate }}</span>
|
|
||||||
</button>
|
|
||||||
</mat-menu>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- List view -->
|
||||||
<div class="clients-table" *ngIf="currentView === 'list'">
|
<div class="clients-table" *ngIf="currentView === 'list'">
|
||||||
<table mat-table matSort [dataSource]="selectedClients" class="mat-elevation-z8">
|
<table mat-table matSort [dataSource]="selectedClients" class="mat-elevation-z8">
|
||||||
|
<ng-container matColumnDef="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">
|
<ng-container matColumnDef="name">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
|
||||||
<td mat-cell *matCellDef="let client">
|
<td mat-cell *matCellDef="let client">
|
||||||
|
@ -263,38 +241,8 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="oglive">
|
<ng-container matColumnDef="oglive">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
|
||||||
<td mat-cell *matCellDef="let client"> {{ client.ogLive?.name }} </td>
|
<td mat-cell *matCellDef="let client"> {{ (client.ogLive?.filename || '').slice(0, 15) }}{{ (client.ogLive?.filename?.length > 15) ? '...' : '' }} </td>
|
||||||
</ng-container>
|
</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>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ng-container matColumnDef="maintenace">
|
<ng-container matColumnDef="maintenace">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'maintenance' | translate }} </th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'maintenance' | translate }} </th>
|
||||||
|
@ -320,22 +268,8 @@
|
||||||
<button mat-icon-button [matMenuTriggerFor]="clientMenu">
|
<button mat-icon-button [matMenuTriggerFor]="clientMenu">
|
||||||
<mat-icon>more_vert</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
<app-execute-command [clientData]="client['@id']"></app-execute-command>
|
||||||
|
|
||||||
|
|
||||||
<mat-menu #clientMenu="matMenu">
|
<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)">
|
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
<span>{{ 'edit' | translate }}</span>
|
<span>{{ 'edit' | translate }}</span>
|
||||||
|
@ -358,4 +292,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</ng-template>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -22,10 +22,8 @@ import { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { JoyrideModule } from 'ngx-joyride';
|
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 { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatTreeModule } from '@angular/material/tree';
|
||||||
|
|
||||||
describe('GroupsComponent', () => {
|
describe('GroupsComponent', () => {
|
||||||
let component: GroupsComponent;
|
let component: GroupsComponent;
|
||||||
|
@ -33,7 +31,7 @@ describe('GroupsComponent', () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
declarations: [GroupsComponent, AdvancedSearchComponent, ClientTabViewComponent, OrganizationalUnitTabViewComponent],
|
declarations: [GroupsComponent],
|
||||||
imports: [
|
imports: [
|
||||||
HttpClientTestingModule,
|
HttpClientTestingModule,
|
||||||
ToastrModule.forRoot(),
|
ToastrModule.forRoot(),
|
||||||
|
@ -56,6 +54,7 @@ describe('GroupsComponent', () => {
|
||||||
MatListModule,
|
MatListModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
|
MatTreeModule,
|
||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
JoyrideModule.forRoot(),
|
JoyrideModule.forRoot(),
|
||||||
],
|
],
|
||||||
|
@ -98,19 +97,4 @@ describe('GroupsComponent', () => {
|
||||||
component.getFilters();
|
component.getFilters();
|
||||||
expect(component.getFilters).toHaveBeenCalled();
|
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 { ShowOrganizationalUnitComponent } from './shared/organizational-units/show-organizational-unit/show-organizational-unit.component';
|
||||||
import { TreeViewComponent } from './shared/tree-view/tree-view.component';
|
import { TreeViewComponent } from './shared/tree-view/tree-view.component';
|
||||||
import { LegendComponent } from './shared/legend/legend.component';
|
import { LegendComponent } from './shared/legend/legend.component';
|
||||||
import { 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 { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||||
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
|
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
|
||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { MatPaginator } from '@angular/material/paginator';
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
|
import {CreateMultipleClientComponent} from "./shared/clients/create-multiple-client/create-multiple-client.component";
|
||||||
|
|
||||||
enum NodeType {
|
enum NodeType {
|
||||||
OrganizationalUnit = 'organizational-unit',
|
OrganizationalUnit = 'organizational-unit',
|
||||||
|
@ -45,6 +44,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
selectedUnidad: UnidadOrganizativa | null = null;
|
selectedUnidad: UnidadOrganizativa | null = null;
|
||||||
selectedDetail: UnidadOrganizativa | null = null;
|
selectedDetail: UnidadOrganizativa | null = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
|
isLoadingClients: boolean = false;
|
||||||
searchTerm = '';
|
searchTerm = '';
|
||||||
treeControl: FlatTreeControl<FlatNode>;
|
treeControl: FlatTreeControl<FlatNode>;
|
||||||
treeFlattener: MatTreeFlattener<TreeNode, FlatNode>;
|
treeFlattener: MatTreeFlattener<TreeNode, FlatNode>;
|
||||||
|
@ -63,7 +63,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
syncingClientId: string | null = null;
|
syncingClientId: string | null = null;
|
||||||
private originalTreeData: TreeNode[] = [];
|
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 _sort!: MatSort;
|
||||||
private _paginator!: MatPaginator;
|
private _paginator!: MatPaginator;
|
||||||
|
@ -83,8 +83,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
this.selectedClients.paginator = this._paginator;
|
this.selectedClients.paginator = this._paginator;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent;
|
|
||||||
@ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent;
|
|
||||||
|
|
||||||
private subscriptions: Subscription = new Subscription();
|
private subscriptions: Subscription = new Subscription();
|
||||||
|
|
||||||
|
@ -116,6 +114,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
this.search();
|
this.search();
|
||||||
this.getFilters();
|
this.getFilters();
|
||||||
this.updateGridCols();
|
this.updateGridCols();
|
||||||
|
this.loadOrganizationalUnits();
|
||||||
window.addEventListener('resize', this.updateGridCols);
|
window.addEventListener('resize', this.updateGridCols);
|
||||||
|
|
||||||
this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => {
|
this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => {
|
||||||
|
@ -135,6 +134,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private transformer = (node: TreeNode, level: number): FlatNode => ({
|
private transformer = (node: TreeNode, level: number): FlatNode => ({
|
||||||
|
id: node.id,
|
||||||
name: node.name,
|
name: node.name,
|
||||||
type: node.type,
|
type: node.type,
|
||||||
level,
|
level,
|
||||||
|
@ -144,6 +144,40 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
'@id': node['@id'],
|
'@id': node['@id'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private loadOrganizationalUnits(): void {
|
||||||
|
this.loading = true;
|
||||||
|
this.isLoadingClients = true;
|
||||||
|
this.dataService.getOrganizationalUnits().subscribe(
|
||||||
|
(data) => {
|
||||||
|
this.organizationalUnits = data;
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
if (this.organizationalUnits.length > 0) {
|
||||||
|
const treeData = this.organizationalUnits.map((unidad) => this.convertToTreeData(unidad));
|
||||||
|
this.treeDataSource.data = treeData.flat();
|
||||||
|
|
||||||
|
this.isTreeViewActive = true;
|
||||||
|
|
||||||
|
const firstNode = this.treeDataSource.data[0];
|
||||||
|
if (firstNode) {
|
||||||
|
this.selectedNode = firstNode;
|
||||||
|
this.fetchClientsForNode(firstNode);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.toastr.info('No existen unidades organizativas');
|
||||||
|
this.isTreeViewActive = false;
|
||||||
|
this.isLoadingClients = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error fetching organizational units', error);
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
toggleView(view: 'card' | 'list'): void {
|
toggleView(view: 'card' | 'list'): void {
|
||||||
this.currentView = view;
|
this.currentView = view;
|
||||||
}
|
}
|
||||||
|
@ -158,14 +192,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
this.selectedDetail = null;
|
this.selectedDetail = null;
|
||||||
this.selectedClients.data = [];
|
this.selectedClients.data = [];
|
||||||
this.isTreeViewActive = false;
|
this.isTreeViewActive = false;
|
||||||
}
|
this.selectedNode = null;
|
||||||
|
|
||||||
onTabChange(event: MatTabChangeEvent): void {
|
|
||||||
if (event.index === 2) {
|
|
||||||
this.clientTabComponent.search();
|
|
||||||
} else if (event.index === 3) {
|
|
||||||
this.organizationalUnitTabComponent.search();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilters(): void {
|
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> {
|
private async loadChildrenAndClients(id: string): Promise<UnidadOrganizativa> {
|
||||||
try {
|
try {
|
||||||
const childrenData = await this.dataService.getChildren(id).toPromise();
|
const childrenData = await this.dataService.getChildren(id).toPromise();
|
||||||
|
@ -268,6 +263,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private convertToTreeData(data: UnidadOrganizativa): TreeNode[] {
|
private convertToTreeData(data: UnidadOrganizativa): TreeNode[] {
|
||||||
const processNode = (node: UnidadOrganizativa): TreeNode => ({
|
const processNode = (node: UnidadOrganizativa): TreeNode => ({
|
||||||
|
id: node.id,
|
||||||
name: node.name,
|
name: node.name,
|
||||||
type: node.type,
|
type: node.type,
|
||||||
'@id': node['@id'],
|
'@id': node['@id'],
|
||||||
|
@ -279,34 +275,23 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
|
||||||
onNodeClick(node: TreeNode): void {
|
onNodeClick(node: TreeNode): void {
|
||||||
|
console.log('Node clicked:', node);
|
||||||
this.selectedNode = node;
|
this.selectedNode = node;
|
||||||
this.fetchClientsForNode(node);
|
this.fetchClientsForNode(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchClientsForNode(node: TreeNode): void {
|
private fetchClientsForNode(node: TreeNode): void {
|
||||||
if (node.hasClients && node['@id']) {
|
console.log('Node:', node);
|
||||||
this.subscriptions.add(
|
this.isLoadingClients = true;
|
||||||
this.http.get<{ clients: Client[] }>(`${this.baseUrl}${node['@id']}`).subscribe(
|
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({
|
||||||
(data) => {
|
next: (response) => {
|
||||||
const clientsWithParentName = (data.clients || []).map(client => ({
|
this.selectedClients.data = response['hydra:member'];
|
||||||
...client,
|
this.isLoadingClients = false;
|
||||||
parentName: node.name
|
|
||||||
}));
|
|
||||||
this.selectedClients.data = clientsWithParentName;
|
|
||||||
this.selectedClients._updateChangeSubscription();
|
|
||||||
if (this._paginator) {
|
|
||||||
this._paginator.firstPage();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
(error) => {
|
error: () => {
|
||||||
console.error('Error fetching clients:', error);
|
this.isLoadingClients = false;
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.selectedClients.data = [];
|
|
||||||
this.selectedClients._updateChangeSubscription();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeIcon(node: TreeNode): string {
|
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 {
|
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.subscriptions.add(
|
||||||
this.dataService.getOrganizationalUnits().subscribe(
|
this.dataService.getOrganizationalUnits().subscribe(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
@ -362,6 +367,15 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
const treeData = this.convertToTreeData(updatedData);
|
const treeData = this.convertToTreeData(updatedData);
|
||||||
this.originalTreeData = treeData[0]?.children || [];
|
this.originalTreeData = treeData[0]?.children || [];
|
||||||
this.treeDataSource.data = [...this.originalTreeData];
|
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 {
|
onEditNode(event: MouseEvent, node: TreeNode | null): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const uuid = node ? this.extractUuid(node['@id']) : null;
|
const uuid = node ? this.extractUuid(node['@id']) : null;
|
||||||
|
@ -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 {
|
onShowClientDetail(event: MouseEvent, client: Client): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.router.navigate(['clients', client.uuid], { state: { clientData: client } });
|
this.router.navigate(['clients', client.uuid], { state: { clientData: client } });
|
||||||
|
@ -511,7 +522,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
iniciarTour(): void {
|
iniciarTour(): void {
|
||||||
this.joyrideService.startTour({
|
this.joyrideService.startTour({
|
||||||
steps: ['groupsTitleStepText', 'addStep', 'keyStep', 'unitStep', 'elementsStep', 'tabsStep'],
|
steps: ['groupsTitleStepText', 'filtersPanelStep', 'addStep', 'keyStep', 'tabsStep'],
|
||||||
showPrevButton: true,
|
showPrevButton: true,
|
||||||
themeColor: '#3f51b5',
|
themeColor: '#3f51b5',
|
||||||
});
|
});
|
||||||
|
@ -563,7 +574,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
this.selectedNode = node;
|
this.selectedNode = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus(client: Client): void {
|
getStatus(client: Client, node: any): void {
|
||||||
if (!client.uuid || !client['@id']) return;
|
if (!client.uuid || !client['@id']) return;
|
||||||
|
|
||||||
this.syncingClientId = client.uuid;
|
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.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.toastr.success('Cliente actualizado correctamente');
|
this.toastr.success('Cliente actualizado correctamente');
|
||||||
this.search();
|
|
||||||
this.syncStatus = false;
|
this.syncStatus = false;
|
||||||
this.syncingClientId = null;
|
this.syncingClientId = null;
|
||||||
|
this.search()
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.toastr.error('Error de conexión con el cliente');
|
this.toastr.error('Error de conexión con el cliente');
|
||||||
this.syncStatus = false;
|
this.syncStatus = false;
|
||||||
this.syncingClientId = null;
|
this.syncingClientId = null;
|
||||||
|
this.search()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -60,6 +60,7 @@ export interface ClientCollection {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TreeNode {
|
export interface TreeNode {
|
||||||
|
id?: string
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
'@id'?: string;
|
'@id'?: string;
|
||||||
|
@ -70,6 +71,7 @@ export interface TreeNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FlatNode {
|
export interface FlatNode {
|
||||||
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
level: number;
|
level: number;
|
||||||
|
|
|
@ -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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 16px;
|
gap: 15px;
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h3, h4 {
|
.form-field {
|
||||||
margin: 0 0 16px;
|
width: 100%;
|
||||||
color: #333;
|
margin-top: 10px;
|
||||||
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 {
|
.mat-dialog-content {
|
||||||
flex: 1;
|
padding: 50px;
|
||||||
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 {
|
button {
|
||||||
flex: 1;
|
text-transform: none;
|
||||||
background-color: #f9f9f9;
|
font-size: 16px;
|
||||||
border-radius: 8px;
|
font-weight: 500;
|
||||||
padding: 16px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 16px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field {
|
.form-field {
|
||||||
width: 100%;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,78 +1,39 @@
|
||||||
<div class="create-client-container mat-elevation-z4">
|
<div class="create-client-container">
|
||||||
<h1>{{ 'addClientTitle' | translate }}s</h1>
|
<h1 mat-dialog-title i18n="@@add-client-dialog-title">Añadir Cliente</h1>
|
||||||
<div class="inputs-container">
|
|
||||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
<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">
|
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
<mat-label i18n="@@organizational-unit-label">Padre</mat-label>
|
||||||
<mat-select formControlName="organizationalUnit">
|
<mat-select formControlName="organizationalUnit">
|
||||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
|
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
|
||||||
<div class="unit-name">{{ unit.name }}</div>
|
<div class="unit-name">{{ unit.name }}</div>
|
||||||
|
<div class="unit-path">{{ unit.path }}</div>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</form>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<ng-container matColumnDef="ip">
|
|
||||||
<th mat-header-cell *matHeaderCellDef> IP </th>
|
|
||||||
<td mat-cell *matCellDef="let client"> {{ client.ip }} </td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Añadir uon cliente -->
|
|
||||||
|
|
||||||
<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-form-field class="form-field">
|
||||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
<mat-label i18n="@@name-label">Nombre</mat-label>
|
||||||
<input matInput formControlName="name">
|
<input matInput formControlName="name">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
<mat-label i18n="@@oglive-label">OgLive</mat-label>
|
||||||
<mat-select formControlName="ogLive">
|
<mat-select formControlName="ogLive">
|
||||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||||
{{ oglive.name }}
|
{{ oglive.filename }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'serialNumberLabel' | translate }}</mat-label>
|
<mat-label i18n="@@serial-number-label">Número de Serie</mat-label>
|
||||||
<input matInput formControlName="serialNumber">
|
<input matInput formControlName="serialNumber">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'netifaceLabel' | translate }}</mat-label>
|
<mat-label i18n="@@netiface-label">Interfaz de red</mat-label>
|
||||||
<mat-select formControlName="netiface">
|
<mat-select formControlName="netiface">
|
||||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||||
{{ type.name }}
|
{{ type.name }}
|
||||||
|
@ -81,7 +42,7 @@
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'netDriverLabel' | translate }}</mat-label>
|
<mat-label i18n="@@net-driver-label">Controlador de red</mat-label>
|
||||||
<mat-select formControlName="netDriver">
|
<mat-select formControlName="netDriver">
|
||||||
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
|
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
|
||||||
{{ type.name }}
|
{{ type.name }}
|
||||||
|
@ -90,21 +51,21 @@
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'macLabel' | translate }}</mat-label>
|
<mat-label i18n="@@mac-label">MAC</mat-label>
|
||||||
<mat-hint>{{ 'macHint' | translate }}</mat-hint>
|
<mat-hint i18n="@@mac-hint">Ejemplo: 00:11:22:33:44:55</mat-hint>
|
||||||
<input matInput formControlName="mac">
|
<input matInput formControlName="mac">
|
||||||
<mat-error>{{ 'macError' | translate }}</mat-error>
|
<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>
|
||||||
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'ipLabel' | translate }}</mat-label>
|
<mat-label i18n="@@ip-label">Dirección IP</mat-label>
|
||||||
<mat-hint>{{ 'ipHint' | translate }}</mat-hint>
|
<mat-hint i18n="@@ip-hint">Ejemplo: 127.0.0.1</mat-hint>
|
||||||
<input matInput formControlName="ip">
|
<input matInput formControlName="ip">
|
||||||
<mat-error>{{ 'ipError' | translate }}</mat-error>
|
<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>
|
||||||
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
<mat-label i18n="@@oglive-label">Plantilla PXE</mat-label>
|
||||||
<mat-select formControlName="template">
|
<mat-select formControlName="template">
|
||||||
<mat-option *ngFor="let template of templates" [value]="template['@id']">
|
<mat-option *ngFor="let template of templates" [value]="template['@id']">
|
||||||
{{ template.name }}
|
{{ template.name }}
|
||||||
|
@ -113,24 +74,38 @@
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
<mat-label i18n="@@hardware-profile-label">Perfil de Hardware</mat-label>
|
||||||
<mat-select formControlName="hardwareProfile">
|
<mat-select formControlName="hardwareProfile">
|
||||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">
|
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">
|
||||||
{{ unit.description }}
|
{{ unit.description }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
<mat-error>{{ 'hardwareProfileError' | translate }}</mat-error>
|
<mat-error i18n="@@hardware-profile-error">Formato de URL inválido.</mat-error>
|
||||||
|
</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>{{ '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>
|
</mat-form-field>
|
||||||
</form>
|
</form>
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div mat-dialog-actions align="end">
|
<div mat-dialog-actions align="end">
|
||||||
<button mat-button (click)="toggleClientForm()">
|
<button mat-button (click)="onNoClick()" i18n="@@cancel-button">Cancelar</button>
|
||||||
{{ isSingleClientForm ? 'Añadir múltiples clientes' : 'Añadir un único cliente' }}
|
<button mat-button [disabled]="!clientForm.valid" (click)="onSubmit()" i18n="@@add-button">Añadir</button>
|
||||||
</button>
|
|
||||||
<button mat-button color="warn" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
|
||||||
<button mat-button color="primary" (click)="onSubmit()">{{ 'addButton' | translate }}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { DataService } from '../../../services/data.service';
|
import { DataService } from '../../../services/data.service';
|
||||||
import * as Papa from 'papaparse';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create-client',
|
selector: 'app-create-client',
|
||||||
|
@ -18,12 +17,11 @@ export class CreateClientComponent implements OnInit {
|
||||||
parentUnits: any[] = [];
|
parentUnits: any[] = [];
|
||||||
hardwareProfiles: any[] = [];
|
hardwareProfiles: any[] = [];
|
||||||
ogLives: any[] = [];
|
ogLives: any[] = [];
|
||||||
|
menus: any[] = [];
|
||||||
templates: any[] = [];
|
templates: any[] = [];
|
||||||
uploadedClients: any[] = [];
|
repositories: any[] = [];
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
displayedColumns: string[] = ['name', 'ip'];
|
displayedColumns: string[] = ['name', 'ip'];
|
||||||
isSingleClientForm: boolean = false;
|
|
||||||
showTextarea: boolean = true;
|
|
||||||
protected netifaceTypes = [
|
protected netifaceTypes = [
|
||||||
{ name: 'Eth0', value: 'eth0' },
|
{ name: 'Eth0', value: 'eth0' },
|
||||||
{ name: 'Eth1', value: 'eth1' },
|
{ name: 'Eth1', value: 'eth1' },
|
||||||
|
@ -49,6 +47,8 @@ export class CreateClientComponent implements OnInit {
|
||||||
this.loadHardwareProfiles();
|
this.loadHardwareProfiles();
|
||||||
this.loadOgLives();
|
this.loadOgLives();
|
||||||
this.loadPxeTemplates();
|
this.loadPxeTemplates();
|
||||||
|
this.loadRepositories();
|
||||||
|
this.loadMenus()
|
||||||
}
|
}
|
||||||
|
|
||||||
initForm(): void {
|
initForm(): void {
|
||||||
|
@ -67,7 +67,9 @@ export class CreateClientComponent implements OnInit {
|
||||||
hardwareProfile: [
|
hardwareProfile: [
|
||||||
this.data.organizationalUnit?.networkSettings?.hardwareProfile?.['@id'] || null
|
this.data.organizationalUnit?.networkSettings?.hardwareProfile?.['@id'] || null
|
||||||
],
|
],
|
||||||
ogLive: [null]
|
ogLive: [null],
|
||||||
|
repository: [null],
|
||||||
|
menu: [null]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,115 +128,45 @@ export class CreateClientComponent implements OnInit {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileUpload(event: any): void {
|
loadMenus(): void {
|
||||||
const file = event.target.files[0];
|
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
|
||||||
if (file) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
reader.onload = (e: any) => {
|
this.http.get<any>(url).subscribe(
|
||||||
const textData = e.target.result;
|
response => {
|
||||||
const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g;
|
this.menus = response['hydra:member'];
|
||||||
let match;
|
},
|
||||||
const clients = [];
|
error => {
|
||||||
|
console.error('Error fetching menus:', error);
|
||||||
while ((match = regex.exec(textData)) !== null) {
|
}
|
||||||
clients.push({
|
);
|
||||||
name: match[1],
|
|
||||||
mac: match[2],
|
|
||||||
ip: match[3]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clients.length > 0) {
|
loadRepositories(): void {
|
||||||
this.uploadedClients = clients;
|
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
|
||||||
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);
|
this.http.get<any>(url).subscribe(
|
||||||
}
|
response => {
|
||||||
}
|
this.repositories = response['hydra:member'];
|
||||||
|
},
|
||||||
onTextarea(text: string): void {
|
error => {
|
||||||
const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g;
|
console.error('Error fetching ogLives:', error);
|
||||||
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 {
|
onSubmit(): void {
|
||||||
if (this.isSingleClientForm) {
|
|
||||||
if (this.clientForm.valid) {
|
if (this.clientForm.valid) {
|
||||||
const formData = this.clientForm.value;
|
const formData = this.clientForm.value;
|
||||||
console.log('Form data:', formData);
|
|
||||||
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
||||||
response => {
|
response => {
|
||||||
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
||||||
this.dialogRef.close(response);
|
this.dialogRef.close(response);
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
console.error('Error durante POST:', error);
|
|
||||||
this.toastService.error('Error al crear el cliente', '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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleClientForm(): void {
|
|
||||||
this.isSingleClientForm = !this.isSingleClientForm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onNoClick(): void {
|
onNoClick(): void {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,7 @@
|
||||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="ogLive">
|
<mat-select formControlName="ogLive">
|
||||||
<mat-option *ngFor="let ogLive of ogLives" [value]="ogLive['@id']">
|
<mat-option *ngFor="let ogLive of ogLives" [value]="ogLive['@id']">
|
||||||
{{ ogLive.name }}
|
{{ ogLive.filename }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
@ -88,6 +88,15 @@
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ export class EditClientComponent {
|
||||||
repositories: any[] = [];
|
repositories: any[] = [];
|
||||||
ogLives: any[] = [];
|
ogLives: any[] = [];
|
||||||
templates: any[] = [];
|
templates: any[] = [];
|
||||||
|
menus: any[] = [];
|
||||||
isEditMode: boolean;
|
isEditMode: boolean;
|
||||||
protected netifaceTypes = [
|
protected netifaceTypes = [
|
||||||
{ "name": 'Eth0', "value": "eth0" },
|
{ "name": 'Eth0', "value": "eth0" },
|
||||||
|
@ -50,6 +51,7 @@ export class EditClientComponent {
|
||||||
this.loadOgLives();
|
this.loadOgLives();
|
||||||
this.loadPxeTemplates()
|
this.loadPxeTemplates()
|
||||||
this.loadRepositories();
|
this.loadRepositories();
|
||||||
|
this.loadMenus()
|
||||||
this.clientForm = this.fb.group({
|
this.clientForm = this.fb.group({
|
||||||
organizationalUnit: [null, Validators.required],
|
organizationalUnit: [null, Validators.required],
|
||||||
name: ['', Validators.required],
|
name: ['', Validators.required],
|
||||||
|
@ -62,6 +64,7 @@ export class EditClientComponent {
|
||||||
hardwareProfile: null,
|
hardwareProfile: null,
|
||||||
ogLive: null,
|
ogLive: null,
|
||||||
repository: 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 {
|
loadRepositories(): void {
|
||||||
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
|
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
|
||||||
|
|
||||||
|
@ -146,6 +162,7 @@ export class EditClientComponent {
|
||||||
repository: data.repository ? data.repository['@id'] : null,
|
repository: data.repository ? data.repository['@id'] : null,
|
||||||
ogLive: data.ogLive ? data.ogLive['@id'] : null,
|
ogLive: data.ogLive ? data.ogLive['@id'] : null,
|
||||||
template: data.template ? data.template['@id'] : null,
|
template: data.template ? data.template['@id'] : null,
|
||||||
|
menu: data.menu ? data.menu['@id'] : null,
|
||||||
});
|
});
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="type" required>
|
<mat-select formControlName="type" required>
|
||||||
<mat-option *ngFor="let type of types" [value]="type">
|
<mat-option *ngFor="let type of filteredTypes" [value]="type">
|
||||||
{{ typeTranslations[type] }}
|
{{ typeTranslations[type] }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
|
@ -88,7 +88,7 @@
|
||||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="oglive" (selectionChange)="onOgLiveChange($event)">
|
<mat-select formControlName="oglive" (selectionChange)="onOgLiveChange($event)">
|
||||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||||
{{ oglive.name }}
|
{{ oglive.filename }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
|
@ -29,10 +29,9 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
||||||
{ name: 'Peer', value: 'p2p-mode-peer' },
|
{ name: 'Peer', value: 'p2p-mode-peer' },
|
||||||
{ name: 'Seeder', value: 'p2p-mode-seeder' },
|
{ name: 'Seeder', value: 'p2p-mode-seeder' },
|
||||||
];
|
];
|
||||||
multicastModeOptions: { name: string, value: string }[] = [
|
protected multicastModeOptions = [
|
||||||
{ name: 'Modo 1', value: 'mode1' },
|
{"name": 'Half duplex', "value": "half"},
|
||||||
{ name: 'Modo 2', value: 'mode2' },
|
{"name": 'Full duplex', "value": "full"},
|
||||||
{ name: 'Modo 3', value: 'mode3' },
|
|
||||||
];
|
];
|
||||||
parentUnits: any[] = [];
|
parentUnits: any[] = [];
|
||||||
hardwareProfiles: any[] = [];
|
hardwareProfiles: any[] = [];
|
||||||
|
@ -41,6 +40,7 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
||||||
repositories: any[] = [];
|
repositories: any[] = [];
|
||||||
selectedCalendarUuid: string | null = null;
|
selectedCalendarUuid: string | null = null;
|
||||||
|
|
||||||
|
|
||||||
@Output() unitAdded = new EventEmitter();
|
@Output() unitAdded = new EventEmitter();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -97,6 +97,10 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
||||||
this.loadRepositories();
|
this.loadRepositories();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get filteredTypes(): string[] {
|
||||||
|
return this.generalFormGroup.get('parent')?.value ? this.types.filter(type => type !== 'organizational-unit') : this.types;
|
||||||
|
}
|
||||||
|
|
||||||
loadParentUnits() {
|
loadParentUnits() {
|
||||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
|
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
|
||||||
this.http.get<any>(url).subscribe(
|
this.http.get<any>(url).subscribe(
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="type" required>
|
<mat-select formControlName="type" required>
|
||||||
<mat-option *ngFor="let type of types" [value]="type">{{ type }}</mat-option>
|
<mat-option *ngFor="let type of filteredTypes" [value]="type">{{ type }}</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="form-field">
|
<mat-form-field class="form-field">
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
||||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||||
{{ oglive.name }}
|
{{ oglive.filename }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
|
@ -31,8 +31,8 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
||||||
{"name": 'Seeder', "value": "p2p-mode-seeder"},
|
{"name": 'Seeder', "value": "p2p-mode-seeder"},
|
||||||
];
|
];
|
||||||
protected multicastModeOptions = [
|
protected multicastModeOptions = [
|
||||||
{"name": 'Half duplex', "value": "half-duplex"},
|
{"name": 'Half duplex', "value": "half"},
|
||||||
{"name": 'Full duplex', "value": "full-duplex"},
|
{"name": 'Full duplex', "value": "full"},
|
||||||
];
|
];
|
||||||
@Output() unitAdded = new EventEmitter();
|
@Output() unitAdded = new EventEmitter();
|
||||||
calendars: any;
|
calendars: any;
|
||||||
|
@ -98,6 +98,10 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
||||||
this.loadRepositories();
|
this.loadRepositories();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get filteredTypes(): string[] {
|
||||||
|
return this.generalFormGroup.get('parent')?.value ? this.types.filter(type => type !== 'organizational-unit') : this.types;
|
||||||
|
}
|
||||||
|
|
||||||
loadParentUnits() {
|
loadParentUnits() {
|
||||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;
|
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,5 @@
|
||||||
<h2 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Añadir' }} imagen ogLive</h2>
|
<h2 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Añadir' }} imagen ogLive</h2>
|
||||||
<mat-dialog-content>
|
<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-spinner class="spinner" *ngIf="loading"></mat-spinner>
|
||||||
|
|
||||||
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">
|
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { ToastrService } from 'ngx-toastr';
|
||||||
})
|
})
|
||||||
export class CreatePXEImageComponent implements OnInit {
|
export class CreatePXEImageComponent implements OnInit {
|
||||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||||
name: string = '';
|
|
||||||
downloads: any[] = [];
|
downloads: any[] = [];
|
||||||
selectedDownload: any;
|
selectedDownload: any;
|
||||||
isEditMode: boolean = false;
|
isEditMode: boolean = false;
|
||||||
|
@ -28,7 +27,6 @@ export class CreatePXEImageComponent implements OnInit {
|
||||||
this.fetchDownloads();
|
this.fetchDownloads();
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.isEditMode = true;
|
this.isEditMode = true;
|
||||||
this.name = this.data.name;
|
|
||||||
this.selectedDownload = this.data.downloadUrl;
|
this.selectedDownload = this.data.downloadUrl;
|
||||||
this.imageId = this.data.uuid; // Assuming UUID is used for edit
|
this.imageId = this.data.uuid; // Assuming UUID is used for edit
|
||||||
}
|
}
|
||||||
|
@ -55,7 +53,6 @@ export class CreatePXEImageComponent implements OnInit {
|
||||||
|
|
||||||
submitForm(): void {
|
submitForm(): void {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: this.name,
|
|
||||||
downloadUrl: this.selectedDownload.url
|
downloadUrl: this.selectedDownload.url
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
<button mat-icon-button color="info" (click)="showOgLive($event, image)">
|
<button mat-icon-button color="info" (click)="showOgLive($event, image)">
|
||||||
<mat-icon>{{ 'viewIcon' | translate }}</mat-icon>
|
<mat-icon>{{ 'viewIcon' | translate }}</mat-icon>
|
||||||
</button>
|
</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>
|
<mat-icon>{{ 'editIcon' | translate }}</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button mat-icon-button color="warn" (click)="deleteImage(image)">
|
<button mat-icon-button color="warn" (click)="deleteImage(image)">
|
||||||
|
|
|
@ -40,9 +40,9 @@ export class PXEimagesComponent implements OnInit {
|
||||||
cell: (user: any) => `${user.id}`
|
cell: (user: any) => `${user.id}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'name',
|
columnDef: 'filename',
|
||||||
header: 'Nombre de imagen',
|
header: 'Og Live',
|
||||||
cell: (user: any) => `${user.name}`
|
cell: (user: any) => `${user.filename}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
columnDef: 'isDefault',
|
columnDef: 'isDefault',
|
||||||
|
|
|
@ -128,5 +128,4 @@ export class RepositoriesComponent {
|
||||||
themeColor: '#3f51b5'
|
themeColor: '#3f51b5'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,17 +151,10 @@
|
||||||
</span>
|
</span>
|
||||||
</mat-list-item>
|
</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">
|
<span class="entry">
|
||||||
<mat-icon class="icon">list</mat-icon>
|
<mat-icon class="icon">list</mat-icon>
|
||||||
<span>{{ 'menus' | translate }}</span>
|
<span>{{ 'menus' | translate }}</span>
|
||||||
</span>
|
</span>
|
||||||
</mat-list-item>
|
</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>
|
</mat-nav-list>
|
||||||
|
|
After Width: | Height: | Size: 778 B |
After Width: | Height: | Size: 762 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 729 B |
After Width: | Height: | Size: 746 B |
After Width: | Height: | Size: 901 B |
After Width: | Height: | Size: 778 B |
After Width: | Height: | Size: 756 B |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 747 B |
|
@ -211,6 +211,7 @@
|
||||||
"mcastPortLabel": "Multicast Port",
|
"mcastPortLabel": "Multicast Port",
|
||||||
"mcastModeLabel": "Multicast Mode",
|
"mcastModeLabel": "Multicast Mode",
|
||||||
"menuUrlLabel": "Menu URL",
|
"menuUrlLabel": "Menu URL",
|
||||||
|
"menuLabel": "Menu",
|
||||||
"hardwareProfileLabel": "Hardware Profile",
|
"hardwareProfileLabel": "Hardware Profile",
|
||||||
"urlFormatError": "Invalid URL format.",
|
"urlFormatError": "Invalid URL format.",
|
||||||
"validationToggle": "Validation",
|
"validationToggle": "Validation",
|
||||||
|
@ -445,5 +446,9 @@
|
||||||
"maintenance": "Maintenance",
|
"maintenance": "Maintenance",
|
||||||
"subnet": "Subnet",
|
"subnet": "Subnet",
|
||||||
"parent": "Parent",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -363,6 +363,7 @@
|
||||||
"selectOptionPlaceholder": "Selecciona una opción",
|
"selectOptionPlaceholder": "Selecciona una opción",
|
||||||
"yesOption": "Sí",
|
"yesOption": "Sí",
|
||||||
"noOption": "No",
|
"noOption": "No",
|
||||||
|
"menuLabel": "Menu",
|
||||||
"actionsColumn": "Acciones",
|
"actionsColumn": "Acciones",
|
||||||
"createServerButton": "Crear servidor",
|
"createServerButton": "Crear servidor",
|
||||||
"labelName": "Nombre",
|
"labelName": "Nombre",
|
||||||
|
@ -447,5 +448,9 @@
|
||||||
"maintenance": "Mantenimiento",
|
"maintenance": "Mantenimiento",
|
||||||
"subnet": "Subred",
|
"subnet": "Subred",
|
||||||
"parent": "Padre",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,535 +1,94 @@
|
||||||
<?xml version="1.0"?>
|
<?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>
|
<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"/>
|
<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>
|
</properties>
|
||||||
<testcase name="CreateOperativeSystemComponent should create" time="0.071" classname="CreateOperativeSystemComponent"/>
|
<testcase name="CreateSoftwareComponent should create" time="0.12" classname="CreateSoftwareComponent"/>
|
||||||
<testcase name="AddClientsToPxeComponent should create" time="0.016" classname="AddClientsToPxeComponent">
|
<testcase name="CommandsComponent should create" time="0.104" classname="CommandsComponent"/>
|
||||||
<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
|
<testcase name="OgbootStatusComponent should create the component" time="0.054" classname="OgbootStatusComponent"/>
|
||||||
error properties: Object({ code: -302 })
|
<testcase name="PxeComponent should have a defined currentPage" time="0.069" classname="PxeComponent"/>
|
||||||
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
|
<testcase name="PxeComponent should have a defined itemsPerPage" time="0.041" classname="PxeComponent"/>
|
||||||
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
|
<testcase name="PxeComponent should have a defined component" time="0.035" classname="PxeComponent"/>
|
||||||
at AddClientsToPxeComponent_Template (ng:///AddClientsToPxeComponent.js:70:9)
|
<testcase name="PxeComponent should have a defined loading" time="0.037" classname="PxeComponent"/>
|
||||||
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
|
<testcase name="PxeComponent should have a defined page" time="0.036" classname="PxeComponent"/>
|
||||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
|
<testcase name="PxeComponent should have a defined selectedElements" time="0.032" classname="PxeComponent"/>
|
||||||
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
|
<testcase name="PxeComponent should have a defined pageSizeOptions" time="0.034" classname="PxeComponent"/>
|
||||||
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
|
<testcase name="PxeComponent should have a defined selectedItem" time="0.031" classname="PxeComponent"/>
|
||||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
|
<testcase name="PxeComponent should have a defined length" time="0.029" classname="PxeComponent"/>
|
||||||
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
|
<testcase name="PxeComponent should have a defined datePipe" time="0.028" classname="PxeComponent"/>
|
||||||
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
|
<testcase name="PxeComponent should have a defined dataSource" time="0.029" classname="PxeComponent"/>
|
||||||
</failure>
|
<testcase name="PxeComponent should have a defined baseUrl" time="0.029" classname="PxeComponent"/>
|
||||||
</testcase>
|
<testcase name="PxeComponent should have a defined alertMessage" time="0.025" classname="PxeComponent"/>
|
||||||
<testcase name="ClientsComponent should create" time="0.011" classname="ClientsComponent">
|
<testcase name="PxeComponent should have a defined pxeTemplates" time="0.027" classname="PxeComponent"/>
|
||||||
<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
|
<testcase name="PxeComponent should have a defined filters" time="0.029" classname="PxeComponent"/>
|
||||||
error properties: Object({ code: -302 })
|
<testcase name="PxeComponent should create the component" time="0.025" classname="PxeComponent"/>
|
||||||
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
|
<testcase name="AdminComponent debería renderizar dos botones" time="0.012" classname="AdminComponent"/>
|
||||||
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
|
<testcase name="AdminComponent debería tener un botón con routerLink a "/users"" time="0.005" classname="AdminComponent"/>
|
||||||
at ClientsComponent_Template (ng:///ClientsComponent.js:77:9)
|
<testcase name="AdminComponent debería aplicar la clase "fab-button" a ambos botones" time="0.005" classname="AdminComponent"/>
|
||||||
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
|
<testcase name="AdminComponent debería crear el componente" time="0.006" classname="AdminComponent"/>
|
||||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
|
<testcase name="CreateOperativeSystemComponent should create" time="0.017" classname="CreateOperativeSystemComponent"/>
|
||||||
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
|
<testcase name="PxeBootFilesComponent should create" time="0.048" classname="PxeBootFilesComponent"/>
|
||||||
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
|
<testcase name="CalendarComponent should create" time="0.05" classname="CalendarComponent"/>
|
||||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
|
<testcase name="CreateRepositoryComponent should create" time="0.018" classname="CreateRepositoryComponent"/>
|
||||||
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
|
<testcase name="PXEimagesComponent should create" time="0.077" classname="PXEimagesComponent"/>
|
||||||
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
|
<testcase name="SoftwareComponent should create" time="0.055" classname="SoftwareComponent"/>
|
||||||
</failure>
|
<testcase name="CreateCommandComponent should create" time="0.033" classname="CreateCommandComponent"/>
|
||||||
</testcase>
|
<testcase name="AppComponent should default to Spanish if no language is saved in localStorage" time="0.01" classname="AppComponent"/>
|
||||||
<testcase name="SoftwareComponent should create" time="0.019" classname="SoftwareComponent">
|
<testcase name="AppComponent should create the app" time="0.003" classname="AppComponent"/>
|
||||||
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -> JoyrideService]:
|
<testcase name="AppComponent should set language to Spanish in sessionStorage on ngOnInit" time="0.002" classname="AppComponent"/>
|
||||||
NullInjectorError: No provider for JoyrideService!
|
<testcase name="AppComponent should have as title 'ogWebconsole'" time="0.003" classname="AppComponent"/>
|
||||||
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
|
<testcase name="AppComponent should set the language from localStorage on creation" time="0.003" classname="AppComponent"/>
|
||||||
NullInjectorError: NullInjectorError: No provider for JoyrideService!
|
<testcase name="DeployImageComponent should create" time="0.08" classname="DeployImageComponent"/>
|
||||||
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
|
<testcase name="CreateSoftwareProfileComponent should create" time="0.085" classname="CreateSoftwareProfileComponent"/>
|
||||||
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
|
<testcase name="OgDhcpSubnetsComponent should create" time="0.078" classname="OgDhcpSubnetsComponent"/>
|
||||||
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
|
<testcase name="OgDhcpSubnetsComponent should call syncSubnets and handle success" time="0.044" classname="OgDhcpSubnetsComponent"/>
|
||||||
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
|
<testcase name="SoftwareProfileComponent should create" time="0.051" classname="SoftwareProfileComponent"/>
|
||||||
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
|
<testcase name="RolesComponent should create" time="0.021" classname="RolesComponent"/>
|
||||||
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
|
<testcase name="RolesComponent should initialize the dataSource" time="0.005" classname="RolesComponent"/>
|
||||||
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
|
<testcase name="RolesComponent should have a defined columns array" time="0.005" classname="RolesComponent"/>
|
||||||
at NodeInjectorFactory.SoftwareComponent_Factory [as factory] (ng:///SoftwareComponent/ɵfac.js:6:52)
|
<testcase name="RolesComponent should have a default itemsPerPage value" time="0.007" classname="RolesComponent"/>
|
||||||
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
|
<testcase name="EnvVarsComponent should create" time="0.019" classname="EnvVarsComponent"/>
|
||||||
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
|
<testcase name="MainRepositoryViewComponent should create" time="0.071" classname="MainRepositoryViewComponent"/>
|
||||||
</failure>
|
<testcase name="StatusComponent should create" time="0.022" classname="StatusComponent"/>
|
||||||
</testcase>
|
<testcase name="CommandsTaskComponent should create" time="0.039" classname="CommandsTaskComponent"/>
|
||||||
<testcase name="OgDhcpSubnetsComponent should create" time="0.02" classname="OgDhcpSubnetsComponent">
|
<testcase name="LoginComponent should add rotating class to the logo when loading" time="0.025" classname="LoginComponent"/>
|
||||||
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -> JoyrideService]:
|
<testcase name="LoginComponent should create" time="0.01" classname="LoginComponent"/>
|
||||||
NullInjectorError: No provider for JoyrideService!
|
<testcase name="LoginComponent should add "invalid" class to username input if it is invalid and touched" time="0.011" classname="LoginComponent"/>
|
||||||
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
|
<testcase name="LoginComponent should show a success toast message" time="0.01" classname="LoginComponent"/>
|
||||||
NullInjectorError: NullInjectorError: No provider for JoyrideService!
|
<testcase name="LoginComponent should not add rotating class to the logo when not loading" time="0.01" classname="LoginComponent"/>
|
||||||
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
|
<testcase name="LoginComponent should show error message on login failure" time="0.01" classname="LoginComponent"/>
|
||||||
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
|
<testcase name="LoginComponent should toggle password visibility when clicking the button" time="0.01" classname="LoginComponent"/>
|
||||||
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
|
<testcase name="LoginComponent should disable the login button if username or password is missing" time="0.01" classname="LoginComponent"/>
|
||||||
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
|
<testcase name="LoginComponent should change language to English and save to localStorage" time="0.009" classname="LoginComponent"/>
|
||||||
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
|
<testcase name="LoginComponent should call onLogin and navigate on successful login" time="0.012" classname="LoginComponent"/>
|
||||||
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
|
<testcase name="LoginComponent should enable the login button if username and password are present" time="0.01" classname="LoginComponent"/>
|
||||||
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
|
<testcase name="LoginComponent should change language to Spanish and save to localStorage" time="0.01" classname="LoginComponent"/>
|
||||||
at NodeInjectorFactory.OgDhcpSubnetsComponent_Factory [as factory] (ng:///OgDhcpSubnetsComponent/ɵfac.js:6:7)
|
<testcase name="LoginComponent should show an error toast message" time="0.01" classname="LoginComponent"/>
|
||||||
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
|
<testcase name="ExecuteCommandComponent should create" time="0.025" classname="ExecuteCommandComponent"/>
|
||||||
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
|
<testcase name="CreateImageComponent should create" time="0.025" classname="CreateImageComponent"/>
|
||||||
</failure>
|
<testcase name="OgdhcpComponent should create" time="0.005" classname="OgdhcpComponent"/>
|
||||||
</testcase>
|
<testcase name="DashboardComponent should create the component" time="0.006" classname="DashboardComponent"/>
|
||||||
<testcase name="CreateCommandComponent should create" time="0.012" classname="CreateCommandComponent">
|
<testcase name="GroupsComponent should call getFilters on ngOnInit" time="0.141" classname="GroupsComponent"/>
|
||||||
<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
|
<testcase name="GroupsComponent should call search on ngOnInit" time="0.011" classname="GroupsComponent"/>
|
||||||
error properties: Object({ code: -302 })
|
<testcase name="GroupsComponent should call getFilters method" time="0.014" classname="GroupsComponent"/>
|
||||||
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
|
<testcase name="GroupsComponent should call onSelectUnidad method" time="0.009" classname="GroupsComponent"/>
|
||||||
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
|
<testcase name="GroupsComponent should call onTabChange method" time="0.011" classname="GroupsComponent"/>
|
||||||
at CreateCommandComponent_Template (ng:///CreateCommandComponent.js:15:9)
|
<testcase name="GroupsComponent should create" time="0.013" classname="GroupsComponent"/>
|
||||||
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
|
<testcase name="GroupsComponent should call search method" time="0.011" classname="GroupsComponent"/>
|
||||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
|
<testcase name="ServerInfoDialogComponent should create" time="0.013" classname="ServerInfoDialogComponent"/>
|
||||||
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
|
<testcase name="UsersComponent should call search on init" time="0.032" classname="UsersComponent"/>
|
||||||
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
|
<testcase name="UsersComponent should have default values for pagination" time="0.006" classname="UsersComponent"/>
|
||||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
|
<testcase name="UsersComponent should create" time="0.008" classname="UsersComponent"/>
|
||||||
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
|
<testcase name="UsersComponent should define displayedColumns" time="0.007" classname="UsersComponent"/>
|
||||||
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
|
<testcase name="UsersComponent should initialize the dataSource" time="0.007" classname="UsersComponent"/>
|
||||||
</failure>
|
<testcase name="OperativeSystemComponent should create" time="0.041" classname="OperativeSystemComponent"/>
|
||||||
</testcase>
|
<testcase name="RepositoriesComponent should create" time="0.037" classname="RepositoriesComponent"/>
|
||||||
<testcase name="RepositoriesComponent should create" time="0.012" classname="RepositoriesComponent">
|
|
||||||
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[HttpClient -> 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 -> 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 -> 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 -> 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 -> 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 -> 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 -> 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 -> 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 -> 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 "Roles"" 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 "Usuarios"" 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 -> TranslateStore -> 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 -> 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 -> 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 -> 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 -> TranslateStore -> 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 -> 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 -> 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 -> 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 (<anonymous>)
|
|
||||||
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.<anonymous> (http://localhost:9876/_karma_webpack_/main.js:3164:70)
|
|
||||||
at Generator.next (<anonymous>)
|
|
||||||
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 -> 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>
|
|
||||||
<system-out>
|
<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-out>
|
||||||
<system-err/>
|
<system-err/>
|
||||||
|
|