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
|
||||
|
||||
## [0.7.0] - 2024-12-10
|
||||
|
||||
### Refactored
|
||||
- Refactored the group screen, removing the separate tabs for clients, advanced search, and organizational units.
|
||||
- Added support for partitioning functionality in the client detail view.
|
||||
|
||||
## [0.6.1] - 2024-11-19
|
||||
|
||||
### Improved
|
||||
|
@ -7,7 +13,6 @@
|
|||
- Improve test coverage.
|
||||
- New view for clients inside the classroom on the main page.
|
||||
|
||||
|
||||
## [0.6.0] - 2024-11-19
|
||||
|
||||
### Added
|
||||
|
@ -30,12 +35,8 @@
|
|||
- Made predefined commands read-only to prevent accidental modifications.
|
||||
- Simplified the task creation modal to enhance user experience.
|
||||
- Adjusted the translation system to cover new elements and improve consistency (work in progress).
|
||||
- New element view from clients on groups main view
|
||||
- New element view from clients on groups main view.
|
||||
|
||||
### Fixed
|
||||
- Resolved an issue that prevented editing software profiles correctly.
|
||||
- Fixed a bug where newly created commands failed to execute in the commands section.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
|
|
@ -134,7 +134,8 @@
|
|||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
"src/styles.css",
|
||||
"src/custom-theme.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
|
|
|
@ -40,50 +40,50 @@ import {
|
|||
MainRepositoryViewComponent
|
||||
} from "./components/repositories/main-repository-view/main-repository-view.component";
|
||||
import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
|
||||
import {MenusComponent} from "./components/menus/menus.component";
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
||||
{
|
||||
path: '',
|
||||
component: MainLayoutComponent,
|
||||
children: [
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: 'admin', component: AdminComponent },
|
||||
{ path: 'users', component: UsersComponent },
|
||||
{ path: 'env-vars', component: EnvVarsComponent },
|
||||
{ path: 'user-groups', component: RolesComponent },
|
||||
{ path: 'groups', component: GroupsComponent },
|
||||
{ path: 'pxe-images', component: PXEimagesComponent },
|
||||
{ path: 'pxe', component: PxeComponent },
|
||||
{ path: 'pxe-boot-file', component: PxeBootFilesComponent },
|
||||
{ path: 'ogboot-status', component: OgbootStatusComponent },
|
||||
{ path: 'dhcp', component: OgdhcpComponent },
|
||||
{ path: 'subnets', component: OgDhcpSubnetsComponent },
|
||||
{ path: 'ogdhcp-status', component: StatusComponent },
|
||||
{ path: 'commands', component: CommandsComponent },
|
||||
{ path: 'commands-groups', component: CommandsGroupsComponent },
|
||||
{ path: 'commands-task', component: CommandsTaskComponent },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent },
|
||||
{ path: 'calendars', component: CalendarComponent },
|
||||
{ path: 'clients/:id', component: ClientMainViewComponent },
|
||||
{ path: 'clients/:id/partition-assistant', component: PartitionAssistantComponent },
|
||||
{ path: 'clients/:id/create-image', component: CreateImageComponent },
|
||||
{ path: 'clients/:id/deploy-image', component: DeployImageComponent },
|
||||
{ path: 'images', component: ImagesComponent },
|
||||
{ path: 'repositories', component: RepositoriesComponent },
|
||||
{ path: 'repository/:id', component: MainRepositoryViewComponent },
|
||||
{ path: 'software', component: SoftwareComponent },
|
||||
{ path: 'software-profiles', component: SoftwareProfileComponent },
|
||||
{ path: 'operative-systems', component: OperativeSystemComponent },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
component: AuthLayoutComponent,
|
||||
children: [
|
||||
{ path: 'login', component: LoginComponent },
|
||||
],
|
||||
},
|
||||
{ path: '**', component: PageNotFoundComponent },
|
||||
{ path: '', component: MainLayoutComponent,
|
||||
children: [
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: 'admin', component: AdminComponent },
|
||||
{ path: 'users', component: UsersComponent },
|
||||
{ path: 'env-vars', component: EnvVarsComponent },
|
||||
{ path: 'user-groups', component: RolesComponent },
|
||||
{ path: 'groups', component: GroupsComponent },
|
||||
{ path: 'pxe-images', component: PXEimagesComponent },
|
||||
{ path: 'pxe', component: PxeComponent },
|
||||
{ path: 'pxe-boot-file', component: PxeBootFilesComponent },
|
||||
{ path: 'ogboot-status', component: OgbootStatusComponent },
|
||||
{ path: 'dhcp', component: OgdhcpComponent },
|
||||
{ path: 'subnets', component: OgDhcpSubnetsComponent },
|
||||
{ path: 'ogdhcp-status', component: StatusComponent },
|
||||
{ path: 'commands', component: CommandsComponent },
|
||||
{ path: 'commands-groups', component: CommandsGroupsComponent },
|
||||
{ path: 'commands-task', component: CommandsTaskComponent },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent },
|
||||
{ path: 'calendars', component: CalendarComponent },
|
||||
{ path: 'clients/:id', component: ClientMainViewComponent },
|
||||
{ path: 'clients/:id/partition-assistant', component: PartitionAssistantComponent },
|
||||
{ path: 'clients/:id/create-image', component: CreateImageComponent },
|
||||
{ path: 'clients/:id/deploy-image', component: DeployImageComponent },
|
||||
{ path: 'images', component: ImagesComponent },
|
||||
{ path: 'repositories', component: RepositoriesComponent },
|
||||
{ path: 'repository/:id', component: MainRepositoryViewComponent },
|
||||
{ path: 'software', component: SoftwareComponent },
|
||||
{ path: 'software-profiles', component: SoftwareProfileComponent },
|
||||
{ path: 'operative-systems', component: OperativeSystemComponent },
|
||||
{ path: 'menus', component: MenusComponent },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
component: AuthLayoutComponent,
|
||||
children: [
|
||||
{ path: 'login', component: LoginComponent },
|
||||
],
|
||||
},
|
||||
{ path: '**', component: PageNotFoundComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -95,10 +95,7 @@ import { CreateCommandGroupComponent } from './components/commands/commands-grou
|
|||
import { DetailCommandGroupComponent } from './components/commands/commands-groups/detail-command-group/detail-command-group.component';
|
||||
import { CreateTaskComponent } from './components/commands/commands-task/create-task/create-task.component';
|
||||
import { DetailTaskComponent } from './components/commands/commands-task/detail-task/detail-task.component';
|
||||
import { ClientTabViewComponent } from './components/groups/components/client-tab-view/client-tab-view.component';
|
||||
import { AdvancedSearchComponent } from './components/groups/components/advanced-search/advanced-search.component';
|
||||
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
|
||||
import { OrganizationalUnitTabViewComponent } from './components/groups/components/organizational-unit-tab-view/organizational-unit-tab-view.component';
|
||||
import { ServerInfoDialogComponent } from './components/ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component';
|
||||
import { StatusComponent } from './components/ogdhcp/og-dhcp-subnets/status/status.component';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
|
@ -124,6 +121,9 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
|||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MenusComponent } from './components/menus/menus.component';
|
||||
import { CreateMenuComponent } from './components/menus/create-menu/create-menu.component';
|
||||
import { CreateMultipleClientComponent } from './components/groups/shared/clients/create-multiple-client/create-multiple-client.component';
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
}
|
||||
|
@ -181,10 +181,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
DetailCommandGroupComponent,
|
||||
CreateTaskComponent,
|
||||
DetailTaskComponent,
|
||||
ClientTabViewComponent,
|
||||
AdvancedSearchComponent,
|
||||
TaskLogsComponent,
|
||||
OrganizationalUnitTabViewComponent,
|
||||
ServerInfoDialogComponent,
|
||||
StatusComponent,
|
||||
ClientMainViewComponent,
|
||||
|
@ -206,6 +203,9 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
MainRepositoryViewComponent,
|
||||
ExecuteCommandOuComponent,
|
||||
EnvVarsComponent,
|
||||
MenusComponent,
|
||||
CreateMenuComponent,
|
||||
CreateMultipleClientComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
|
|
@ -31,9 +31,11 @@
|
|||
<ng-container *ngIf="column.columnDef !== 'readOnly'">
|
||||
{{ column.cell(command) }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'readOnly'">
|
||||
<mat-chip *ngIf="command.readOnly" class="mat-chip-readonly-true"><mat-icon style="color:white;">check</mat-icon></mat-chip>
|
||||
<mat-chip *ngIf="!command.readOnly" class="mat-chip-readonly-false"><mat-icon style="color:white;">close</mat-icon></mat-chip>
|
||||
<mat-icon [color]="command[column.columnDef] ? 'primary' : 'warn'">
|
||||
{{ command[column.columnDef] ? 'check_circle' : 'cancel' }}
|
||||
</mat-icon>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
@ -41,7 +43,6 @@
|
|||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let command" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||
<button mat-icon-button color="info" (click)="executeCommand($event, command)"><mat-icon>play_arrow</mat-icon></button>
|
||||
<button mat-icon-button color="info" (click)="viewDetails($event, command)"><mat-icon>visibility</mat-icon></button>
|
||||
<button mat-icon-button color="primary" [disabled]="command.readOnly" (click)="editCommand($event, command)"><mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" [disabled]="command.readOnly" (click)="deleteCommand($event, command)"><mat-icon>delete</mat-icon></button>
|
||||
|
|
|
@ -50,7 +50,7 @@ export class CommandsComponent implements OnInit {
|
|||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
private apiUrl = `${this.baseUrl}/commands`;
|
||||
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
private joyrideService: JoyrideService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -114,19 +114,6 @@ export class CommandsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
executeCommand(event: MouseEvent, command: any): void {
|
||||
this.dialog.open(ExecuteCommandComponent, {
|
||||
width: '50%',
|
||||
data: { commandData: command }
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
console.log('Comando ejecutado con éxito');
|
||||
} else {
|
||||
console.log('Ejecución de comando cancelada');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
|
@ -147,5 +134,5 @@ export class CommandsComponent implements OnInit {
|
|||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,40 +1,10 @@
|
|||
<h2 mat-dialog-title>{{ 'executeCommandTitle' | translate }}</h2>
|
||||
<button mat-icon-button color="primary" [matMenuTriggerFor]="commandMenu">
|
||||
<mat-icon>terminal</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="form" class="command-form">
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="unit">
|
||||
<mat-option *ngFor="let unit of units" [value]="unit.uuid">{{ unit.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-menu #commandMenu="matMenu">
|
||||
<button mat-menu-item [disabled]="command.disabled" *ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)">
|
||||
{{ command.name }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'subOrganizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="childUnit">
|
||||
<mat-option *ngFor="let child of childUnits" [value]="child.uuid">{{ child.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<label>{{ 'clientsLabel' | translate }}</label>
|
||||
<div *ngIf="clients.length > 0">
|
||||
<mat-checkbox *ngFor="let client of clients"
|
||||
(change)="toggleClientSelection(client.uuid)"
|
||||
[checked]="form.get('clientSelection')?.value.includes(client.uuid)">
|
||||
{{ client.name }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div *ngIf="clients.length === 0">
|
||||
<p>{{ 'noClientsAvailable' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="closeModal()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button mat-button (click)="executeCommand()" [disabled]="!form.get('clientSelection')?.value.length">{{ 'buttonExecute' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -10,11 +10,13 @@ import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/materia
|
|||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from '../data.service';
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatMenu, MatMenuModule} from "@angular/material/menu";
|
||||
|
||||
describe('ExecuteCommandComponent', () => {
|
||||
let component: ExecuteCommandComponent;
|
||||
|
@ -31,9 +33,11 @@ describe('ExecuteCommandComponent', () => {
|
|||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
BrowserAnimationsModule,
|
||||
MatTableModule,
|
||||
MatSelectModule,
|
||||
MatIconModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {Component, Inject, Input, OnInit} from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import {Router} from "@angular/router";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-execute-command',
|
||||
|
@ -9,92 +11,129 @@ import { FormBuilder, FormGroup } from '@angular/forms';
|
|||
styleUrls: ['./execute-command.component.css']
|
||||
})
|
||||
export class ExecuteCommandComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
units: any[] = [];
|
||||
childUnits: any[] = [];
|
||||
clients: any[] = [];
|
||||
selectedClients: any[] = [];
|
||||
@Input() clientData: any = {};
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
loading: boolean = true;
|
||||
|
||||
arrayCommands: any[] = [
|
||||
{name: 'Enceder', slug: 'power-on', disabled: false},
|
||||
{name: 'Apagar', slug: 'power-off', disabled: false},
|
||||
{name: 'Reiniciar', slug: 'reboot', disabled: false},
|
||||
{name: 'Iniciar Sesión', slug: 'login', disabled: true},
|
||||
{name: 'Crear Image', slug: 'create-image', disabled: false},
|
||||
{name: 'Deploy Image', slug: 'deploy-image', disabled: false},
|
||||
{name: 'Eliminar Imagen Cache', slug: 'delete-image-cache', disabled: true},
|
||||
{name: 'Particionar y Formatear', slug: 'partition', disabled: false},
|
||||
{name: 'Inventario Software', slug: 'software-inventory', disabled: true},
|
||||
{name: 'Inventario Hardware', slug: 'hardware-inventory', disabled: true},
|
||||
{name: 'Ejecutar script', slug: 'run-script', disabled: true},
|
||||
];
|
||||
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<ExecuteCommandComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
private fb: FormBuilder
|
||||
private fb: FormBuilder,
|
||||
private router: Router,
|
||||
private toastService: ToastrService
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
unit: [null],
|
||||
childUnit: [null],
|
||||
clientSelection: [[]]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadUnits();
|
||||
this.form.get('unit')?.valueChanges.subscribe(value => this.onUnitChange(value));
|
||||
this.form.get('childUnit')?.valueChanges.subscribe(value => this.onChildUnitChange(value));
|
||||
this.clientData = this.clientData || {};
|
||||
this.loadClient(this.clientData)
|
||||
}
|
||||
|
||||
loadUnits(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`).subscribe(
|
||||
response => {
|
||||
this.units = response['hydra:member'].filter((unit: { type: string; }) => unit.type === 'organizational-unit');
|
||||
loadClient = (uuid: string) => {
|
||||
this.http.get<any>(`${this.baseUrl}${uuid}`).subscribe({
|
||||
next: data => {
|
||||
this.clientData = data;
|
||||
this.loading = false;
|
||||
},
|
||||
error => console.error('Error fetching organizational units:', error)
|
||||
error: error => {
|
||||
console.error('Error al obtener el cliente:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCommandSelect(action: any): void {
|
||||
if (action === 'partition') {
|
||||
this.openPartitionAssistant();
|
||||
}
|
||||
|
||||
if (action === 'create-image') {
|
||||
this.openCreateImageAssistant();
|
||||
}
|
||||
|
||||
if (action === 'deploy-image') {
|
||||
this.openDeployImageAssistant();
|
||||
}
|
||||
|
||||
if (action === 'reboot') {
|
||||
this.rebootClient();
|
||||
}
|
||||
|
||||
if (action === 'power-off') {
|
||||
this.powerOffClient();
|
||||
}
|
||||
|
||||
if (action === 'power-on') {
|
||||
this.powerOnClient();
|
||||
}
|
||||
}
|
||||
|
||||
rebootClient(): void {
|
||||
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/reboot`, {}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onUnitChange(unitId: string): void {
|
||||
const unit = this.units.find(unit => unit.uuid === unitId);
|
||||
this.childUnits = unit ? this.getAllChildren(unit) : [];
|
||||
this.clients = [];
|
||||
this.form.patchValue({ childUnit: null, clientSelection: [] });
|
||||
}
|
||||
|
||||
getAllChildren(unit: any): any[] {
|
||||
let allChildren = [];
|
||||
if (unit.children && unit.children.length > 0) {
|
||||
for (const child of unit.children) {
|
||||
allChildren.push(child);
|
||||
allChildren = allChildren.concat(this.getAllChildren(child));
|
||||
}
|
||||
}
|
||||
return allChildren;
|
||||
}
|
||||
|
||||
onChildUnitChange(childUnitId: string): void {
|
||||
const childUnit = this.childUnits.find(unit => unit.uuid === childUnitId);
|
||||
this.clients = childUnit && childUnit.clients ? childUnit.clients : [];
|
||||
this.form.patchValue({ clientSelection: [] });
|
||||
}
|
||||
|
||||
executeCommand(): void {
|
||||
powerOnClient(): void {
|
||||
const payload = {
|
||||
clients: ['/clients/'+this.form.get('clientSelection')?.value]
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/commands/${this.data.commandData.uuid}/execute`, payload)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
console.log('Comando ejecutado con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al ejecutar el comando:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeModal(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
toggleClientSelection(clientId: string): void {
|
||||
const selectedClients = this.form.get('clientSelection')?.value;
|
||||
if (selectedClients.includes(clientId)) {
|
||||
this.form.get('clientSelection')?.setValue(selectedClients.filter((id: string) => id !== clientId));
|
||||
} else {
|
||||
this.form.get('clientSelection')?.setValue([...selectedClients, clientId]);
|
||||
client: this.clientData['@id']
|
||||
}
|
||||
|
||||
this.http.post(`${this.baseUrl}${this.clientData.repository['@id']}/wol`, payload).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
powerOffClient(): void {
|
||||
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/power-off`, {}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
openPartitionAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/partition-assistant`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
||||
openCreateImageAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/create-image`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
||||
openDeployImageAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/deploy-image`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
.back-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,11 @@
|
|||
</mat-menu>
|
||||
</div>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="navigateToGroups()" class="back-button">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
{{ 'Back' | translate }}
|
||||
</button>
|
||||
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
|
|
|
@ -122,6 +122,11 @@ export class ClientMainViewComponent implements OnInit {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
navigateToGroups() {
|
||||
this.router.navigate(['/groups']);
|
||||
}
|
||||
|
||||
updateGeneralData() {
|
||||
this.generalData = [
|
||||
{ property: 'Nombre', value: this.clientData?.name || '' },
|
||||
|
@ -198,7 +203,7 @@ export class ClientMainViewComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadPartitions(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[partitionNumber]=ASC`).subscribe({
|
||||
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[diskNumber, partitionNumber]=ASC`).subscribe({
|
||||
next: data => {
|
||||
this.dataSource = data['hydra:member'];
|
||||
this.partitions = data['hydra:member'];
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<div class="header-container">
|
||||
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
||||
<h2 class="title" i18n="@@subnetsTitle">Crear Imagen desde {{ clientName }}</h2>
|
||||
<div class="subnets-button-row">
|
||||
<button mat-flat-button color="primary" (click)="save()">Guardar y ejecutar</button>
|
||||
|
|
|
@ -16,7 +16,6 @@ import { ActivatedRoute } from '@angular/router';
|
|||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||
import { of } from 'rxjs';
|
||||
import { DataService } from '../../client-tab-view/data.service';
|
||||
|
||||
describe('CreateImageComponent', () => {
|
||||
let component: CreateImageComponent;
|
||||
|
@ -42,7 +41,6 @@ describe('CreateImageComponent', () => {
|
|||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
|
|
|
@ -68,6 +68,7 @@ export class CreateImageComponent {
|
|||
selectedImage: string | null = null;
|
||||
selectedPartition: any = null;
|
||||
name: string = '';
|
||||
client: any = null;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
columns = [
|
||||
{
|
||||
|
@ -120,6 +121,7 @@ export class CreateImageComponent {
|
|||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
this.client = response;
|
||||
this.clientName = response.name;
|
||||
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
|
@ -145,6 +147,10 @@ export class CreateImageComponent {
|
|||
);
|
||||
}
|
||||
|
||||
back() {
|
||||
this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const payload = {
|
||||
client: `/clients/${this.clientId}`,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<div class="header-container">
|
||||
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
||||
<h2 class="title" i18n="@@subnetsTitle">Desplegar imagen en {{ clientName }}</h2>
|
||||
<div class="subnets-button-row">
|
||||
<button mat-flat-button color="primary" (click)="save()">Guardar</button>
|
||||
|
@ -54,9 +55,9 @@
|
|||
</table>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
<h3 *ngIf="isMethod('multicast')" class="input-group">Opciones multicast</h3>
|
||||
<h3 *ngIf="isMethod('torrent')" class="input-group">Opciones torrent</h3>
|
||||
<div *ngIf="isMethod('multicast')" class="input-group">
|
||||
<h3 *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">Opciones multicast</h3>
|
||||
<h3 *ngIf="isMethod('p2p')" class="input-group">Opciones torrent</h3>
|
||||
<div *ngIf="isMethod('udpcast') || isMethod('uftp')" class="input-group">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Puerto</mat-label>
|
||||
<input matInput [(ngModel)]="mcastPort" name="mcastPort">
|
||||
|
@ -92,7 +93,7 @@
|
|||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isMethod('torrent')" class="input-group">
|
||||
<div *ngIf="isMethod('p2p')" class="input-group">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
|
||||
<mat-select [(ngModel)]="p2pMode" name="p2pMode">
|
||||
|
|
|
@ -15,7 +15,6 @@ import { MatRadioModule } from '@angular/material/radio'; // Importar MatRadioMo
|
|||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from '../../client-tab-view/data.service';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
|
||||
|
@ -28,7 +27,7 @@ describe('DeployImageComponent', () => {
|
|||
declarations: [DeployImageComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
|
@ -36,7 +35,7 @@ describe('DeployImageComponent', () => {
|
|||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatDividerModule,
|
||||
MatRadioModule,
|
||||
MatRadioModule,
|
||||
MatSelectModule,
|
||||
BrowserAnimationsModule,
|
||||
ToastrModule.forRoot(),
|
||||
|
@ -45,7 +44,6 @@ describe('DeployImageComponent', () => {
|
|||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
provideRouter([]),
|
||||
|
|
|
@ -32,6 +32,7 @@ export class DeployImageComponent {
|
|||
p2pMode: string = '';
|
||||
p2pTime: Number = 0;
|
||||
name: string = '';
|
||||
client: any = null;
|
||||
|
||||
protected p2pModeOptions = [
|
||||
{ name: 'Leecher', value: 'p2p-mode-leecher' },
|
||||
|
@ -39,22 +40,25 @@ export class DeployImageComponent {
|
|||
{ name: 'Seeder', value: 'p2p-mode-seeder' },
|
||||
];
|
||||
protected multicastModeOptions = [
|
||||
{"name": 'Half duplex', "value": "half-duplex"},
|
||||
{"name": 'Full duplex', "value": "full-duplex"},
|
||||
{ name: 'Half duplex', value: "half"},
|
||||
{ name: 'Full duplex', value: "full"},
|
||||
];
|
||||
|
||||
allMethods = [
|
||||
'multicast',
|
||||
'uftp',
|
||||
'udpcast',
|
||||
'multicast-direct',
|
||||
'unicast',
|
||||
'unicast-direct',
|
||||
'torrent'
|
||||
'p2p'
|
||||
];
|
||||
|
||||
updateCacheMethods = [
|
||||
'uftp',
|
||||
'udpcast',
|
||||
'multicast',
|
||||
'unicast',
|
||||
'torrent'
|
||||
'p2p'
|
||||
];
|
||||
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
|
@ -116,6 +120,7 @@ export class DeployImageComponent {
|
|||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
this.client = response;
|
||||
this.clientName = response.name;
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0;
|
||||
|
@ -146,6 +151,10 @@ export class DeployImageComponent {
|
|||
);
|
||||
}
|
||||
|
||||
back() {
|
||||
this.router.navigate(['clients', this.clientId], { state: { clientData: this.client} });
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.selectedImage) {
|
||||
this.toastService.error('Debe seleccionar una imagen');
|
||||
|
@ -177,8 +186,7 @@ export class DeployImageComponent {
|
|||
this.http.post(`${this.baseUrl}${this.selectedImage}/deploy-image`, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Imagen creada exitosamente');
|
||||
this.router.navigate(['/commmands-logs'])
|
||||
this.toastService.success('Petición de despliegue enviada correctamente');
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error:', error);
|
||||
|
|
|
@ -162,3 +162,8 @@ button.remove-btn:hover {
|
|||
align-content: center;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.disk-select {
|
||||
padding: 20px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,28 @@
|
|||
<div class="header-container">
|
||||
<button mat-flat-button color="primary" (click)="back()">Volver</button>
|
||||
<h2 class="title" i18n="@@subnetsTitle">Asistente de particionado</h2>
|
||||
<div class="subnets-button-row">
|
||||
<button mat-flat-button color="primary" (click)="save()">Ejecutar</button>
|
||||
<button mat-flat-button color="primary" [disabled]="data.status === 'busy'" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="partition-assistant" *ngFor="let disk of disks; let i = index">
|
||||
<div class="header">
|
||||
<label for="disk-number-{{ i }}">Disco {{ disk.diskNumber }}:</label>
|
||||
<span class="disk-size">Tamaño: {{ (disk.totalDiskSize / 1024).toFixed(2) }} GB</span>
|
||||
</div>
|
||||
<div class="disk-select">
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Seleccionar disco</mat-label>
|
||||
<mat-select [(ngModel)]="selectedDiskNumber">
|
||||
<mat-option *ngFor="let disk of disks" [value]="disk.diskNumber">
|
||||
Disco {{ disk.diskNumber }} ({{ (disk.totalDiskSize / 1024).toFixed(2) }} GB)
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="partition-assistant" *ngIf="selectedDisk">
|
||||
<div class="partition-bar">
|
||||
<div
|
||||
*ngFor="let partition of activePartitions(disk.diskNumber)"
|
||||
*ngFor="let partition of activePartitions(selectedDisk.diskNumber)"
|
||||
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
|
||||
class="partition-segment"
|
||||
>
|
||||
|
@ -23,7 +30,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button mat-flat-button color="primary" (click)="addPartition(disk.diskNumber)">Añadir partición</button>
|
||||
<button mat-flat-button color="primary" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
@ -41,7 +48,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container *ngFor="let partition of disk.partitions; let j = index">
|
||||
<ng-container *ngFor="let partition of selectedDisk.partitions; let j = index">
|
||||
<tr *ngIf="!partition.removed">
|
||||
<td>{{ partition.partitionNumber }}</td>
|
||||
<td>
|
||||
|
@ -62,21 +69,21 @@
|
|||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.size" required
|
||||
(input)="updatePartitionSize(disk.diskNumber, j, partition.size)"
|
||||
(input)="updatePartitionSize(selectedDisk.diskNumber, j, partition.size)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.percentage"
|
||||
(input)="updatePartitionSizeFromPercentage(disk.diskNumber, j, partition.percentage)"
|
||||
(input)="updatePartitionSizeFromPercentage(selectedDisk.diskNumber, j, partition.percentage)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" [(ngModel)]="partition.format" />
|
||||
</td>
|
||||
<td>
|
||||
<button (click)="removePartition(disk.diskNumber, partition)" class="remove-btn">X</button>
|
||||
<button (click)="removePartition(selectedDisk.diskNumber, partition)" class="remove-btn">X</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
@ -87,7 +94,7 @@
|
|||
<div class="chart-container">
|
||||
<ngx-charts-pie-chart
|
||||
[view]="view"
|
||||
[results]="disk.chartData"
|
||||
[results]="selectedDisk.chartData"
|
||||
[doughnut]="true"
|
||||
>
|
||||
</ngx-charts-pie-chart>
|
||||
|
@ -96,5 +103,4 @@
|
|||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
|
||||
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>
|
||||
|
|
|
@ -35,6 +35,7 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
originalPartitions: any[] = [];
|
||||
clientId: string | null = null;
|
||||
newPartitions: any[] = [];
|
||||
selectedDiskNumber: number | null = null;
|
||||
updateRequests: any[] = [];
|
||||
data: any = {};
|
||||
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = [];
|
||||
|
@ -57,6 +58,10 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
this.loadPartitions();
|
||||
}
|
||||
|
||||
get selectedDisk():any {
|
||||
return this.disks.find(disk => disk.diskNumber === this.selectedDiskNumber) || null;
|
||||
}
|
||||
|
||||
loadPartitions() {
|
||||
const url = `${this.baseUrl}/clients/${this.clientId}`;
|
||||
this.http.get(url).subscribe(
|
||||
|
@ -245,60 +250,62 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
return modifiedPartitions;
|
||||
}
|
||||
|
||||
save() {
|
||||
const invalidDisks = this.disks.filter(disk => {
|
||||
const totalPartitionSize = disk.partitions.reduce((sum, partition) => sum + partition.size, 0);
|
||||
return totalPartitionSize > disk.totalDiskSize;
|
||||
});
|
||||
back() {
|
||||
this.router.navigate(['clients', this.data.uuid], { state: { clientData: this.data } });
|
||||
}
|
||||
|
||||
console.log(invalidDisks);
|
||||
if (invalidDisks.length > 0) {
|
||||
this.errorMessage = 'El tamaño total de las particiones en uno o más discos excede el tamaño total del disco.';
|
||||
save() {
|
||||
if (!this.selectedDisk) {
|
||||
this.errorMessage = 'Por favor selecciona un disco antes de guardar.';
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPartitionSize = this.selectedDisk.partitions.reduce((sum: any, partition: { size: any; }) => sum + partition.size, 0);
|
||||
|
||||
if (totalPartitionSize > this.selectedDisk.totalDiskSize) {
|
||||
this.errorMessage = 'El tamaño total de las particiones en el disco seleccionado excede el tamaño total del disco.';
|
||||
return;
|
||||
} else {
|
||||
this.errorMessage = '';
|
||||
}
|
||||
|
||||
const modifiedPartitions = this.getModifiedOrNewPartitions();
|
||||
const modifiedPartitions = this.selectedDisk.partitions.filter((partition: { removed: any; format: any; }) => !partition.removed || partition.format);
|
||||
|
||||
if (modifiedPartitions.length === 0) {
|
||||
this.errorMessage = 'No hay cambios para guardar.';
|
||||
this.errorMessage = 'No hay cambios para guardar en el disco seleccionado.';
|
||||
return;
|
||||
}
|
||||
|
||||
modifiedPartitions.forEach(({ partition, diskNumber, partitionNumber }) => {
|
||||
const payload = {
|
||||
diskNumber: diskNumber,
|
||||
partitionNumber: partitionNumber,
|
||||
memoryUsage: partition.memoryUsage,
|
||||
size: partition.size,
|
||||
partitionCode: partition.partitionCode,
|
||||
filesystem: partition.filesystem,
|
||||
client: `/clients/${this.clientId}`,
|
||||
uuid: partition.uuid,
|
||||
removed: partition.removed || false,
|
||||
format: partition.format || false,
|
||||
};
|
||||
const newPartitions = modifiedPartitions.map((partition: { partitionNumber: any; memoryUsage: any; size: any; partitionCode: any; filesystem: any; uuid: any; removed: any; format: any; }) => ({
|
||||
diskNumber: this.selectedDisk.diskNumber,
|
||||
partitionNumber: partition.partitionNumber,
|
||||
memoryUsage: partition.memoryUsage,
|
||||
size: partition.size,
|
||||
partitionCode: partition.partitionCode,
|
||||
filesystem: partition.filesystem,
|
||||
client: `/clients/${this.clientId}`,
|
||||
uuid: partition.uuid,
|
||||
removed: partition.removed || false,
|
||||
format: partition.format || false,
|
||||
}));
|
||||
|
||||
this.newPartitions.push(payload);
|
||||
});
|
||||
|
||||
if (this.newPartitions.length > 0) {
|
||||
const bulkPayload = { partitions: this.newPartitions };
|
||||
if (newPartitions.length > 0) {
|
||||
const bulkPayload = { partitions: newPartitions };
|
||||
|
||||
this.http.post(this.apiUrl, bulkPayload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Particiones creadas exitosamente');
|
||||
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al crear las particiones:', error);
|
||||
this.toastService.error('Error al crear las particiones');
|
||||
this.toastService.error('Error al crear las particiones.');
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
removePartition(diskNumber: number, partition: Partition) {
|
||||
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
|
||||
if (disk) {
|
||||
|
|
|
@ -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 {
|
||||
display: flex;
|
||||
|
@ -14,6 +8,12 @@
|
|||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.groups-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
|
@ -189,10 +189,6 @@ mat-tree {
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -432,7 +428,7 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
padding: 2px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
|
@ -447,10 +443,6 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
display: block;
|
||||
font-size: 1.2em;
|
||||
|
@ -471,8 +463,8 @@ button[mat-raised-button] {
|
|||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 8px; /* Espaciado reducido entre cards */
|
||||
}
|
||||
|
||||
.clients-list {
|
||||
|
@ -495,12 +487,6 @@ button[mat-raised-button] {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.view-toggle-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -520,35 +506,16 @@ button[mat-raised-button] {
|
|||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.client-image {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.header-actions-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.view-toggle-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filters-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -584,3 +551,47 @@ button[mat-raised-button] {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.client-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.clients-view-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.clients-title-name {
|
||||
font-size: x-large;
|
||||
display: block;
|
||||
padding: 1rem 1rem 1rem 15px;
|
||||
}
|
||||
|
||||
.no-clients-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 1.5rem;
|
||||
}
|
|
@ -1,18 +1,24 @@
|
|||
<!-- Header -->
|
||||
<div class="header-container" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2 class="title" joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
||||
{{ 'adminGroupsTitle' | translate }}
|
||||
</h2>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
||||
{{ 'adminGroupsTitle' | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="groups-button-row" joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}">
|
||||
<button mat-flat-button color="primary" (click)="addOU($event)"
|
||||
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'newOrganizationalUnitButton' | translate }}
|
||||
</button>
|
||||
<button mat-flat-button color="primary" (click)="addClient($event)" matTooltipShowDelay="1000">
|
||||
{{ 'newClientButton' | translate }}
|
||||
</button>
|
||||
<button mat-flat-button color="primary" [matMenuTriggerFor]="menuClients">{{ 'newClientButton' | translate }}</button>
|
||||
<mat-menu #menuClients="matMenu">
|
||||
<button mat-menu-item (click)="addClient($event)" >Añadir cliente unitario</button>
|
||||
<button mat-menu-item (click)="addMultipleClients($event)">Añadir clientes masivamente</button>
|
||||
</mat-menu>
|
||||
|
||||
<button mat-flat-button (click)="openBottomSheet()" joyrideStep="keyStep" text="{{ 'keyStepText' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
{{ 'legendButton' | translate }}
|
||||
|
@ -20,7 +26,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<mat-expansion-panel *ngIf="isTreeViewActive" class="filters-panel">
|
||||
<!-- Filters Panel -->
|
||||
<mat-expansion-panel *ngIf="isTreeViewActive" class="filters-panel" joyrideStep="filtersPanelStep" text="{{ 'filtersPanelStepText' | translate }}">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>{{ 'filters' | translate }}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
@ -49,178 +56,131 @@
|
|||
<mat-option value="group">{{ 'computerGroups' | translate }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
<div *ngIf="!selectedUnidad; else detailsTemplate" class="card-container">
|
||||
<mat-card *ngFor="let unidad of organizationalUnits"
|
||||
[ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}"
|
||||
(click)="onSelectUnidad(unidad)" class="unidad-card small-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
<mat-icon>apartment</mat-icon> {{ unidad.name }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-actions>
|
||||
<div class="button-container">
|
||||
<button mat-raised-button color="primary" [matMenuTriggerFor]="menu" (click)="$event.stopPropagation()">
|
||||
<mat-icon>menu</mat-icon>
|
||||
{{ 'Menu' | translate }}
|
||||
<!-- Unit details view-->
|
||||
<div class="main-container">
|
||||
<!-- Tree view -->
|
||||
<div class="tree-container">
|
||||
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
||||
<mat-tree-node [ngClass]="{'selected-node': node === selectedNode}" *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick(node)">
|
||||
<button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable" [ngClass]="{'disabled-toggle': !node.expandable}">
|
||||
<mat-icon>{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}</mat-icon>
|
||||
</button>
|
||||
<mat-icon class="node-icon {{ node.type }}">
|
||||
{{
|
||||
node.type === 'organizational-unit' ? 'apartment'
|
||||
: node.type === 'classrooms-group' ? 'meeting_room'
|
||||
: node.type === 'classroom' ? 'school'
|
||||
: node.type === 'clients-group' ? 'lan'
|
||||
: node.type === 'client' ? 'computer'
|
||||
: 'group'
|
||||
}}
|
||||
</mat-icon>
|
||||
<span>{{ node.name }}</span>
|
||||
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onNodeClick(node)">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</mat-tree-node>
|
||||
<mat-tree-node [ngClass]="{'selected-node': node === selectedNode}" *matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding (click)="onNodeClick(node)">
|
||||
<button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle"></button>
|
||||
<mat-icon style="color: green;">
|
||||
{{
|
||||
node.type === 'organizational-unit' ? 'apartment'
|
||||
: node.type === 'classrooms-group' ? 'meeting_room'
|
||||
: node.type === 'classroom' ? 'school'
|
||||
: node.type === 'clients-group' ? 'lan'
|
||||
: node.type === 'client' ? 'computer'
|
||||
: 'group'
|
||||
}}
|
||||
</mat-icon>
|
||||
<span>{{ node.name }}</span>
|
||||
<ng-container *ngIf="node.type === 'client'">
|
||||
<span> - IP: {{ node.ip }}</span>
|
||||
</ng-container>
|
||||
<button mat-icon-button [matMenuTriggerFor]="menuNode" (click)="onNodeClick(node)">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</mat-tree-node>
|
||||
</mat-tree>
|
||||
</div>
|
||||
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
|
||||
<!-- Tree node actions -->
|
||||
<mat-menu restoreFocus=false #commandMenu="matMenu">
|
||||
<button mat-menu-item *ngFor="let command of commands" (click)="executeCommand(command, selectedNode)">
|
||||
<span>{{ command.name }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<mat-menu #menuNode="matMenu">
|
||||
<button mat-menu-item (click)="onShowDetailsClick($event, selectedNode)">
|
||||
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
|
||||
<span>{{ 'viewUnitMenu' | translate }}</span>
|
||||
</button>
|
||||
<button *ngIf="selectedNode?.type === 'classroom'" mat-menu-item (click)="onRoomMap(selectedNode)">
|
||||
<mat-icon>map</mat-icon>
|
||||
<span>{{ 'roomMap' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addClient($event, selectedNode)">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'addClientMenu' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addOU($event, selectedNode)">
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
<span>{{ 'addOrganizationalUnit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onEditNode($event, selectedNode)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="selectedNode && onTreeClick($event, selectedNode)">
|
||||
<mat-icon matTooltip="{{ 'viewTreeTooltip' | translate }}" matTooltipHideDelay="0">account_tree</mat-icon>
|
||||
<span>{{ 'viewTreeMenu' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<!-- Clients view -->
|
||||
<div class="clients-container">
|
||||
<div class="clients-view-header">
|
||||
<span class="clients-title-name">{{ 'clients' | translate }}
|
||||
<strong>{{ selectedNode?.name }}</strong>
|
||||
</span>
|
||||
<div class="view-type-container">
|
||||
<button mat-button color="primary" (click)="toggleView('card')" [disabled]="currentView === 'card'">
|
||||
<mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
|
||||
</button>
|
||||
<button mat-button color="primary" (click)="toggleView('list')" [disabled]="currentView === 'list'">
|
||||
<mat-icon>list</mat-icon> {{ 'Vista Lista' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="onEditClick($event, unidad.type, unidad.uuid)">
|
||||
<mat-icon matTooltip="{{ 'editUnitTooltip' | translate }}" matTooltipHideDelay="0">edit</mat-icon>
|
||||
<span>{{ 'editUnitMenu' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onShowDetailsClick($event, unidad)">
|
||||
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
|
||||
<span>{{ 'viewUnitMenu' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addOU($event, unidad)">
|
||||
<mat-icon matTooltip="{{ 'addInternalUnitTooltip' | translate }}" matTooltipHideDelay="0">add_home_work</mat-icon>
|
||||
<span>{{ 'addInternalUnitMenu' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addClient($event, unidad)">
|
||||
<mat-icon matTooltip="{{ 'addClientTooltip' | translate }}" matTooltipHideDelay="0">devices</mat-icon>
|
||||
<span>{{ 'addClientMenu' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onTreeClick($event, unidad)">
|
||||
<mat-icon matTooltip="{{ 'viewTreeTooltip' | translate }}" matTooltipHideDelay="0">account_tree</mat-icon>
|
||||
<span>{{ 'viewTreeMenu' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, unidad)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<ng-template #detailsTemplate>
|
||||
<div class="header-actions-container">
|
||||
<button mat-raised-button color="primary" (click)="clearSelection()" class="back-button">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
{{ 'Back' | translate }}
|
||||
</button>
|
||||
<div class="view-toggle-container" *ngIf="selectedDetail">
|
||||
<button mat-button color="primary" (click)="toggleView('card')" [disabled]="currentView === 'card'">
|
||||
<mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
|
||||
</button>
|
||||
<button mat-button color="primary" (click)="toggleView('list')" [disabled]="currentView === 'list'">
|
||||
<mat-icon>list</mat-icon> {{ 'Vista Lista' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-container">
|
||||
<div class="tree-container">
|
||||
<h2>{{ selectedUnidad?.name }}</h2>
|
||||
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
||||
<mat-tree-node [ngClass]="{'selected-node': node === selectedNode}" *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick(node)">
|
||||
<button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable" [ngClass]="{'disabled-toggle': !node.expandable}">
|
||||
<mat-icon>{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}</mat-icon>
|
||||
</button>
|
||||
<mat-icon class="node-icon {{ node.type }}">
|
||||
{{
|
||||
node.type === 'organizational-unit' ? 'apartment'
|
||||
: node.type === 'classrooms-group' ? 'meeting_room'
|
||||
: node.type === 'classroom' ? 'school'
|
||||
: node.type === 'clients-group' ? 'lan'
|
||||
: node.type === 'client' ? 'computer'
|
||||
: 'group'
|
||||
}}
|
||||
</mat-icon>
|
||||
<span>{{ node.name }}</span>
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu" (click)="setSelectedNode(node); $event.stopPropagation()">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</mat-tree-node>
|
||||
<mat-tree-node [ngClass]="{'selected-node': node === selectedNode}" *matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding (click)="onNodeClick(node)">
|
||||
<button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle"></button>
|
||||
<mat-icon style="color: green;">
|
||||
{{
|
||||
node.type === 'organizational-unit' ? 'apartment'
|
||||
: node.type === 'classrooms-group' ? 'meeting_room'
|
||||
: node.type === 'classroom' ? 'school'
|
||||
: node.type === 'clients-group' ? 'lan'
|
||||
: node.type === 'client' ? 'computer'
|
||||
: 'group'
|
||||
}}
|
||||
</mat-icon>
|
||||
<span>{{ node.name }}</span>
|
||||
<ng-container *ngIf="node.type === 'client'">
|
||||
<span> - IP: {{ node.ip }}</span>
|
||||
</ng-container>
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu" (click)="setSelectedNode(node)">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</mat-tree-node>
|
||||
</mat-tree>
|
||||
</div>
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
|
||||
<mat-menu restoreFocus=false #commandMenu="matMenu">
|
||||
<button mat-menu-item *ngFor="let command of commands" (click)="executeCommand(command, selectedNode)">
|
||||
<span>{{ command.name }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button *ngIf="selectedNode?.type === 'classroom'" mat-menu-item [matMenuTriggerFor]="commandMenu" (click)="fetchCommands()">
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
<span>{{ 'executeCommand' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onShowDetailsClick($event, selectedNode)">
|
||||
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
|
||||
<span>{{ 'viewUnitMenu' | translate }}</span>
|
||||
</button>
|
||||
<button *ngIf="selectedNode?.type === 'classroom'" mat-menu-item (click)="onRoomMap(selectedNode)">
|
||||
<mat-icon>map</mat-icon>
|
||||
<span>{{ 'roomMap' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addClient($event, selectedNode)">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'addClientMenu' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addOU($event, selectedNode)">
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
<span>{{ 'addOrganizationalUnit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onEditNode($event, selectedNode)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<div class="clients-container" *ngIf="(selectedClients.data?.length || 0) > 0">
|
||||
<h3>{{ 'clients' | translate }} {{ selectedNode?.name ? ('del ' + selectedNode?.name) : '' }}</h3>
|
||||
<div *ngIf="(selectedClients.data?.length || 0) > 0; else noClientsTemplate">
|
||||
<!-- Cards view -->
|
||||
<div class="clients-grid" *ngIf="currentView === 'card'">
|
||||
<div *ngFor="let client of selectedClients.data" class="client-item">
|
||||
<div class="client-card">
|
||||
<img src="assets/images/client.png" alt="Client Icon" class="client-image" />
|
||||
<img
|
||||
[src]="'assets/images/ordenador_' + client.status + '.png'"
|
||||
alt="Client Icon"
|
||||
class="client-image" />
|
||||
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<div class="flex">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-og-live': client.status === 'og-live',
|
||||
'chip-busy': client.status === 'busy',
|
||||
'chip-windows': client.status === 'windows' || client.status === 'windows-session',
|
||||
'chip-linux': client.status === 'linux' || client.status === 'linux-session',
|
||||
'chip-macos': client.status === 'macos',
|
||||
'chip-off': client.status === 'off'
|
||||
}">
|
||||
{{ client.status || 'off' }}
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
|
||||
</mat-chip>
|
||||
<div class="action-icons">
|
||||
<button
|
||||
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
|
||||
mat-icon-button color="primary"
|
||||
(click)="getStatus(client)">
|
||||
(click)="getStatus(client, selectedNode)">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
|
||||
|
@ -229,28 +189,46 @@
|
|||
mat-icon-button color="primary">
|
||||
<mat-spinner diameter="24"></mat-spinner>
|
||||
</button>
|
||||
</div>
|
||||
<button mat-raised-button color="primary" [matMenuTriggerFor]="clientMenu">Acciones</button>
|
||||
<mat-menu #clientMenu="matMenu">
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onShowClientDetail($event, client)">
|
||||
|
||||
<button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
<span>{{ 'viewDetails' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client, selectedNode)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<app-execute-command [clientData]="client['@id']"></app-execute-command>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- List view -->
|
||||
<div class="clients-table" *ngIf="currentView === 'list'">
|
||||
<table mat-table matSort [dataSource]="selectedClients" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<img
|
||||
[src]="'assets/images/ordenador_' + client.status + '.png'"
|
||||
alt="Client Icon"
|
||||
class="client-image" />
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="sync">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'sync' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<button
|
||||
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
|
||||
mat-icon-button color="primary"
|
||||
(click)="getStatus(client, selectedNode)">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="syncStatus && syncingClientId === client.uuid"
|
||||
mat-icon-button color="primary">
|
||||
<mat-spinner diameter="24"></mat-spinner>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
|
@ -263,39 +241,9 @@
|
|||
</ng-container>
|
||||
<ng-container matColumnDef="oglive">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.ogLive?.name }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-og-live': client.status === 'og-live',
|
||||
'chip-busy': client.status === 'busy',
|
||||
'chip-windows': client.status === 'windows' || client.status === 'windows-session',
|
||||
'chip-linux': client.status === 'linux' || client.status === 'linux-session',
|
||||
'chip-macos': client.status === 'macos',
|
||||
'chip-off': client.status === 'off'
|
||||
}">
|
||||
{{ client.status || 'off' }}
|
||||
|
||||
</mat-chip>
|
||||
<button
|
||||
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
|
||||
mat-icon-button color="primary"
|
||||
(click)="getStatus(client)">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="syncStatus && syncingClientId === client.uuid"
|
||||
mat-icon-button color="primary">
|
||||
<mat-spinner diameter="24"></mat-spinner>
|
||||
</button>
|
||||
</td>
|
||||
<td mat-cell *matCellDef="let client"> {{ (client.ogLive?.filename || '').slice(0, 15) }}{{ (client.ogLive?.filename?.length > 15) ? '...' : '' }} </td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
<ng-container matColumnDef="maintenace">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'maintenance' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.maintenance }} </td>
|
||||
|
@ -314,28 +262,14 @@
|
|||
<td mat-cell *matCellDef="let client"> {{ client.parentName }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<ng-container matColumnDef="actions" >
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'actions' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<button mat-icon-button [matMenuTriggerFor]="clientMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
<app-execute-command [clientData]="client['@id']"></app-execute-command>
|
||||
<mat-menu #clientMenu="matMenu">
|
||||
|
||||
<mat-menu restoreFocus=false #commandMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item *ngFor="let command of commands" (click)="executeClientCommand(command, client)">
|
||||
<span>{{ command.name }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<button mat-menu-item [matMenuTriggerFor]="commandMenu" (click)="fetchCommands()">
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
<span>{{ 'executeCommand' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
|
@ -358,4 +292,17 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No clients view -->
|
||||
<ng-template #noClientsTemplate>
|
||||
<div *ngIf="isLoadingClients" class="loading-container">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
<div *ngIf="!isLoadingClients" class="no-clients-info">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ 'noClients' | translate }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
@ -22,10 +22,8 @@ import { MatTabsModule } from '@angular/material/tabs';
|
|||
import { MatCardModule } from '@angular/material/card';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { JoyrideModule } from 'ngx-joyride';
|
||||
import { AdvancedSearchComponent } from './components/advanced-search/advanced-search.component';
|
||||
import { ClientTabViewComponent } from './components/client-tab-view/client-tab-view.component';
|
||||
import { OrganizationalUnitTabViewComponent } from './components/organizational-unit-tab-view/organizational-unit-tab-view.component';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTreeModule } from '@angular/material/tree';
|
||||
|
||||
describe('GroupsComponent', () => {
|
||||
let component: GroupsComponent;
|
||||
|
@ -33,7 +31,7 @@ describe('GroupsComponent', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [GroupsComponent, AdvancedSearchComponent, ClientTabViewComponent, OrganizationalUnitTabViewComponent],
|
||||
declarations: [GroupsComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
|
@ -56,6 +54,7 @@ describe('GroupsComponent', () => {
|
|||
MatListModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatTreeModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
|
@ -98,19 +97,4 @@ describe('GroupsComponent', () => {
|
|||
component.getFilters();
|
||||
expect(component.getFilters).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onTabChange method', () => {
|
||||
spyOn(component, 'onTabChange');
|
||||
const event = { index: 2 } as any;
|
||||
component.onTabChange(event);
|
||||
expect(component.onTabChange).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
it('should call onSelectUnidad method', () => {
|
||||
spyOn(component, 'onSelectUnidad');
|
||||
const unidad = { id: '1', name: 'Test' } as any;
|
||||
component.onSelectUnidad(unidad);
|
||||
expect(component.onSelectUnidad).toHaveBeenCalledWith(unidad);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -18,13 +18,12 @@ import { EditClientComponent } from './shared/clients/edit-client/edit-client.co
|
|||
import { ShowOrganizationalUnitComponent } from './shared/organizational-units/show-organizational-unit/show-organizational-unit.component';
|
||||
import { TreeViewComponent } from './shared/tree-view/tree-view.component';
|
||||
import { LegendComponent } from './shared/legend/legend.component';
|
||||
import { ClientTabViewComponent } from './components/client-tab-view/client-tab-view.component';
|
||||
import { OrganizationalUnitTabViewComponent } from './components/organizational-unit-tab-view/organizational-unit-tab-view.component';
|
||||
import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import {CreateMultipleClientComponent} from "./shared/clients/create-multiple-client/create-multiple-client.component";
|
||||
|
||||
enum NodeType {
|
||||
OrganizationalUnit = 'organizational-unit',
|
||||
|
@ -45,6 +44,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
selectedUnidad: UnidadOrganizativa | null = null;
|
||||
selectedDetail: UnidadOrganizativa | null = null;
|
||||
loading = false;
|
||||
isLoadingClients: boolean = false;
|
||||
searchTerm = '';
|
||||
treeControl: FlatTreeControl<FlatNode>;
|
||||
treeFlattener: MatTreeFlattener<TreeNode, FlatNode>;
|
||||
|
@ -63,7 +63,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
syncingClientId: string | null = null;
|
||||
private originalTreeData: TreeNode[] = [];
|
||||
|
||||
displayedColumns: string[] = ['name', 'oglive', 'status', 'maintenace', 'subnet', 'pxeTemplate', 'parentName', 'actions'];
|
||||
displayedColumns: string[] = ['status','sync', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
|
||||
|
||||
private _sort!: MatSort;
|
||||
private _paginator!: MatPaginator;
|
||||
|
@ -83,8 +83,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.selectedClients.paginator = this._paginator;
|
||||
}
|
||||
}
|
||||
@ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent;
|
||||
@ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent;
|
||||
|
||||
private subscriptions: Subscription = new Subscription();
|
||||
|
||||
|
@ -116,6 +114,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.search();
|
||||
this.getFilters();
|
||||
this.updateGridCols();
|
||||
this.loadOrganizationalUnits();
|
||||
window.addEventListener('resize', this.updateGridCols);
|
||||
|
||||
this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => {
|
||||
|
@ -135,6 +134,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private transformer = (node: TreeNode, level: number): FlatNode => ({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
level,
|
||||
|
@ -144,6 +144,40 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
'@id': node['@id'],
|
||||
});
|
||||
|
||||
private loadOrganizationalUnits(): void {
|
||||
this.loading = true;
|
||||
this.isLoadingClients = true;
|
||||
this.dataService.getOrganizationalUnits().subscribe(
|
||||
(data) => {
|
||||
this.organizationalUnits = data;
|
||||
this.loading = false;
|
||||
|
||||
if (this.organizationalUnits.length > 0) {
|
||||
const treeData = this.organizationalUnits.map((unidad) => this.convertToTreeData(unidad));
|
||||
this.treeDataSource.data = treeData.flat();
|
||||
|
||||
this.isTreeViewActive = true;
|
||||
|
||||
const firstNode = this.treeDataSource.data[0];
|
||||
if (firstNode) {
|
||||
this.selectedNode = firstNode;
|
||||
this.fetchClientsForNode(firstNode);
|
||||
}
|
||||
} else {
|
||||
this.toastr.info('No existen unidades organizativas');
|
||||
this.isTreeViewActive = false;
|
||||
this.isLoadingClients = false;
|
||||
return;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching organizational units', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
toggleView(view: 'card' | 'list'): void {
|
||||
this.currentView = view;
|
||||
}
|
||||
|
@ -158,14 +192,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.selectedDetail = null;
|
||||
this.selectedClients.data = [];
|
||||
this.isTreeViewActive = false;
|
||||
}
|
||||
|
||||
onTabChange(event: MatTabChangeEvent): void {
|
||||
if (event.index === 2) {
|
||||
this.clientTabComponent.search();
|
||||
} else if (event.index === 3) {
|
||||
this.organizationalUnitTabComponent.search();
|
||||
}
|
||||
this.selectedNode = null;
|
||||
}
|
||||
|
||||
getFilters(): void {
|
||||
|
@ -212,38 +239,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
onSelectUnidad(unidad: UnidadOrganizativa): void {
|
||||
this.selectedUnidad = unidad;
|
||||
this.selectedDetail = unidad;
|
||||
this.selectedClients.data = this.collectAllClients(unidad);
|
||||
this.selectedClientsOriginal = [...this.selectedClients.data];
|
||||
this.loadChildrenAndClients(unidad.id).then((fullData) => {
|
||||
const treeData = this.convertToTreeData(fullData);
|
||||
this.treeDataSource.data = treeData[0]?.children || [];
|
||||
});
|
||||
this.isTreeViewActive = true;
|
||||
|
||||
console.log('Selected unidad:', unidad);
|
||||
}
|
||||
|
||||
private collectAllClients(node: UnidadOrganizativa): Client[] {
|
||||
let clients = (node.clients || []).map(client => ({
|
||||
...client,
|
||||
parentName: node.name
|
||||
}));
|
||||
|
||||
if (node.children) {
|
||||
node.children.forEach((child) => {
|
||||
clients = clients.concat(this.collectAllClients(child).map(client => ({
|
||||
...client,
|
||||
parentName: client.parentName || ''
|
||||
})));
|
||||
});
|
||||
}
|
||||
return clients;
|
||||
}
|
||||
|
||||
|
||||
private async loadChildrenAndClients(id: string): Promise<UnidadOrganizativa> {
|
||||
try {
|
||||
const childrenData = await this.dataService.getChildren(id).toPromise();
|
||||
|
@ -268,6 +263,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
private convertToTreeData(data: UnidadOrganizativa): TreeNode[] {
|
||||
const processNode = (node: UnidadOrganizativa): TreeNode => ({
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
'@id': node['@id'],
|
||||
|
@ -279,34 +275,23 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
|
||||
onNodeClick(node: TreeNode): void {
|
||||
console.log('Node clicked:', node);
|
||||
this.selectedNode = node;
|
||||
this.fetchClientsForNode(node);
|
||||
}
|
||||
|
||||
private fetchClientsForNode(node: TreeNode): void {
|
||||
if (node.hasClients && node['@id']) {
|
||||
this.subscriptions.add(
|
||||
this.http.get<{ clients: Client[] }>(`${this.baseUrl}${node['@id']}`).subscribe(
|
||||
(data) => {
|
||||
const clientsWithParentName = (data.clients || []).map(client => ({
|
||||
...client,
|
||||
parentName: node.name
|
||||
}));
|
||||
this.selectedClients.data = clientsWithParentName;
|
||||
this.selectedClients._updateChangeSubscription();
|
||||
if (this._paginator) {
|
||||
this._paginator.firstPage();
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching clients:', error);
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.selectedClients.data = [];
|
||||
this.selectedClients._updateChangeSubscription();
|
||||
}
|
||||
console.log('Node:', node);
|
||||
this.isLoadingClients = true;
|
||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({
|
||||
next: (response) => {
|
||||
this.selectedClients.data = response['hydra:member'];
|
||||
this.isLoadingClients = false;
|
||||
},
|
||||
error: () => {
|
||||
this.isLoadingClients = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getNodeIcon(node: TreeNode): string {
|
||||
|
@ -351,7 +336,27 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
||||
event.stopPropagation();
|
||||
const dialogRef = this.dialog.open(CreateMultipleClientComponent, {
|
||||
data: { organizationalUnit },
|
||||
width: '900px',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.refreshOrganizationalUnits();
|
||||
if (organizationalUnit && organizationalUnit['@id']) {
|
||||
this.refreshClientsForNode(organizationalUnit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private refreshOrganizationalUnits(): void {
|
||||
const expandedNodeIds = this.treeControl.dataNodes
|
||||
? this.treeControl.dataNodes
|
||||
.filter(node => this.treeControl.isExpanded(node))
|
||||
.map(node => this.extractUuid(node['@id']))
|
||||
: [];
|
||||
|
||||
this.subscriptions.add(
|
||||
this.dataService.getOrganizationalUnits().subscribe(
|
||||
(data) => {
|
||||
|
@ -362,6 +367,15 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
const treeData = this.convertToTreeData(updatedData);
|
||||
this.originalTreeData = treeData[0]?.children || [];
|
||||
this.treeDataSource.data = [...this.originalTreeData];
|
||||
|
||||
setTimeout(() => {
|
||||
this.treeControl.dataNodes.forEach(node => {
|
||||
const nodeId = this.extractUuid(node['@id']);
|
||||
if (nodeId && expandedNodeIds.includes(nodeId)) {
|
||||
this.treeControl.expand(node);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -370,6 +384,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
onEditNode(event: MouseEvent, node: TreeNode | null): void {
|
||||
event.stopPropagation();
|
||||
const uuid = node ? this.extractUuid(node['@id']) : null;
|
||||
|
@ -478,10 +493,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
executeClientCommand(command: Command, client: Client): void {
|
||||
this.toastr.success(`Ejecutando comando: ${command.name} en ${client.name}`);
|
||||
}
|
||||
|
||||
onShowClientDetail(event: MouseEvent, client: Client): void {
|
||||
event.stopPropagation();
|
||||
this.router.navigate(['clients', client.uuid], { state: { clientData: client } });
|
||||
|
@ -511,7 +522,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: ['groupsTitleStepText', 'addStep', 'keyStep', 'unitStep', 'elementsStep', 'tabsStep'],
|
||||
steps: ['groupsTitleStepText', 'filtersPanelStep', 'addStep', 'keyStep', 'tabsStep'],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5',
|
||||
});
|
||||
|
@ -563,7 +574,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.selectedNode = node;
|
||||
}
|
||||
|
||||
getStatus(client: Client): void {
|
||||
getStatus(client: Client, node: any): void {
|
||||
if (!client.uuid || !client['@id']) return;
|
||||
|
||||
this.syncingClientId = client.uuid;
|
||||
|
@ -573,14 +584,15 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe(
|
||||
() => {
|
||||
this.toastr.success('Cliente actualizado correctamente');
|
||||
this.search();
|
||||
this.syncStatus = false;
|
||||
this.syncingClientId = null;
|
||||
this.search()
|
||||
},
|
||||
() => {
|
||||
this.toastr.error('Error de conexión con el cliente');
|
||||
this.syncStatus = false;
|
||||
this.syncingClientId = null;
|
||||
this.search()
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
|
@ -60,6 +60,7 @@ export interface ClientCollection {
|
|||
}
|
||||
|
||||
export interface TreeNode {
|
||||
id?: string
|
||||
name: string;
|
||||
type: string;
|
||||
'@id'?: string;
|
||||
|
@ -70,6 +71,7 @@ export interface TreeNode {
|
|||
}
|
||||
|
||||
export interface FlatNode {
|
||||
id?: string;
|
||||
name: string;
|
||||
type: string;
|
||||
level: number;
|
||||
|
|
|
@ -1,161 +1,63 @@
|
|||
.create-client-container {
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
color: #3f51b5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.network-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
h1, h3, h4 {
|
||||
margin: 0 0 16px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.inputs-container {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-top: 16px;
|
||||
.form-field {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
flex: 1;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
min-width: 600px;
|
||||
max-width: 90vw;
|
||||
width: 800px;
|
||||
padding: 50px;
|
||||
}
|
||||
|
||||
.create-multiple-client-container {
|
||||
flex: 1;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
button {
|
||||
text-transform: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.client-form {
|
||||
.mat-slide-toggle {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
mat-option .unit-name {
|
||||
display: block;
|
||||
}
|
||||
|
||||
mat-option .unit-path {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.create-client-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scrollable-table {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f1f1f1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.mat-dialog-actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
button.mat-raised-button {
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
margin: 16px auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #007BFF;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.toggle-button:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mat-divider {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.inputs-container {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.mat-dialog-content, .create-multiple-client-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.scrollable-table {
|
||||
max-height: 150px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,136 +1,111 @@
|
|||
<div class="create-client-container mat-elevation-z4">
|
||||
<h1>{{ 'addClientTitle' | translate }}s</h1>
|
||||
<div class="inputs-container">
|
||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
||||
|
||||
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit">
|
||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
<div class="create-client-container">
|
||||
<h1 mat-dialog-title i18n="@@add-client-dialog-title">Añadir Cliente</h1>
|
||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@organizational-unit-label">Padre</mat-label>
|
||||
<mat-select formControlName="organizationalUnit">
|
||||
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
<div class="unit-path">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div *ngIf="!isSingleClientForm; else singleClientForm">
|
||||
<h3>Añadir múltiples clientes</h3>
|
||||
<div class="upload-container">
|
||||
<button mat-raised-button color="primary" (click)="fileInput.click()">Subir fichero</button>
|
||||
<input #fileInput type="file" (change)="onFileUpload($event)" accept="*" hidden>
|
||||
<p>o añadelos manualmente:</p>
|
||||
<div *ngIf="showTextarea">
|
||||
<textarea #textarea matInput placeholder="Ejemplo: host bbaa-it1-11 { hardware ethernet a0:48:1c:8a:f1:5b; fixed-address 172.17.69.11; };" rows="20" cols="100"></textarea>
|
||||
<button mat-raised-button color="primary" (click)="onTextarea(textarea.value)">cargar</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@name-label">Nombre</mat-label>
|
||||
<input matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
|
||||
<h4 *ngIf="uploadedClients.length > 0">Clientes importados:</h4>
|
||||
<div class="scrollable-table">
|
||||
<table mat-table [dataSource]="uploadedClients" class="mat-elevation-z8" *ngIf="uploadedClients.length > 0">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> Nombre </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.name }} </td>
|
||||
</ng-container>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@oglive-label">OgLive</mat-label>
|
||||
<mat-select formControlName="ogLive">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.filename }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<ng-container matColumnDef="ip">
|
||||
<th mat-header-cell *matHeaderCellDef> IP </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.ip }} </td>
|
||||
</ng-container>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@serial-number-label">Número de Serie</mat-label>
|
||||
<input matInput formControlName="serialNumber">
|
||||
</mat-form-field>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@netiface-label">Interfaz de red</mat-label>
|
||||
<mat-select formControlName="netiface">
|
||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Añadir uon cliente -->
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@net-driver-label">Controlador de red</mat-label>
|
||||
<mat-select formControlName="netDriver">
|
||||
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<ng-template #singleClientForm>
|
||||
<h3>Añadir un cliente</h3>
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@mac-label">MAC</mat-label>
|
||||
<mat-hint i18n="@@mac-hint">Ejemplo: 00:11:22:33:44:55</mat-hint>
|
||||
<input matInput formControlName="mac">
|
||||
<mat-error i18n="@@mac-error">Formato de MAC inválido. Ejemplo válido: 00:11:22:33:44:55</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="ogLive">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@ip-label">Dirección IP</mat-label>
|
||||
<mat-hint i18n="@@ip-hint">Ejemplo: 127.0.0.1</mat-hint>
|
||||
<input matInput formControlName="ip">
|
||||
<mat-error i18n="@@ip-error">Formato de dirección IP inválido. Ejemplo válido: 127.0.0.1</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'serialNumberLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="serialNumber">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@oglive-label">Plantilla PXE</mat-label>
|
||||
<mat-select formControlName="template">
|
||||
<mat-option *ngFor="let template of templates" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'netifaceLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netiface">
|
||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@hardware-profile-label">Perfil de Hardware</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">
|
||||
{{ unit.description }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error i18n="@@hardware-profile-error">Formato de URL inválido.</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'netDriverLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netDriver">
|
||||
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@hardware-profile-label">Repositorio</mat-label>
|
||||
<mat-select formControlName="repository">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'macLabel' | translate }}</mat-label>
|
||||
<mat-hint>{{ 'macHint' | translate }}</mat-hint>
|
||||
<input matInput formControlName="mac">
|
||||
<mat-error>{{ 'macError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ipLabel' | translate }}</mat-label>
|
||||
<mat-hint>{{ 'ipHint' | translate }}</mat-hint>
|
||||
<input matInput formControlName="ip">
|
||||
<mat-error>{{ 'ipError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="template">
|
||||
<mat-option *ngFor="let template of templates" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">
|
||||
{{ unit.description }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>{{ 'hardwareProfileError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</ng-template>
|
||||
</div>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'menuLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="menu">
|
||||
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
|
||||
{{ menu.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>{{ 'menuError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="toggleClientForm()">
|
||||
{{ isSingleClientForm ? 'Añadir múltiples clientes' : 'Añadir un único cliente' }}
|
||||
</button>
|
||||
<button mat-button color="warn" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button color="primary" (click)="onSubmit()">{{ 'addButton' | translate }}</button>
|
||||
<button mat-button (click)="onNoClick()" i18n="@@cancel-button">Cancelar</button>
|
||||
<button mat-button [disabled]="!clientForm.valid" (click)="onSubmit()" i18n="@@add-button">Añadir</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
|||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import * as Papa from 'papaparse';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-client',
|
||||
|
@ -18,12 +17,11 @@ export class CreateClientComponent implements OnInit {
|
|||
parentUnits: any[] = [];
|
||||
hardwareProfiles: any[] = [];
|
||||
ogLives: any[] = [];
|
||||
menus: any[] = [];
|
||||
templates: any[] = [];
|
||||
uploadedClients: any[] = [];
|
||||
repositories: any[] = [];
|
||||
loading: boolean = false;
|
||||
displayedColumns: string[] = ['name', 'ip'];
|
||||
isSingleClientForm: boolean = false;
|
||||
showTextarea: boolean = true;
|
||||
protected netifaceTypes = [
|
||||
{ name: 'Eth0', value: 'eth0' },
|
||||
{ name: 'Eth1', value: 'eth1' },
|
||||
|
@ -49,6 +47,8 @@ export class CreateClientComponent implements OnInit {
|
|||
this.loadHardwareProfiles();
|
||||
this.loadOgLives();
|
||||
this.loadPxeTemplates();
|
||||
this.loadRepositories();
|
||||
this.loadMenus()
|
||||
}
|
||||
|
||||
initForm(): void {
|
||||
|
@ -67,7 +67,9 @@ export class CreateClientComponent implements OnInit {
|
|||
hardwareProfile: [
|
||||
this.data.organizationalUnit?.networkSettings?.hardwareProfile?.['@id'] || null
|
||||
],
|
||||
ogLive: [null]
|
||||
ogLive: [null],
|
||||
repository: [null],
|
||||
menu: [null]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -126,117 +128,47 @@ export class CreateClientComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
onFileUpload(event: any): void {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e: any) => {
|
||||
const textData = e.target.result;
|
||||
const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g;
|
||||
let match;
|
||||
const clients = [];
|
||||
|
||||
while ((match = regex.exec(textData)) !== null) {
|
||||
clients.push({
|
||||
name: match[1],
|
||||
mac: match[2],
|
||||
ip: match[3]
|
||||
});
|
||||
}
|
||||
|
||||
if (clients.length > 0) {
|
||||
this.uploadedClients = clients;
|
||||
this.toastService.success('Archivo cargado correctamente, los datos están listos para enviarse.', 'Éxito');
|
||||
this.showTextarea = false;
|
||||
} else {
|
||||
this.toastService.error('No se encontraron datos válidos', 'Error');
|
||||
this.showTextarea = true;
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
loadMenus(): void {
|
||||
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.menus = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching menus:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onTextarea(text: string): void {
|
||||
const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g;
|
||||
let match;
|
||||
const clients = [];
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
clients.push({
|
||||
name: match[1],
|
||||
mac: match[2],
|
||||
ip: match[3]
|
||||
});
|
||||
}
|
||||
|
||||
if (clients.length > 0) {
|
||||
this.uploadedClients = clients;
|
||||
this.toastService.success('Datos cargados correctamente, los datos están listos para enviarse.', 'Éxito');
|
||||
this.showTextarea = false;
|
||||
} else {
|
||||
this.toastService.error('No se encontraron datos válidos', 'Error');
|
||||
this.showTextarea = true;
|
||||
}
|
||||
|
||||
loadRepositories(): void {
|
||||
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.repositories = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.isSingleClientForm) {
|
||||
if (this.clientForm.valid) {
|
||||
const formData = this.clientForm.value;
|
||||
console.log('Form data:', formData);
|
||||
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
||||
this.dialogRef.close(response);
|
||||
},
|
||||
error => {
|
||||
console.error('Error durante POST:', error);
|
||||
this.toastService.error('Error al crear el cliente', 'Error');
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (this.uploadedClients.length > 0) {
|
||||
this.uploadedClients.forEach(client => {
|
||||
const formData = {
|
||||
organizationalUnit: this.clientForm.value.organizationalUnit || null,
|
||||
name: client.name || null,
|
||||
mac: client.mac || null,
|
||||
ip: client.ip || null,
|
||||
template: this.clientForm.value.template || null,
|
||||
hardwareProfile: this.clientForm.value.hardwareProfile || null,
|
||||
ogLive: this.clientForm.value.ogLive || null,
|
||||
serialNumber: null,
|
||||
netiface: null,
|
||||
netDriver: null
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
||||
response => {
|
||||
this.toastService.success(`Cliente ${client.name} creado exitosamente`, 'Éxito');
|
||||
},
|
||||
error => {
|
||||
console.error(`Error al crear el cliente ${client.name}:`, error);
|
||||
this.toastService.error(`Error al crear el cliente ${client.name}`, 'Error');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
this.uploadedClients = [];
|
||||
this.dialogRef.close();
|
||||
} else {
|
||||
this.toastService.error('No hay clientes cargados para añadir', 'Error');
|
||||
}
|
||||
if (this.clientForm.valid) {
|
||||
const formData = this.clientForm.value;
|
||||
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente creado exitosamente', 'Éxito');
|
||||
this.dialogRef.close(response);
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error al crear el cliente', 'Error');
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toggleClientForm(): void {
|
||||
this.isSingleClientForm = !this.isSingleClientForm;
|
||||
}
|
||||
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
|
|
@ -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-select formControlName="ogLive">
|
||||
<mat-option *ngFor="let ogLive of ogLives" [value]="ogLive['@id']">
|
||||
{{ ogLive.name }}
|
||||
{{ ogLive.filename }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
@ -88,6 +88,15 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'menuLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="menu">
|
||||
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
|
||||
{{ menu.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>{{ 'menuError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ export class EditClientComponent {
|
|||
repositories: any[] = [];
|
||||
ogLives: any[] = [];
|
||||
templates: any[] = [];
|
||||
menus: any[] = [];
|
||||
isEditMode: boolean;
|
||||
protected netifaceTypes = [
|
||||
{ "name": 'Eth0', "value": "eth0" },
|
||||
|
@ -50,6 +51,7 @@ export class EditClientComponent {
|
|||
this.loadOgLives();
|
||||
this.loadPxeTemplates()
|
||||
this.loadRepositories();
|
||||
this.loadMenus()
|
||||
this.clientForm = this.fb.group({
|
||||
organizationalUnit: [null, Validators.required],
|
||||
name: ['', Validators.required],
|
||||
|
@ -62,6 +64,7 @@ export class EditClientComponent {
|
|||
hardwareProfile: null,
|
||||
ogLive: null,
|
||||
repository: null,
|
||||
menu: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -102,6 +105,19 @@ export class EditClientComponent {
|
|||
);
|
||||
}
|
||||
|
||||
loadMenus(): void {
|
||||
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.menus = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching menus:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadRepositories(): void {
|
||||
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
|
||||
|
||||
|
@ -146,6 +162,7 @@ export class EditClientComponent {
|
|||
repository: data.repository ? data.repository['@id'] : null,
|
||||
ogLive: data.ogLive ? data.ogLive['@id'] : null,
|
||||
template: data.template ? data.template['@id'] : null,
|
||||
menu: data.menu ? data.menu['@id'] : null,
|
||||
});
|
||||
this.loading = false;
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
<mat-option *ngFor="let type of types" [value]="type">
|
||||
<mat-option *ngFor="let type of filteredTypes" [value]="type">
|
||||
{{ typeTranslations[type] }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
|
@ -88,7 +88,7 @@
|
|||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="oglive" (selectionChange)="onOgLiveChange($event)">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.name }}
|
||||
{{ oglive.filename }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -29,10 +29,9 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
{ name: 'Peer', value: 'p2p-mode-peer' },
|
||||
{ name: 'Seeder', value: 'p2p-mode-seeder' },
|
||||
];
|
||||
multicastModeOptions: { name: string, value: string }[] = [
|
||||
{ name: 'Modo 1', value: 'mode1' },
|
||||
{ name: 'Modo 2', value: 'mode2' },
|
||||
{ name: 'Modo 3', value: 'mode3' },
|
||||
protected multicastModeOptions = [
|
||||
{"name": 'Half duplex', "value": "half"},
|
||||
{"name": 'Full duplex', "value": "full"},
|
||||
];
|
||||
parentUnits: any[] = [];
|
||||
hardwareProfiles: any[] = [];
|
||||
|
@ -41,6 +40,7 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
repositories: any[] = [];
|
||||
selectedCalendarUuid: string | null = null;
|
||||
|
||||
|
||||
@Output() unitAdded = new EventEmitter();
|
||||
|
||||
constructor(
|
||||
|
@ -97,6 +97,10 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
this.loadRepositories();
|
||||
}
|
||||
|
||||
get filteredTypes(): string[] {
|
||||
return this.generalFormGroup.get('parent')?.value ? this.types.filter(type => type !== 'organizational-unit') : this.types;
|
||||
}
|
||||
|
||||
loadParentUnits() {
|
||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
|
||||
this.http.get<any>(url).subscribe(
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
<mat-option *ngFor="let type of types" [value]="type">{{ type }}</mat-option>
|
||||
<mat-option *ngFor="let type of filteredTypes" [value]="type">{{ type }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
|
@ -85,7 +85,7 @@
|
|||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.name }}
|
||||
{{ oglive.filename }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
|
|
@ -31,8 +31,8 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
{"name": 'Seeder', "value": "p2p-mode-seeder"},
|
||||
];
|
||||
protected multicastModeOptions = [
|
||||
{"name": 'Half duplex', "value": "half-duplex"},
|
||||
{"name": 'Full duplex', "value": "full-duplex"},
|
||||
{"name": 'Half duplex', "value": "half"},
|
||||
{"name": 'Full duplex', "value": "full"},
|
||||
];
|
||||
@Output() unitAdded = new EventEmitter();
|
||||
calendars: any;
|
||||
|
@ -98,6 +98,10 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
this.loadRepositories();
|
||||
}
|
||||
|
||||
get filteredTypes(): string[] {
|
||||
return this.generalFormGroup.get('parent')?.value ? this.types.filter(type => type !== 'organizational-unit') : this.types;
|
||||
}
|
||||
|
||||
loadParentUnits() {
|
||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;
|
||||
|
||||
|
|
|
@ -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>
|
||||
<mat-dialog-content>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Nombre</mat-label>
|
||||
<input matInput [(ngModel)]="name">
|
||||
</mat-form-field>
|
||||
<mat-spinner class="spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<mat-form-field *ngIf="!loading" appearance="fill" class="full-width">
|
||||
|
|
|
@ -10,7 +10,6 @@ import { ToastrService } from 'ngx-toastr';
|
|||
})
|
||||
export class CreatePXEImageComponent implements OnInit {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
name: string = '';
|
||||
downloads: any[] = [];
|
||||
selectedDownload: any;
|
||||
isEditMode: boolean = false;
|
||||
|
@ -28,7 +27,6 @@ export class CreatePXEImageComponent implements OnInit {
|
|||
this.fetchDownloads();
|
||||
if (this.data) {
|
||||
this.isEditMode = true;
|
||||
this.name = this.data.name;
|
||||
this.selectedDownload = this.data.downloadUrl;
|
||||
this.imageId = this.data.uuid; // Assuming UUID is used for edit
|
||||
}
|
||||
|
@ -55,7 +53,6 @@ export class CreatePXEImageComponent implements OnInit {
|
|||
|
||||
submitForm(): void {
|
||||
const payload = {
|
||||
name: this.name,
|
||||
downloadUrl: this.selectedDownload.url
|
||||
};
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
<button mat-icon-button color="info" (click)="showOgLive($event, image)">
|
||||
<mat-icon>{{ 'viewIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="editImage(image)">
|
||||
<button mat-icon-button disabled color="primary" (click)="editImage(image)">
|
||||
<mat-icon>{{ 'editIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteImage(image)">
|
||||
|
|
|
@ -40,9 +40,9 @@ export class PXEimagesComponent implements OnInit {
|
|||
cell: (user: any) => `${user.id}`
|
||||
},
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'Nombre de imagen',
|
||||
cell: (user: any) => `${user.name}`
|
||||
columnDef: 'filename',
|
||||
header: 'Og Live',
|
||||
cell: (user: any) => `${user.filename}`
|
||||
},
|
||||
{
|
||||
columnDef: 'isDefault',
|
||||
|
|
|
@ -128,5 +128,4 @@ export class RepositoriesComponent {
|
|||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -151,17 +151,10 @@
|
|||
</span>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item class="disabled" matTooltip="{{ 'TOOLTIP_MENUS' | translate }}" matTooltipShowDelay="1000">
|
||||
<mat-list-item routerLink="/menus" matTooltip="{{ 'TOOLTIP_MENUS' | translate }}" matTooltipShowDelay="1000">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">list</mat-icon>
|
||||
<span>{{ 'menus' | translate }}</span>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item class="disabled" matTooltip="{{ 'TOOLTIP_SEARCH' | translate }}" matTooltipShowDelay="1000">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">search</mat-icon>
|
||||
<span>{{ 'search' | translate }}</span>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
|
|
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",
|
||||
"mcastModeLabel": "Multicast Mode",
|
||||
"menuUrlLabel": "Menu URL",
|
||||
"menuLabel": "Menu",
|
||||
"hardwareProfileLabel": "Hardware Profile",
|
||||
"urlFormatError": "Invalid URL format.",
|
||||
"validationToggle": "Validation",
|
||||
|
@ -445,5 +446,9 @@
|
|||
"maintenance": "Maintenance",
|
||||
"subnet": "Subnet",
|
||||
"parent": "Parent",
|
||||
"adminUsersTitle": "Manage users"
|
||||
"adminUsersTitle": "Manage users",
|
||||
"filtersPanelStepText": "Use these filters to search or load configurations.",
|
||||
"organizationalUnitsStepText": "List of Organizational Units. Click on one to view details.",
|
||||
"defaultMenuLabel": "Main menu",
|
||||
"noClients": "No clients"
|
||||
}
|
||||
|
|
|
@ -363,6 +363,7 @@
|
|||
"selectOptionPlaceholder": "Selecciona una opción",
|
||||
"yesOption": "Sí",
|
||||
"noOption": "No",
|
||||
"menuLabel": "Menu",
|
||||
"actionsColumn": "Acciones",
|
||||
"createServerButton": "Crear servidor",
|
||||
"labelName": "Nombre",
|
||||
|
@ -447,5 +448,9 @@
|
|||
"maintenance": "Mantenimiento",
|
||||
"subnet": "Subred",
|
||||
"parent": "Padre",
|
||||
"adminUsersTitle": "Administrar usuarios"
|
||||
"adminUsersTitle": "Administrar usuarios",
|
||||
"filtersPanelStepText": "Utiliza estos filtros para buscar o cargar configuraciones.",
|
||||
"organizationalUnitsStepText": "Lista de Unidades Organizacionales. Haz clic en una para ver detalles.",
|
||||
"defaultMenuLabel": "Menú por defecto",
|
||||
"noClients": "No hay clientes"
|
||||
}
|
||||
|
|
|
@ -1,535 +1,94 @@
|
|||
<?xml version="1.0"?>
|
||||
<testsuite name="Chrome Headless 131.0.0.0 (Linux x86_64)" package="" timestamp="2024-11-19T13:35:34" id="0" hostname="alvaro-Latitude-3420" tests="38" errors="0" failures="32" time="0.486">
|
||||
<testsuite name="Chrome Headless 131.0.0.0 (Linux x86_64)" package="" timestamp="2024-12-09T10:14:38" id="0" hostname="alvaro-Latitude-3420" tests="80" errors="0" failures="0" time="2.257">
|
||||
<properties>
|
||||
<property name="browser.fullName" value="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/131.0.0.0 Safari/537.36"/>
|
||||
</properties>
|
||||
<testcase name="CreateOperativeSystemComponent should create" time="0.071" classname="CreateOperativeSystemComponent"/>
|
||||
<testcase name="AddClientsToPxeComponent should create" time="0.016" classname="AddClientsToPxeComponent">
|
||||
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'AddClientsToPxeComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
|
||||
error properties: Object({ code: -302 })
|
||||
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
|
||||
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
|
||||
at AddClientsToPxeComponent_Template (ng:///AddClientsToPxeComponent.js:70:9)
|
||||
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
|
||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
|
||||
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
|
||||
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
|
||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
|
||||
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
|
||||
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
|
||||
</failure>
|
||||
</testcase>
|
||||
<testcase name="ClientsComponent should create" time="0.011" classname="ClientsComponent">
|
||||
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'ClientsComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
|
||||
error properties: Object({ code: -302 })
|
||||
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
|
||||
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
|
||||
at ClientsComponent_Template (ng:///ClientsComponent.js:77:9)
|
||||
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
|
||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
|
||||
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
|
||||
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
|
||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
|
||||
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
|
||||
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
|
||||
</failure>
|
||||
</testcase>
|
||||
<testcase name="SoftwareComponent should create" time="0.019" classname="SoftwareComponent">
|
||||
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -> JoyrideService]:
|
||||
NullInjectorError: No provider for JoyrideService!
|
||||
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
|
||||
NullInjectorError: NullInjectorError: No provider for JoyrideService!
|
||||
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
|
||||
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
|
||||
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
|
||||
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
|
||||
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
|
||||
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
|
||||
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
|
||||
at NodeInjectorFactory.SoftwareComponent_Factory [as factory] (ng:///SoftwareComponent/ɵfac.js:6:52)
|
||||
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
|
||||
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
|
||||
</failure>
|
||||
</testcase>
|
||||
<testcase name="OgDhcpSubnetsComponent should create" time="0.02" classname="OgDhcpSubnetsComponent">
|
||||
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[JoyrideService -> JoyrideService]:
|
||||
NullInjectorError: No provider for JoyrideService!
|
||||
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'JoyrideService', 'JoyrideService' ] })
|
||||
NullInjectorError: NullInjectorError: No provider for JoyrideService!
|
||||
at NullInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:76593:21)
|
||||
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
|
||||
at R3Injector.get (http://localhost:9876/_karma_webpack_/vendor.js:77975:27)
|
||||
at ChainedInjector.get (http://localhost:9876/_karma_webpack_/vendor.js:80243:32)
|
||||
at lookupTokenUsingModuleInjector (http://localhost:9876/_karma_webpack_/vendor.js:80586:31)
|
||||
at getOrCreateInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80632:10)
|
||||
at ɵɵdirectiveInject (http://localhost:9876/_karma_webpack_/vendor.js:85998:17)
|
||||
at NodeInjectorFactory.OgDhcpSubnetsComponent_Factory [as factory] (ng:///OgDhcpSubnetsComponent/ɵfac.js:6:7)
|
||||
at getNodeInjectable (http://localhost:9876/_karma_webpack_/vendor.js:80826:38)
|
||||
at createRootComponent (http://localhost:9876/_karma_webpack_/vendor.js:91610:31)
|
||||
</failure>
|
||||
</testcase>
|
||||
<testcase name="CreateCommandComponent should create" time="0.012" classname="CreateCommandComponent">
|
||||
<failure type="">Error: NG0302: The pipe 'translate' could not be found in the 'CreateCommandComponent' component. Verify that it is declared or imported in this module. Find more at https://angular.dev/errors/NG0302
|
||||
error properties: Object({ code: -302 })
|
||||
at getPipeDef (http://localhost:9876/_karma_webpack_/vendor.js:103465:11)
|
||||
at ɵɵpipe (http://localhost:9876/_karma_webpack_/vendor.js:103408:15)
|
||||
at CreateCommandComponent_Template (ng:///CreateCommandComponent.js:15:9)
|
||||
at executeTemplate (http://localhost:9876/_karma_webpack_/vendor.js:86203:5)
|
||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87365:7)
|
||||
at renderComponent (http://localhost:9876/_karma_webpack_/vendor.js:87311:3)
|
||||
at renderChildComponents (http://localhost:9876/_karma_webpack_/vendor.js:87411:5)
|
||||
at renderView (http://localhost:9876/_karma_webpack_/vendor.js:87393:7)
|
||||
at ComponentFactory.create (http://localhost:9876/_karma_webpack_/vendor.js:91476:9)
|
||||
at initComponent (http://localhost:9876/_karma_webpack_/vendor.js:116898:45)
|
||||
</failure>
|
||||
</testcase>
|
||||
<testcase name="RepositoriesComponent should create" time="0.012" classname="RepositoriesComponent">
|
||||
<failure type="">NullInjectorError: R3InjectorError(DynamicTestModule)[HttpClient -> 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>
|
||||
<testcase name="CreateSoftwareComponent should create" time="0.12" classname="CreateSoftwareComponent"/>
|
||||
<testcase name="CommandsComponent should create" time="0.104" classname="CommandsComponent"/>
|
||||
<testcase name="OgbootStatusComponent should create the component" time="0.054" classname="OgbootStatusComponent"/>
|
||||
<testcase name="PxeComponent should have a defined currentPage" time="0.069" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined itemsPerPage" time="0.041" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined component" time="0.035" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined loading" time="0.037" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined page" time="0.036" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined selectedElements" time="0.032" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined pageSizeOptions" time="0.034" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined selectedItem" time="0.031" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined length" time="0.029" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined datePipe" time="0.028" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined dataSource" time="0.029" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined baseUrl" time="0.029" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined alertMessage" time="0.025" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined pxeTemplates" time="0.027" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should have a defined filters" time="0.029" classname="PxeComponent"/>
|
||||
<testcase name="PxeComponent should create the component" time="0.025" classname="PxeComponent"/>
|
||||
<testcase name="AdminComponent debería renderizar dos botones" time="0.012" classname="AdminComponent"/>
|
||||
<testcase name="AdminComponent debería tener un botón con routerLink a "/users"" time="0.005" classname="AdminComponent"/>
|
||||
<testcase name="AdminComponent debería aplicar la clase "fab-button" a ambos botones" time="0.005" classname="AdminComponent"/>
|
||||
<testcase name="AdminComponent debería crear el componente" time="0.006" classname="AdminComponent"/>
|
||||
<testcase name="CreateOperativeSystemComponent should create" time="0.017" classname="CreateOperativeSystemComponent"/>
|
||||
<testcase name="PxeBootFilesComponent should create" time="0.048" classname="PxeBootFilesComponent"/>
|
||||
<testcase name="CalendarComponent should create" time="0.05" classname="CalendarComponent"/>
|
||||
<testcase name="CreateRepositoryComponent should create" time="0.018" classname="CreateRepositoryComponent"/>
|
||||
<testcase name="PXEimagesComponent should create" time="0.077" classname="PXEimagesComponent"/>
|
||||
<testcase name="SoftwareComponent should create" time="0.055" classname="SoftwareComponent"/>
|
||||
<testcase name="CreateCommandComponent should create" time="0.033" classname="CreateCommandComponent"/>
|
||||
<testcase name="AppComponent should default to Spanish if no language is saved in localStorage" time="0.01" classname="AppComponent"/>
|
||||
<testcase name="AppComponent should create the app" time="0.003" classname="AppComponent"/>
|
||||
<testcase name="AppComponent should set language to Spanish in sessionStorage on ngOnInit" time="0.002" classname="AppComponent"/>
|
||||
<testcase name="AppComponent should have as title 'ogWebconsole'" time="0.003" classname="AppComponent"/>
|
||||
<testcase name="AppComponent should set the language from localStorage on creation" time="0.003" classname="AppComponent"/>
|
||||
<testcase name="DeployImageComponent should create" time="0.08" classname="DeployImageComponent"/>
|
||||
<testcase name="CreateSoftwareProfileComponent should create" time="0.085" classname="CreateSoftwareProfileComponent"/>
|
||||
<testcase name="OgDhcpSubnetsComponent should create" time="0.078" classname="OgDhcpSubnetsComponent"/>
|
||||
<testcase name="OgDhcpSubnetsComponent should call syncSubnets and handle success" time="0.044" classname="OgDhcpSubnetsComponent"/>
|
||||
<testcase name="SoftwareProfileComponent should create" time="0.051" classname="SoftwareProfileComponent"/>
|
||||
<testcase name="RolesComponent should create" time="0.021" classname="RolesComponent"/>
|
||||
<testcase name="RolesComponent should initialize the dataSource" time="0.005" classname="RolesComponent"/>
|
||||
<testcase name="RolesComponent should have a defined columns array" time="0.005" classname="RolesComponent"/>
|
||||
<testcase name="RolesComponent should have a default itemsPerPage value" time="0.007" classname="RolesComponent"/>
|
||||
<testcase name="EnvVarsComponent should create" time="0.019" classname="EnvVarsComponent"/>
|
||||
<testcase name="MainRepositoryViewComponent should create" time="0.071" classname="MainRepositoryViewComponent"/>
|
||||
<testcase name="StatusComponent should create" time="0.022" classname="StatusComponent"/>
|
||||
<testcase name="CommandsTaskComponent should create" time="0.039" classname="CommandsTaskComponent"/>
|
||||
<testcase name="LoginComponent should add rotating class to the logo when loading" time="0.025" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should create" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should add "invalid" class to username input if it is invalid and touched" time="0.011" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should show a success toast message" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should not add rotating class to the logo when not loading" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should show error message on login failure" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should toggle password visibility when clicking the button" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should disable the login button if username or password is missing" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should change language to English and save to localStorage" time="0.009" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should call onLogin and navigate on successful login" time="0.012" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should enable the login button if username and password are present" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should change language to Spanish and save to localStorage" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="LoginComponent should show an error toast message" time="0.01" classname="LoginComponent"/>
|
||||
<testcase name="ExecuteCommandComponent should create" time="0.025" classname="ExecuteCommandComponent"/>
|
||||
<testcase name="CreateImageComponent should create" time="0.025" classname="CreateImageComponent"/>
|
||||
<testcase name="OgdhcpComponent should create" time="0.005" classname="OgdhcpComponent"/>
|
||||
<testcase name="DashboardComponent should create the component" time="0.006" classname="DashboardComponent"/>
|
||||
<testcase name="GroupsComponent should call getFilters on ngOnInit" time="0.141" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should call search on ngOnInit" time="0.011" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should call getFilters method" time="0.014" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should call onSelectUnidad method" time="0.009" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should call onTabChange method" time="0.011" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should create" time="0.013" classname="GroupsComponent"/>
|
||||
<testcase name="GroupsComponent should call search method" time="0.011" classname="GroupsComponent"/>
|
||||
<testcase name="ServerInfoDialogComponent should create" time="0.013" classname="ServerInfoDialogComponent"/>
|
||||
<testcase name="UsersComponent should call search on init" time="0.032" classname="UsersComponent"/>
|
||||
<testcase name="UsersComponent should have default values for pagination" time="0.006" classname="UsersComponent"/>
|
||||
<testcase name="UsersComponent should create" time="0.008" classname="UsersComponent"/>
|
||||
<testcase name="UsersComponent should define displayedColumns" time="0.007" classname="UsersComponent"/>
|
||||
<testcase name="UsersComponent should initialize the dataSource" time="0.007" classname="UsersComponent"/>
|
||||
<testcase name="OperativeSystemComponent should create" time="0.041" classname="OperativeSystemComponent"/>
|
||||
<testcase name="RepositoriesComponent should create" time="0.037" classname="RepositoriesComponent"/>
|
||||
<system-out>
|
||||
<![CDATA[
|
||||
<![CDATA[Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error fetching organizational units:', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/organizational-units?type=classroom&page=1&itemsPerPage=50', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/organizational-units?type=classroom&page=1&itemsPerPage=50: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
|
||||
,Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error al sincronizar', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives/sync', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives/sync: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
|
||||
,Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error fetching images', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
|
||||
,Chrome Headless 131.0.0.0 (Linux x86_64) ERROR: 'Error fetching og lives', HttpErrorResponse{headers: HttpHeaders{normalizedNames: Map{}, lazyUpdate: null, headers: Map{}}, status: 0, statusText: 'Unknown Error', url: 'https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000', ok: false, name: 'HttpErrorResponse', message: 'Http failure response for https://127.0.0.1:8443/og-lives?page=1&itemsPerPage=1000: 0 Unknown Error', error: ProgressEvent{isTrusted: true}}
|
||||
|
||||
]]>
|
||||
</system-out>
|
||||
<system-err/>
|
||||
|
|