Merge branch 'develop'
commit
faee839580
|
@ -24,6 +24,7 @@
|
|||
"jwt-decode": "^4.0.0",
|
||||
"ngx-joyride": "^2.5.0",
|
||||
"ngx-toastr": "^19.0.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "^0.14.6"
|
||||
|
@ -35,6 +36,7 @@
|
|||
"@angular/localize": "^18.1.0",
|
||||
"@ngx-env/builder": "^18.0.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
|
@ -5902,6 +5904,15 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/papaparse": {
|
||||
"version": "5.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.15.tgz",
|
||||
"integrity": "sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
|
||||
|
@ -11955,6 +11966,11 @@
|
|||
"node": "^16.14.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/papaparse": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
|
||||
"integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"jwt-decode": "^4.0.0",
|
||||
"ngx-joyride": "^2.5.0",
|
||||
"ngx-toastr": "^19.0.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "^0.14.6"
|
||||
|
@ -37,6 +38,7 @@
|
|||
"@angular/localize": "^18.1.0",
|
||||
"@ngx-env/builder": "^18.0.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
|
|
|
@ -113,8 +113,6 @@ import { CreateSoftwareProfileComponent } from './components/software-profile/cr
|
|||
import { OperativeSystemComponent } from './components/operative-system/operative-system.component';
|
||||
import { CreateOperativeSystemComponent } from './components/operative-system/create-operative-system/create-operative-system.component';
|
||||
import { ShowTemplateContentComponent } from './components/ogboot/pxe/show-template-content/show-template-content.component';
|
||||
import { AddClientsToPxeComponent } from './components/ogboot/pxe/add-clients-to-pxe/add-clients-to-pxe.component';
|
||||
import { ClientsComponent } from './components/ogboot/pxe/clients/clients.component';
|
||||
import { RepositoriesComponent } from './components/repositories/repositories.component';
|
||||
import { CreateRepositoryComponent } from './components/repositories/create-repository/create-repository.component';
|
||||
import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component';
|
||||
|
@ -125,7 +123,7 @@ import { JoyrideModule } from 'ngx-joyride';
|
|||
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';
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
}
|
||||
|
@ -200,8 +198,6 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
OperativeSystemComponent,
|
||||
CreateOperativeSystemComponent,
|
||||
ShowTemplateContentComponent,
|
||||
AddClientsToPxeComponent,
|
||||
ClientsComponent,
|
||||
RepositoriesComponent,
|
||||
CreateRepositoryComponent,
|
||||
ExecuteCommandComponent,
|
||||
|
@ -238,6 +234,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
MatDatepickerModule,
|
||||
MatNativeDateModule,
|
||||
MatSliderModule,
|
||||
MatSortModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title">{{ 'adminImagesTitle' | translate }}</h2>
|
||||
<h2 class="title">{{ 'adminUsersTitle' | translate }}</h2>
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="primary" (click)="addUser()">
|
||||
{{ 'addUser' | translate }}
|
||||
|
|
|
@ -71,12 +71,12 @@ table {
|
|||
}
|
||||
|
||||
.chip-failed {
|
||||
background-color: #f15d5d !important;
|
||||
background-color: #e87979 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-success {
|
||||
background-color: #32c532 !important;
|
||||
background-color: #46c446 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,11 @@ export class TaskLogsComponent implements OnInit {
|
|||
header: 'Hilo de trabajo',
|
||||
cell: (trace: any) => `${trace.jobId}`
|
||||
},
|
||||
{
|
||||
columnDef: 'output',
|
||||
header: 'Logs',
|
||||
cell: (trace: any) => `${trace.output}`
|
||||
},
|
||||
{
|
||||
columnDef: 'executedAt',
|
||||
header: 'Programación de ejecución',
|
||||
|
|
|
@ -5,16 +5,6 @@
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
.client-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.client-icon {
|
||||
flex-shrink: 0;
|
||||
margin-right: 20px;
|
||||
|
@ -63,7 +53,6 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.icon-pc {
|
||||
font-size: 25px;
|
||||
color: #3b82f6;
|
||||
|
@ -85,9 +74,7 @@
|
|||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 30px;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
|
@ -246,3 +233,29 @@
|
|||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.disk-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 3;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.disk-usage {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -12,71 +12,68 @@
|
|||
</mat-menu>
|
||||
</div>
|
||||
|
||||
<mat-tab-group dynamicHeight>
|
||||
<mat-tab label="Datos generales">
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading" class="client-info">
|
||||
<div class="info-section">
|
||||
<div class="two-column-table">
|
||||
<div class="table-row" *ngFor="let clientData of generalData">
|
||||
<div class="column property">{{ clientData?.property }}</div>
|
||||
<div class="column value">{{ clientData?.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two-column-table">
|
||||
<div class="table-row" *ngFor="let clientData of networkData">
|
||||
<div class="column property">{{ clientData?.property }}</div>
|
||||
<div class="column value">{{ clientData?.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!loading" class="client-info">
|
||||
<div class="info-section">
|
||||
<div class="two-column-table">
|
||||
<div class="table-row" *ngFor="let clientData of generalData">
|
||||
<div class="column property">{{ clientData?.property }}</div>
|
||||
<div class="column value">{{ clientData?.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab label="Discos/Particiones">
|
||||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
|
||||
<div class="two-column-table">
|
||||
<div class="table-row" *ngFor="let clientData of networkData">
|
||||
<div class="column property">{{ clientData?.property }}</div>
|
||||
<div class="column value">{{ clientData?.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image" >
|
||||
<ng-container *ngIf="column.columnDef !== 'size'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<mat-chip color="primary" >
|
||||
{{ (image.size / 1024).toFixed(2) }} GB
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="charts-wrapper">
|
||||
<ng-container *ngIf="diskUsageData && diskUsageData.length > 0">
|
||||
<div class="charts-row">
|
||||
<div *ngFor="let disk of chartDisk" class="disk-usage">
|
||||
<ngx-charts-pie-chart
|
||||
[view]="view"
|
||||
[results]="disk.chartData"
|
||||
[legend]="showLegend">
|
||||
</ngx-charts-pie-chart>
|
||||
|
||||
<h3>Disco {{ disk.diskNumber }}</h3>
|
||||
<p>Usado: {{ (disk.used).toFixed(2) }} GB ({{ disk.percentage }}%)</p>
|
||||
<p>Total: {{ disk.total }} GB</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="disk-container">
|
||||
<!-- Tabla de particiones -->
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef !== 'size'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<mat-chip color="primary">
|
||||
{{ (image.size / 1024).toFixed(2) }} GB
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
</div>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Gráfico circular -->
|
||||
<div class="charts-container">
|
||||
<ng-container *ngIf="diskUsageData && diskUsageData.length > 0">
|
||||
<div *ngFor="let disk of chartDisk" class="disk-usage">
|
||||
<ngx-charts-pie-chart
|
||||
[view]="view"
|
||||
[results]="disk.chartData"
|
||||
[doughnut]="true"
|
||||
>
|
||||
</ngx-charts-pie-chart>
|
||||
|
||||
<h3>Disco {{ disk.diskNumber }}</h3>
|
||||
<p>Usado: {{ (disk.used).toFixed(2) }} GB ({{ disk.percentage }}%)</p>
|
||||
<p>Total: {{ disk.total }} GB</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
|
|
|
@ -6,6 +6,7 @@ import {PartitionAssistantComponent} from "./partition-assistant/partition-assis
|
|||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {Router} from "@angular/router";
|
||||
import {EditClientComponent} from "../../shared/clients/edit-client/edit-client.component";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
interface ClientInfo {
|
||||
property: string;
|
||||
|
@ -33,7 +34,7 @@ export class ClientMainViewComponent implements OnInit {
|
|||
partitions: any[] = [];
|
||||
commands: any[] = [];
|
||||
chartDisk: any[] = [];
|
||||
view: [number, number] = [600, 300];
|
||||
view: [number, number] = [300, 200];
|
||||
showLegend: boolean = true;
|
||||
|
||||
arrayCommands: any[] = [
|
||||
|
@ -90,7 +91,8 @@ export class ClientMainViewComponent implements OnInit {
|
|||
constructor(
|
||||
private http: HttpClient,
|
||||
private dialog: MatDialog,
|
||||
private router: Router
|
||||
private router: Router,
|
||||
private toastService: ToastrService
|
||||
) {
|
||||
const url = window.location.href;
|
||||
const segments = url.split('/');
|
||||
|
@ -98,38 +100,47 @@ export class ClientMainViewComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.clientData = history.state.clientData;
|
||||
this.loadPartitions()
|
||||
this.updateGeneralData();
|
||||
this.updateNetworkData();
|
||||
this.clientData = history.state.clientData['@id'];
|
||||
this.loadClient(this.clientData)
|
||||
this.loadCommands()
|
||||
this.calculateDiskUsage();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
|
||||
loadClient = (uuid: string) => {
|
||||
this.http.get<any>(`${this.baseUrl}${uuid}`).subscribe({
|
||||
next: data => {
|
||||
this.clientData = data;
|
||||
this.updateGeneralData();
|
||||
this.updateNetworkData();
|
||||
this.loadPartitions()
|
||||
this.loading = false;
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error al obtener el cliente:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
updateGeneralData() {
|
||||
this.generalData = [
|
||||
{ property: 'Nombre', value: this.clientData?.name || '' },
|
||||
{ property: 'Uuid', value: this.clientData?.uuid || '' },
|
||||
{ property: 'IP', value: this.clientData?.ip || '' },
|
||||
{ property: 'MAC', value: this.clientData?.mac || '' },
|
||||
{ property: 'Nº de serie', value: this.clientData?.serialNumber || '' },
|
||||
{ property: 'Netiface', value: this.clientData?.netiface || '' },
|
||||
{ property: 'Netiface', value: this.clientData?.netiface || this.clientData?.organizationalUnit?.networkSettings?.netiface || '' },
|
||||
{ property: 'Perfil hardware', value: this.clientData?.hardwareProfile?.description || '' },
|
||||
{ property: 'Menú', value: this.clientData?.menu?.description || '' },
|
||||
];
|
||||
}
|
||||
|
||||
updateNetworkData() {
|
||||
this.networkData = [
|
||||
{ property: 'Pxe', value: this.clientData?.template?.name || '' },
|
||||
{ property: 'Remote Pc', value: this.clientData.remotePc || '' },
|
||||
{ property: 'Subred', value: this.clientData?.subnet || '' },
|
||||
{ property: 'OGlive', value: this.clientData?.ogLive?.name || '' },
|
||||
{ property: 'Autoexec', value: '' },
|
||||
{ property: 'Repositorio', value: this.clientData?.repository?.name || '' },
|
||||
{ property: 'Validación', value: this.clientData?.organizationalUnit?.networkSettings?.validation || '' },
|
||||
{ property: 'Pxe', value: this.clientData?.template?.name || '' },
|
||||
{ property: 'Creado por', value: this.clientData?.createdBy || '' }
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -186,17 +197,8 @@ export class ClientMainViewComponent implements OnInit {
|
|||
dialogRef.afterClosed().subscribe();
|
||||
}
|
||||
|
||||
getStrokeOffset(partitions: any[], index: number): number {
|
||||
const totalSize = partitions.reduce((acc, part) => acc + (part.size / 1024), 0);
|
||||
|
||||
const offset = partitions.slice(0, index).reduce((acc, part) => acc + (part.size / 1024), 0);
|
||||
console.log(offset, totalSize)
|
||||
|
||||
return totalSize > 0 ? (offset / totalSize) : 0;
|
||||
}
|
||||
|
||||
loadPartitions(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}`).subscribe({
|
||||
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[partitionNumber]=ASC`).subscribe({
|
||||
next: data => {
|
||||
this.dataSource = data['hydra:member'];
|
||||
this.partitions = data['hydra:member'];
|
||||
|
@ -231,6 +233,55 @@ export class ClientMainViewComponent implements OnInit {
|
|||
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');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
powerOnClient(): void {
|
||||
const payload = {
|
||||
client: this.clientData['@id']
|
||||
}
|
||||
|
||||
this.http.post(`${this.baseUrl}${this.clientData.repository['@id']}/wol`, payload).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
powerOffClient(): void {
|
||||
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/power-off`, {}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
openPartitionAssistant(): void {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@subnetsTitle">Deploy imagen en {{ clientName }}</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import {MatTableDataSource} from "@angular/material/table";
|
|||
import {SelectionModel} from "@angular/cdk/collections";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'app-deploy-image',
|
||||
|
@ -20,7 +20,7 @@ export class DeployImageComponent {
|
|||
images: any[] = [];
|
||||
clientName: string = '';
|
||||
selectedImage: string | null = null;
|
||||
selectedOption: string | null = null;
|
||||
selectedOption: string | null = 'deploy-image';
|
||||
selectedMethod: string | null = null;
|
||||
selectedPartition: any = null;
|
||||
mcastIp: string = '';
|
||||
|
@ -92,12 +92,13 @@ export class DeployImageComponent {
|
|||
constructor(
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
private route: ActivatedRoute
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.clientId = this.route.snapshot.paramMap.get('id');
|
||||
|
||||
this.selectedOption = 'deploy-image';
|
||||
this.loadPartitions();
|
||||
this.loadImages();
|
||||
}
|
||||
|
@ -134,7 +135,7 @@ export class DeployImageComponent {
|
|||
}
|
||||
|
||||
loadImages() {
|
||||
const url = `${this.baseUrl}/images?page=1&itemsPerPage=1000`;
|
||||
const url = `${this.baseUrl}/images?status=success&page=1&itemsPerPage=1000`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
this.images = response['hydra:member'];
|
||||
|
@ -146,6 +147,21 @@ export class DeployImageComponent {
|
|||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.selectedImage) {
|
||||
this.toastService.error('Debe seleccionar una imagen');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selectedMethod) {
|
||||
this.toastService.error('Debe seleccionar un método');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.selectedPartition) {
|
||||
this.toastService.error('Debe seleccionar una partición');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
client: `/clients/${this.clientId}`,
|
||||
method: this.selectedMethod,
|
||||
|
@ -162,6 +178,7 @@ export class DeployImageComponent {
|
|||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Imagen creada exitosamente');
|
||||
this.router.navigate(['/commmands-logs'])
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error:', error);
|
||||
|
|
|
@ -122,3 +122,43 @@ button.remove-btn:hover {
|
|||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.partition-assistant .row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
flex: 0 0 65%;
|
||||
max-width: 65%;
|
||||
padding-right: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 0 0 35%;
|
||||
max-width: 35%;
|
||||
}
|
||||
|
||||
.partition-bar {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.partition-segment {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-container ngx-charts-pie-chart {
|
||||
display: block;
|
||||
align-content: center;
|
||||
justify-self: center;
|
||||
}
|
||||
|
|
|
@ -1,67 +1,100 @@
|
|||
<h2 mat-dialog-title>Asistente de particionado</h2>
|
||||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@subnetsTitle">Asistente de particionado</h2>
|
||||
<div class="subnets-button-row">
|
||||
<button mat-flat-button color="primary" (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>
|
||||
<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="partition-bar">
|
||||
<div
|
||||
*ngFor="let partition of disk.partitions"
|
||||
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
|
||||
class="partition-segment"
|
||||
>
|
||||
{{ partition.type }} ({{ (partition.size / 1024).toFixed(2) }} GB)
|
||||
<div class="partition-bar">
|
||||
<div
|
||||
*ngFor="let partition of activePartitions(disk.diskNumber)"
|
||||
[ngStyle]="{'width': partition.percentage + '%', 'background-color': partition.color}"
|
||||
class="partition-segment"
|
||||
>
|
||||
{{ partition.partitionCode }} ({{ (partition.size / 1024).toFixed(2) }} GB)
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button mat-flat-button color="primary" (click)="addPartition(disk.diskNumber)">Añadir partición</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="form-container">
|
||||
<table class="partition-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Partición</th>
|
||||
<th>Tipo partición</th>
|
||||
<th>S. ficheros</th>
|
||||
<th>Tamaño (MB)</th>
|
||||
<th>Tamaño (%)</th>
|
||||
<th>Formatear</th>
|
||||
<th>Eliminar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container *ngFor="let partition of disk.partitions; let j = index">
|
||||
<tr *ngIf="!partition.removed">
|
||||
<td>{{ partition.partitionNumber }}</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.partitionCode" required>
|
||||
<option *ngFor="let type of partitionTypes" [value]="type.name">
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.filesystem" required>
|
||||
<option *ngFor="let type of filesystemTypes" [value]="type.name">
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.size" required
|
||||
(input)="updatePartitionSize(disk.diskNumber, j, partition.size)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.percentage"
|
||||
(input)="updatePartitionSizeFromPercentage(disk.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>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<ngx-charts-pie-chart
|
||||
[view]="view"
|
||||
[results]="disk.chartData"
|
||||
[doughnut]="true"
|
||||
>
|
||||
</ngx-charts-pie-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<button mat-flat-button color="primary" (click)="addPartition(disk.diskNumber)"> + </button>
|
||||
|
||||
<table class="partition-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Partición</th>
|
||||
<th>Tipo partición</th>
|
||||
<th>Tamaño (MB)</th>
|
||||
<th>Uso (%)</th>
|
||||
<th>Formatear</th>
|
||||
<th>Eliminar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let partition of disk.partitions; let j = index">
|
||||
<td>{{ partition.partitionNumber }}</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.type">
|
||||
<option value="NTFS">NTFS</option>
|
||||
<option value="LINUX">LINUX</option>
|
||||
<option value="CACHE">CACHE</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.size"
|
||||
(input)="updatePartitionSize(disk.diskNumber, j, partition.size)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.memoryUsage"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" [(ngModel)]="partition.format" />
|
||||
</td>
|
||||
<td>
|
||||
<button (click)="removePartition(disk.diskNumber, partition)" class="remove-btn">X</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>
|
||||
|
|
|
@ -2,18 +2,23 @@ import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/c
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import {MAT_DIALOG_DATA} from "@angular/material/dialog";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import { PARTITION_TYPES } from '../../../../../shared/constants/partition-types';
|
||||
import { FILESYSTEM_TYPES } from '../../../../../shared/constants/filesystem-types';
|
||||
import {toUnredirectedSourceFile} from "@angular/compiler-cli/src/ngtsc/util/src/typescript";
|
||||
|
||||
interface Partition {
|
||||
uuid?: string;
|
||||
partitionNumber: number;
|
||||
size: number;
|
||||
type: string;
|
||||
partitionCode: string;
|
||||
filesystem: string;
|
||||
sizeBytes: number;
|
||||
memoryUsage: number;
|
||||
format: boolean;
|
||||
color: string;
|
||||
percentage: number;
|
||||
removed: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
@ -24,19 +29,27 @@ interface Partition {
|
|||
export class PartitionAssistantComponent implements OnInit {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
|
||||
partitionTypes = PARTITION_TYPES;
|
||||
filesystemTypes = FILESYSTEM_TYPES;
|
||||
errorMessage = '';
|
||||
originalPartitions: any[] = [];
|
||||
clientId: string | null = null;
|
||||
newPartitions: any[] = [];
|
||||
updateRequests: any[] = [];
|
||||
data: any = {};
|
||||
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[] }[] = [];
|
||||
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = [];
|
||||
|
||||
private apiUrl: string = this.baseUrl + '/partitions';
|
||||
|
||||
view: [number, number] = [400, 300];
|
||||
showLegend = true;
|
||||
showLabels = true;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
private route: ActivatedRoute
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -77,33 +90,81 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
partitionNumber: partition.partitionNumber,
|
||||
size: this.convertBytesToGB(partition.size),
|
||||
memoryUsage: partition.memoryUsage,
|
||||
type: partition.type || partition.filesystem || 'NTFS',
|
||||
partitionCode: partition.partitionCode,
|
||||
filesystem: partition.filesystem,
|
||||
sizeBytes: partition.size,
|
||||
format: false,
|
||||
color: '#1f1b91',
|
||||
percentage: 0
|
||||
percentage: 0,
|
||||
removed: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
disksMap.forEach((disk, diskNumber) => {
|
||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||
|
||||
const used = this.calculateUsedSpace(disk.partitions);
|
||||
const percentage = (used / disk.totalDiskSize) * 100;
|
||||
const chartData = this.generateChartData(disk.partitions);
|
||||
|
||||
this.disks.push({
|
||||
diskNumber: diskNumber,
|
||||
totalDiskSize: disk.totalDiskSize,
|
||||
partitions: disk.partitions
|
||||
partitions: disk.partitions,
|
||||
chartData: chartData,
|
||||
used: used,
|
||||
percentage: percentage
|
||||
});
|
||||
});
|
||||
this.disks.forEach((disk) => {
|
||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||
});
|
||||
}
|
||||
|
||||
convertBytesToGB(bytes: number): number {
|
||||
return bytes
|
||||
}
|
||||
|
||||
activePartitions(diskNumber: number) {
|
||||
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
|
||||
if (disk) {
|
||||
return disk.partitions.filter((partition) => !partition.removed);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
updatePartitionPercentages(partitions: Partition[], totalDiskSize: number) {
|
||||
let totalUsedPercentage = 0;
|
||||
|
||||
partitions.forEach((partition) => {
|
||||
partition.percentage = (partition.size / totalDiskSize) * 100
|
||||
partition.percentage = Number(((partition.size / totalDiskSize) * 100).toFixed(2))
|
||||
totalUsedPercentage += partition.percentage;
|
||||
});
|
||||
|
||||
const unusedPercentage = 100 - totalUsedPercentage;
|
||||
|
||||
const chartData = partitions
|
||||
.filter((partition) => !partition.removed)
|
||||
.map((partition) => ({
|
||||
name: `Partición ${partition.partitionNumber}`,
|
||||
value: partition.percentage,
|
||||
color: partition.color,
|
||||
}));
|
||||
|
||||
if (unusedPercentage > 0) {
|
||||
chartData.push({
|
||||
name: 'Espacio sin usar',
|
||||
value: unusedPercentage,
|
||||
color: '#ffffff'
|
||||
});
|
||||
}
|
||||
|
||||
const disk = this.disks.find(d => d.partitions === partitions);
|
||||
if (disk) {
|
||||
disk.chartData = chartData;
|
||||
}
|
||||
}
|
||||
|
||||
addPartition(diskNumber: number) {
|
||||
|
@ -112,21 +173,26 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
if (disk) {
|
||||
const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize);
|
||||
if (remainingGB > 0) {
|
||||
const removedPartitions = disk.partitions.filter((p) => !p.removed);
|
||||
const maxPartitionNumber =
|
||||
disk.partitions.length > 0 ? Math.max(...disk.partitions.map((p) => p.partitionNumber)) : 0;
|
||||
removedPartitions.length > 0 ? Math.max(...removedPartitions.map((p) => p.partitionNumber)) : 0;
|
||||
const newPartitionNumber = maxPartitionNumber + 1;
|
||||
|
||||
|
||||
disk.partitions.push({
|
||||
partitionNumber: newPartitionNumber,
|
||||
size: 0,
|
||||
type: 'NTFS',
|
||||
partitionCode: 'LINUX',
|
||||
filesystem: 'EXT4',
|
||||
memoryUsage: 0,
|
||||
sizeBytes: 0,
|
||||
format: false,
|
||||
color: '#' + Math.floor(Math.random() * 16777215).toString(16),
|
||||
percentage: 0
|
||||
percentage: 0,
|
||||
removed: false
|
||||
});
|
||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||
this.updateDiskChart(disk);
|
||||
} else {
|
||||
this.errorMessage = 'No hay suficiente espacio libre en el disco para crear una nueva partición.';
|
||||
}
|
||||
|
@ -138,7 +204,9 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
if (disk) {
|
||||
const partition = disk.partitions[index];
|
||||
const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize) + partition.size;
|
||||
|
||||
if (partition) {
|
||||
partition.percentage = Number(((size / disk.totalDiskSize) * 100).toFixed(2));
|
||||
}
|
||||
if (size > remainingGB) {
|
||||
this.errorMessage = `El tamaño de la partición no puede superar el espacio libre (${remainingGB.toFixed(2)} GB).`;
|
||||
} else {
|
||||
|
@ -148,41 +216,16 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
|
||||
partition.percentage = (size / disk.totalDiskSize) * 100;
|
||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||
this.updateDiskChart(disk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatePartitionPercentage(diskNumber: number, index: number, percentage: number) {
|
||||
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
|
||||
if (disk) {
|
||||
const partition = disk.partitions[index];
|
||||
|
||||
const newSizeMB = (percentage / 100) * disk.totalDiskSize;
|
||||
|
||||
const totalPercentage = disk.partitions.reduce((sum, part) => sum + (part === partition ? percentage : part.percentage), 0);
|
||||
|
||||
if (totalPercentage > 100) {
|
||||
this.errorMessage = 'El tamaño total en porcentaje de las particiones no puede exceder el 100%';
|
||||
partition.percentage = 100 - (totalPercentage - percentage);
|
||||
} else {
|
||||
this.errorMessage = '';
|
||||
partition.percentage = percentage;
|
||||
partition.size = newSizeMB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
getRemainingGB(partitions: Partition[], totalDiskSize: number): number {
|
||||
const totalUsedGB = partitions.reduce((acc, partition) => acc + partition.size, 0);
|
||||
return Math.max(0, totalDiskSize - totalUsedGB);
|
||||
}
|
||||
|
||||
isPartitionModified(original: any, current: Partition): boolean {
|
||||
return this.convertBytesToGB(original.size) !== current.size || original.type !== current.type;
|
||||
}
|
||||
|
||||
getModifiedOrNewPartitions() {
|
||||
const modifiedPartitions: any[] = [];
|
||||
|
||||
|
@ -191,22 +234,11 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
const originalPartition = this.originalPartitions.find(
|
||||
(p) => p.diskNumber === disk.diskNumber && p.partitionNumber === partition.partitionNumber
|
||||
);
|
||||
|
||||
if (!originalPartition) {
|
||||
modifiedPartitions.push({
|
||||
partition,
|
||||
diskNumber: disk.diskNumber,
|
||||
partitionNumber: partition.partitionNumber,
|
||||
isNew: true
|
||||
});
|
||||
} else if (this.isPartitionModified(originalPartition, partition)) {
|
||||
modifiedPartitions.push({
|
||||
partition,
|
||||
diskNumber: disk.diskNumber,
|
||||
partitionNumber: partition.partitionNumber,
|
||||
isNew: false
|
||||
});
|
||||
}
|
||||
modifiedPartitions.push({
|
||||
partition,
|
||||
diskNumber: disk.diskNumber,
|
||||
partitionNumber: partition.partitionNumber,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -219,6 +251,7 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
return totalPartitionSize > disk.totalDiskSize;
|
||||
});
|
||||
|
||||
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.';
|
||||
return;
|
||||
|
@ -233,63 +266,78 @@ export class PartitionAssistantComponent implements OnInit {
|
|||
return;
|
||||
}
|
||||
|
||||
modifiedPartitions.forEach(({ partition, diskNumber, partitionNumber, isNew }) => {
|
||||
modifiedPartitions.forEach(({ partition, diskNumber, partitionNumber }) => {
|
||||
const payload = {
|
||||
diskNumber: diskNumber,
|
||||
partitionNumber: partitionNumber,
|
||||
memoryUsage: partition.memoryUsage,
|
||||
size: partition.size,
|
||||
filesystem: partition.type,
|
||||
client: `/clients/${this.clientId}`
|
||||
partitionCode: partition.partitionCode,
|
||||
filesystem: partition.filesystem,
|
||||
client: `/clients/${this.clientId}`,
|
||||
uuid: partition.uuid,
|
||||
removed: partition.removed || false,
|
||||
format: partition.format || false,
|
||||
};
|
||||
|
||||
if (isNew) {
|
||||
this.http.post(this.apiUrl, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Partición creada exitosamente');
|
||||
window.location.reload();
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al crear la partición:', error);
|
||||
this.toastService.error('Error al crear la partición');
|
||||
}
|
||||
);
|
||||
} else if (partition.uuid) {
|
||||
const patchUrl = `${this.apiUrl}/${partition.uuid}`;
|
||||
this.http.patch(patchUrl, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Partición actualizada exitosamente');
|
||||
window.location.reload();
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al actualizar la partición:', error);
|
||||
this.toastService.error('Error al actualizar la partición');
|
||||
}
|
||||
);
|
||||
}
|
||||
this.newPartitions.push(payload);
|
||||
});
|
||||
|
||||
if (this.newPartitions.length > 0) {
|
||||
const bulkPayload = { partitions: this.newPartitions };
|
||||
|
||||
this.http.post(this.apiUrl, bulkPayload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Particiones creadas exitosamente');
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al crear las particiones:', error);
|
||||
this.toastService.error('Error al crear las particiones');
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
removePartition(diskNumber: number, partition: Partition) {
|
||||
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
|
||||
|
||||
if (disk) {
|
||||
const index = disk.partitions.indexOf(partition);
|
||||
if (index !== -1) {
|
||||
disk.partitions.splice(index, 1);
|
||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||
|
||||
if (partition.uuid) {
|
||||
const deleteUrl = `${this.apiUrl}/${partition.uuid}`;
|
||||
this.http.delete(deleteUrl).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Partición eliminada exitosamente');
|
||||
window.location.reload();
|
||||
},
|
||||
(error) => {}
|
||||
);
|
||||
}
|
||||
const partitionToRemove = disk.partitions.find((p) => p === partition);
|
||||
if (partitionToRemove) {
|
||||
partitionToRemove.removed = true;
|
||||
}
|
||||
this.updateDiskChart(disk);
|
||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||
}
|
||||
}
|
||||
|
||||
updatePartitionSizeFromPercentage(diskNumber: number, partitionIndex: number, percentage: number): void {
|
||||
const disk = this.disks.find(d => d.diskNumber === diskNumber);
|
||||
if (disk) {
|
||||
const partition = disk.partitions[partitionIndex];
|
||||
if (partition) {
|
||||
partition.size = (disk.totalDiskSize * percentage) / 100;
|
||||
}
|
||||
this.updateDiskChart(disk);
|
||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||
}
|
||||
}
|
||||
|
||||
calculateUsedSpace(partitions: Partition[]): number {
|
||||
return partitions.reduce((acc, partition) => acc + partition.size, 0);
|
||||
}
|
||||
|
||||
generateChartData(partitions: Partition[]): any[] {
|
||||
return partitions.map((partition) => ({
|
||||
name: `Partición ${partition.partitionNumber}`,
|
||||
value: partition.percentage,
|
||||
color: partition.color
|
||||
}));
|
||||
}
|
||||
|
||||
updateDiskChart(disk: any) {
|
||||
disk.chartData = this.generateChartData(disk.partitions);
|
||||
disk.used = this.calculateUsedSpace(disk.partitions);
|
||||
disk.percentage = (disk.used / disk.totalDiskSize) * 100;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,321 +1,586 @@
|
|||
.groupLists-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.search-container mat-form-field {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex-grow: 1;
|
||||
margin: 10px;
|
||||
|
||||
border: 2px solid rgba(102, 102, 102, 0.103)
|
||||
.card-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.unidad-card {
|
||||
flex: 1 1 20%;
|
||||
background-color: #fafafa;
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
box-shadow: none !important;
|
||||
|
||||
}
|
||||
|
||||
.elements-card {
|
||||
flex: 1 1 75%;
|
||||
background-color: #fafafa;
|
||||
height: 600px;
|
||||
overflow-y: auto;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.element-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.details-card, .classroom-view {
|
||||
flex: 1 1 25%;
|
||||
}
|
||||
|
||||
mat-card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.title-with-breadcrumb {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
mat-card-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
|
||||
mat-card-subtitle a {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
color: #929292;
|
||||
}
|
||||
|
||||
mat-card-subtitle a:hover {
|
||||
text-decoration: none;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.groups-button-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
.button-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.item-content mat-icon {
|
||||
margin-right: 10px;
|
||||
button[mat-raised-button] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.clickable-item:hover {
|
||||
mat-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
mat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.unidad-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selected-item {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
align-self: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.unidad-card.selected-item {
|
||||
border: 2px solid #1976d2;
|
||||
}
|
||||
|
||||
mat-card-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
mat-card-title mat-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 8px;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
mat-card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.actions mat-icon {
|
||||
cursor: pointer;
|
||||
margin-left: 16px;
|
||||
color: #757575;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.actions mat-icon:hover {
|
||||
color: #212121;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
height: 200px;
|
||||
font-size: 16px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.classroomBtn-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header mat-form-field {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
.search-container mat-form-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filters {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 300px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.saved-filter {
|
||||
.details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 300px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.results {
|
||||
.details-wrapper {
|
||||
width: 95%;
|
||||
padding: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.details-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.results-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
button[mat-raised-button] {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.card-container {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.groups-button-row {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
mat-card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.unidad-card {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.details-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 30px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
mat-tree {
|
||||
background-color: #f9f9f9;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node:hover {
|
||||
background-color: #e3f2fd;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node button.mat-icon-button {
|
||||
margin-left: auto;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node button.mat-icon-button:hover {
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node span {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node mat-icon {
|
||||
margin-right: 10px;
|
||||
color: #757575;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node.expandable mat-icon {
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node.expandable.disabled mat-icon {
|
||||
color: grey;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node:hover mat-icon {
|
||||
color: black;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node mat-icon.node-icon {
|
||||
color: #757575;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node mat-icon.node-icon.organizational-unit {
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node mat-icon.node-icon.classroom {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node mat-icon.node-icon.client {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node mat-icon.node-icon.group {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node button.mat-icon-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node button.mat-icon-button.disabled-toggle {
|
||||
color: grey;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node button.mat-icon-button.disabled-toggle:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node:hover {
|
||||
background-color: #e3f2fd;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
mat-tree mat-tree-node.disabled:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.selected-node {
|
||||
background-color: #e0f7fa;
|
||||
border-left: 4px solid #3F51B5;
|
||||
padding-left: calc(16px - 4px);
|
||||
}
|
||||
|
||||
.mat-menu-item .mat-menu-item-submenu-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filters-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
height: 250px;
|
||||
}
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.filters-container mat-form-field {
|
||||
flex: 1 1 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
.filter-container {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
mat-card {
|
||||
margin-bottom: 20px;
|
||||
.chip-busy {
|
||||
background-color: indianred !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.mat-tooltip {
|
||||
white-space: pre-line;
|
||||
.chip-og-live {
|
||||
background-color: yellow !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.classroom-grid {
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
.clients-card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
justify-content: flex-start; /* Opcional: para alinear a la izquierda */
|
||||
padding: 16px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.classroom-item {
|
||||
flex: 0 1 calc(16.66% - 16px); /* 6 columnas */
|
||||
max-width: calc(16.66% - 16px);
|
||||
text-align: center;
|
||||
flex: 1 1 calc(25% - 16px);
|
||||
max-width: calc(25% - 16px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.classroom-pc {
|
||||
position: relative;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background-color: #f4f4f4;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pc-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
.classroom-pc .pc-image {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pc-details {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-weight: bold;
|
||||
.pc-details .client-name,
|
||||
.pc-details .client-ip,
|
||||
.pc-details .client-mac {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.client-ip,
|
||||
.client-mac {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-size: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pc-actions {
|
||||
margin-top: 8px;
|
||||
.pc-actions button {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
width: 25%;
|
||||
padding: 16px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.clients-container {
|
||||
width: 75%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.clients-container h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.5em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pc-og-live {
|
||||
border: 2px solid #4caf50;
|
||||
.client-card {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.pc-busy {
|
||||
border: 2px solid #ff9800;
|
||||
.client-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.pc-off {
|
||||
border: 2px solid #f44336;
|
||||
.client-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.pc-linux {
|
||||
border: 2px solid #9c27b0;
|
||||
}
|
||||
|
||||
.pc-windows {
|
||||
border: 2px solid #2196f3;
|
||||
}
|
||||
|
||||
/* Pantallas medianas: 4 columnas */
|
||||
@media (max-width: 1024px) {
|
||||
.classroom-item {
|
||||
flex: 0 1 calc(25% - 16px); /* 4 columnas */
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas pequeñas: 2 columnas */
|
||||
@media (max-width: 768px) {
|
||||
.classroom-item {
|
||||
flex: 0 1 calc(50% - 16px); /* 2 columnas */
|
||||
}
|
||||
}
|
||||
|
||||
/* Pantallas muy pequeñas: 1 columna */
|
||||
@media (max-width: 480px) {
|
||||
.classroom-item {
|
||||
flex: 0 1 100%; /* 1 columna */
|
||||
}
|
||||
}
|
||||
|
||||
.client-text {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
.client-details {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
display: block;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
button[mat-raised-button] {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.clients-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.client-item-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.client-details-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.view-toggle-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.clients-list .list-item-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.client-card, .list-item-content {
|
||||
border: 1px solid #ccc;
|
||||
padding: 1rem;
|
||||
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;
|
||||
gap: 16px;
|
||||
margin: 16px 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.filters-container mat-form-field {
|
||||
flex: 1 1 300px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.client-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-self: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,156 +1,361 @@
|
|||
<mat-tab-group (selectedTabChange)="onTabChange($event)">
|
||||
<mat-tab label="{{ 'generalTabLabel' | translate }}">
|
||||
<div class="header-container" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
<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="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 (click)="openBottomSheet()" joyrideStep="keyStep" text="{{ 'keyStepText' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
{{ 'legendButton' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-expansion-panel *ngIf="isTreeViewActive" class="filters-panel">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>{{ 'filters' | translate }}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="filters-container">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'searchClient' | translate }}</mat-label>
|
||||
<input matInput (input)="onClientFilterInput($event)" placeholder="Buscar nombre, IP, estado o MAC">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-select (selectionChange)="loadSelectedFilter($event.value)" placeholder="Cargar filtro" disabled>
|
||||
<mat-option *ngFor="let savedFilter of savedFilterNames" [value]="savedFilter">
|
||||
{{ savedFilter[0] }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'searchTree' | translate }}</mat-label>
|
||||
<input matInput (input)="onTreeFilterInput($event)" placeholder="Buscar nombre o tipo" disabled>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'filterByType' | translate }}</mat-label>
|
||||
<mat-select [(value)]="selectedTreeFilter" (selectionChange)="filterTree(searchTerm, $event.value)" disabled>
|
||||
<mat-option [value]=""> {{ 'all' | translate }} </mat-option>
|
||||
<mat-option value="classrooms-group">{{ 'classroomsGroup' | translate }}</mat-option>
|
||||
<mat-option value="classroom">{{ 'classrooms' | translate }}</mat-option>
|
||||
<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 }}
|
||||
</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>
|
||||
<h2 class="title" joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">{{ 'adminGroupsTitle' | translate }}</h2>
|
||||
<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-raised-button (click)="openBottomSheet()" joyrideStep="keyStep" text="{{ 'keyStepText' | translate }}" matTooltipShowDelay="1000">{{ 'legendButton' | 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 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" />
|
||||
<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' }}
|
||||
|
||||
</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>
|
||||
</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)">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clients-table" *ngIf="currentView === 'list'">
|
||||
<table mat-table matSort [dataSource]="selectedClients" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<div class="client-info">
|
||||
<div class="client-name">{{ client.name }}</div>
|
||||
<div class="client-ip">{{ client.ip }}</div>
|
||||
<div class="client-ip">{{ client.mac }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</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>
|
||||
</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>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="subnet">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'subnet' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.subnet }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="pxeTemplate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'pxeTemplate' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.template?.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="parentName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'parent' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.parentName }} </td>
|
||||
</ng-container>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
<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>
|
||||
</button>
|
||||
<button mat-menu-item (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>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5, 10, 20]" showFirstLastButtons></mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="groupLists-container">
|
||||
<mat-card class="card unidad-card" joyrideStep="unitStep" text="{{ 'unitStepText' | translate }}" matTooltipShowDelay="1000" matTooltipPosition="above">
|
||||
<mat-card-title>{{ 'organizationalUnitTitle' | translate }}</mat-card-title>
|
||||
<mat-card-content>
|
||||
<mat-spinner *ngIf="loading"></mat-spinner>
|
||||
<mat-list *ngIf="!loading">
|
||||
<mat-list-item *ngFor="let unidad of organizationalUnits" [ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}" (click)="onSelectUnidad(unidad)">
|
||||
<div class="item-content">
|
||||
<mat-icon>apartment</mat-icon>
|
||||
{{ unidad.name }}
|
||||
<span class="actions">
|
||||
<mat-icon mat-button [matMenuTriggerFor]="menu" (click)="$event.stopPropagation()">more_vert</mat-icon>
|
||||
<mat-menu #menu="matMenu">
|
||||
<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)="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)="onShowClick($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>
|
||||
</mat-menu>
|
||||
</span>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="card elements-card">
|
||||
<mat-card-title>
|
||||
<div class="title-with-breadcrumb">
|
||||
<span>{{ 'internalElementsTitle' | translate }}</span>
|
||||
<mat-card-subtitle>
|
||||
<ng-container *ngFor="let crumb of breadcrumb; let i = index">
|
||||
<a (click)="navigateToBreadcrumb(i)">{{ crumb }}</a>
|
||||
<span *ngIf="i < breadcrumb.length - 1"> > </span>
|
||||
</ng-container>
|
||||
</mat-card-subtitle>
|
||||
</div>
|
||||
</mat-card-title>
|
||||
<mat-card-content class="element-content">
|
||||
<mat-spinner *ngIf="loadingChildren"></mat-spinner>
|
||||
|
||||
<!-- Mostrar lista normal si no es un aula -->
|
||||
<mat-list *ngIf="!loadingChildren && selectedDetail?.type !== 'classroom'">
|
||||
<div *ngIf="children.length === 0" class="empty-list">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>{{ 'noInternalElementsMessage' | translate }}</span>
|
||||
</div>
|
||||
<mat-list-item *ngFor="let child of children"
|
||||
[ngClass]="{'selected-item': child === selectedUnidad, 'clickable-item': true}"
|
||||
(click)="onSelectChild(child)">
|
||||
<div class="item-content">
|
||||
<mat-icon [ngSwitch]="child.type">
|
||||
<ng-container *ngSwitchCase="'organizational-unit'">apartment</ng-container>
|
||||
<ng-container *ngSwitchCase="'classrooms-group'">meeting_room</ng-container>
|
||||
<ng-container *ngSwitchCase="'classroom'">school</ng-container>
|
||||
<ng-container *ngSwitchCase="'client'">computer</ng-container>
|
||||
<ng-container *ngSwitchCase="'clients-group'">lan</ng-container>
|
||||
<ng-container *ngSwitchDefault>help_outline</ng-container>
|
||||
</mat-icon>
|
||||
{{ child.name }}
|
||||
<div class="actions">
|
||||
<mat-icon mat-button [matMenuTriggerFor]="menu" (click)="$event.stopPropagation()">more_vert</mat-icon>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="onEditClick($event, child.type, child.uuid)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'editElementMenu' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, child.uuid, child.name, child.type)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'deleteElementMenu' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
|
||||
<!-- Mostrar cuadrícula si es un aula -->
|
||||
<div *ngIf="selectedDetail?.type === 'classroom'" class="classroom-grid">
|
||||
<div *ngFor="let pc of selectedDetail.clients" class="classroom-item">
|
||||
<div class="classroom-pc" [ngClass]="{
|
||||
'pc-og-live': pc.status === 'og-live',
|
||||
'pc-busy': pc.status === 'busy',
|
||||
'pc-windows': pc.status === 'windows' || pc.status === 'windows-session',
|
||||
'pc-linux': pc.status === 'linux' || pc.status === 'linux-session',
|
||||
'pc-macos': pc.status === 'macos',
|
||||
'pc-off': pc.status === 'off'
|
||||
}">
|
||||
<img mat-card-image src="assets/images/client.png" alt="PC Icon" class="pc-image">
|
||||
<div class="pc-details">
|
||||
<span class="client-name">{{ pc.name }}</span>
|
||||
<span class="client-ip">{{ pc.ip }}</span>
|
||||
<span class="client-mac">{{ pc.mac }}</span>
|
||||
</div>
|
||||
<div class="pc-actions">
|
||||
<button mat-icon-button color="primary" (click)="onEditClick($event, 'client', pc.uuid)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="onDeleteClick($event, pc.uuid, pc.name, 'client')">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="{{ 'advancedSearchTabLabel' | translate }}">
|
||||
<app-advanced-search></app-advanced-search>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="{{ 'clientsTabLabel' | translate }}">
|
||||
<app-client-tab-view #clientTab></app-client-tab-view>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="{{ 'organizationalUnitsTabLabel' | translate }}">
|
||||
<app-organizational-unit-tab-view #organizationalUnitTab></app-organizational-unit-tab-view>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -113,131 +113,4 @@ describe('GroupsComponent', () => {
|
|||
expect(component.onSelectUnidad).toHaveBeenCalledWith(unidad);
|
||||
});
|
||||
|
||||
it('should call onSelectChild method', () => {
|
||||
spyOn(component, 'onSelectChild');
|
||||
const child = { id: '1', name: 'Test', type: 'unit' } as any;
|
||||
component.onSelectChild(child);
|
||||
expect(component.onSelectChild).toHaveBeenCalledWith(child);
|
||||
});
|
||||
|
||||
it('should call navigateToBreadcrumb method', () => {
|
||||
spyOn(component, 'navigateToBreadcrumb');
|
||||
component.navigateToBreadcrumb(1);
|
||||
expect(component.navigateToBreadcrumb).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should call loadChildrenAndClients method', () => {
|
||||
spyOn(component, 'loadChildrenAndClients');
|
||||
component.loadChildrenAndClients('1');
|
||||
expect(component.loadChildrenAndClients).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('should call onDeleteClick method', () => {
|
||||
spyOn(component, 'onDeleteClick');
|
||||
const event = new MouseEvent('click');
|
||||
component.onDeleteClick(event, 'uuid', 'name', 'client');
|
||||
expect(component.onDeleteClick).toHaveBeenCalledWith(event, 'uuid', 'name', 'client');
|
||||
});
|
||||
|
||||
it('should call onEditClick method', () => {
|
||||
spyOn(component, 'onEditClick');
|
||||
const event = new MouseEvent('click');
|
||||
component.onEditClick(event, 'client', 'uuid');
|
||||
expect(component.onEditClick).toHaveBeenCalledWith(event, 'client', 'uuid');
|
||||
});
|
||||
|
||||
it('should call onShowClick method', () => {
|
||||
spyOn(component, 'onShowClick');
|
||||
const event = new MouseEvent('click');
|
||||
component.onShowClick(event, { type: 'unit' });
|
||||
expect(component.onShowClick).toHaveBeenCalledWith(event, { type: 'unit' });
|
||||
});
|
||||
|
||||
it('should call onTreeClick method', () => {
|
||||
spyOn(component, 'onTreeClick');
|
||||
const event = new MouseEvent('click');
|
||||
component.onTreeClick(event, { type: 'unit' });
|
||||
expect(component.onTreeClick).toHaveBeenCalledWith(event, { type: 'unit' });
|
||||
});
|
||||
|
||||
it('should call onExecuteCommand method', () => {
|
||||
spyOn(component, 'onExecuteCommand');
|
||||
const event = new MouseEvent('click');
|
||||
component.onExecuteCommand(event, 'child', 'name', 'type');
|
||||
expect(component.onExecuteCommand).toHaveBeenCalledWith(event, 'child', 'name', 'type');
|
||||
});
|
||||
|
||||
it('should call openSnackBar method', () => {
|
||||
spyOn(component, 'openSnackBar');
|
||||
component.openSnackBar(true, 'message');
|
||||
expect(component.openSnackBar).toHaveBeenCalledWith(true, 'message');
|
||||
});
|
||||
|
||||
it('should call openBottomSheet method', () => {
|
||||
spyOn(component, 'openBottomSheet');
|
||||
component.openBottomSheet();
|
||||
expect(component.openBottomSheet).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call roomMap method', () => {
|
||||
spyOn(component, 'roomMap');
|
||||
component.roomMap();
|
||||
expect(component.roomMap).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call applyFilter method', () => {
|
||||
spyOn(component, 'applyFilter');
|
||||
component.applyFilter();
|
||||
expect(component.applyFilter).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onPageChange method', () => {
|
||||
spyOn(component, 'onPageChange');
|
||||
const event = { pageIndex: 1, pageSize: 10 } as any;
|
||||
component.onPageChange(event);
|
||||
expect(component.onPageChange).toHaveBeenCalledWith(event);
|
||||
});
|
||||
|
||||
it('should call saveFilters method', () => {
|
||||
spyOn(component, 'saveFilters');
|
||||
component.saveFilters();
|
||||
expect(component.saveFilters).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call loadSelectedFilter method', () => {
|
||||
spyOn(component, 'loadSelectedFilter');
|
||||
component.loadSelectedFilter(['name', 'uuid']);
|
||||
expect(component.loadSelectedFilter).toHaveBeenCalledWith(['name', 'uuid']);
|
||||
});
|
||||
|
||||
it('should call onCheckboxChange method', () => {
|
||||
spyOn(component, 'onCheckboxChange');
|
||||
const event = { checked: true } as any;
|
||||
component.onCheckboxChange(event, 'name', 'uuid');
|
||||
expect(component.onCheckboxChange).toHaveBeenCalledWith(event, 'name', 'uuid');
|
||||
});
|
||||
|
||||
it('should call toggleSelectAll method', () => {
|
||||
spyOn(component, 'toggleSelectAll');
|
||||
component.toggleSelectAll();
|
||||
expect(component.toggleSelectAll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call isSelected method', () => {
|
||||
spyOn(component, 'isSelected');
|
||||
component.isSelected('name');
|
||||
expect(component.isSelected).toHaveBeenCalledWith('name');
|
||||
});
|
||||
|
||||
it('should call sendActions method', () => {
|
||||
spyOn(component, 'sendActions');
|
||||
component.sendActions();
|
||||
expect(component.sendActions).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call iniciarTour method', () => {
|
||||
spyOn(component, 'iniciarTour');
|
||||
component.iniciarTour();
|
||||
expect(component.iniciarTour).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,458 +1,592 @@
|
|||
import {Component, OnInit, ViewChild} from '@angular/core';
|
||||
import { DataService } from './services/data.service';
|
||||
import { ClientCollection, UnidadOrganizativa } from './model/model';
|
||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatBottomSheet } from '@angular/material/bottom-sheet';
|
||||
import { MatTabChangeEvent } from '@angular/material/tabs';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||
import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { DataService } from './services/data.service';
|
||||
import { UnidadOrganizativa, Client, TreeNode, FlatNode, Command } from './model/model';
|
||||
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 { 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 {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 {AdvancedSearchComponent} from "./components/advanced-search/advanced-search.component";
|
||||
import {MatTabChangeEvent} from "@angular/material/tabs";
|
||||
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 { ExecuteCommandComponent } from '../commands/main-commands/execute-command/execute-command.component';
|
||||
import { ExecuteCommandOuComponent } from './shared/execute-command-ou/execute-command-ou.component';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
|
||||
enum NodeType {
|
||||
OrganizationalUnit = 'organizational-unit',
|
||||
ClassroomsGroup = 'classrooms-group',
|
||||
Classroom = 'classroom',
|
||||
ClientsGroup = 'clients-group',
|
||||
Client = 'client',
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-groups',
|
||||
templateUrl: './groups.component.html',
|
||||
styleUrls: ['./groups.component.css']
|
||||
styleUrls: ['./groups.component.css'],
|
||||
})
|
||||
export class GroupsComponent implements OnInit {
|
||||
export class GroupsComponent implements OnInit, OnDestroy {
|
||||
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 = 1;
|
||||
pageSizeOptions: number[] = [5, 10, 25, 100];
|
||||
selectedElements: any[] = [];
|
||||
isAllSelected: boolean = false;
|
||||
filters: { [key: string]: string } = {};
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
selectedDetail: UnidadOrganizativa | null = null;
|
||||
loading = false;
|
||||
searchTerm = '';
|
||||
treeControl: FlatTreeControl<FlatNode>;
|
||||
treeFlattener: MatTreeFlattener<TreeNode, FlatNode>;
|
||||
treeDataSource: MatTreeFlatDataSource<TreeNode, FlatNode>;
|
||||
selectedNode: TreeNode | null = null;
|
||||
commands: Command[] = [];
|
||||
commandsLoading = false;
|
||||
selectedClients = new MatTableDataSource<Client>([]);
|
||||
cols = 4;
|
||||
selectedClientsOriginal: Client[] = [];
|
||||
currentView: 'card' | 'list' = 'list';
|
||||
isTreeViewActive = false;
|
||||
savedFilterNames: [string, string][] = [];
|
||||
selectedTreeFilter = '';
|
||||
syncStatus = false;
|
||||
syncingClientId: string | null = null;
|
||||
private originalTreeData: TreeNode[] = [];
|
||||
|
||||
displayedColumns: string[] = ['name', 'oglive', 'status', 'maintenace', 'subnet', 'pxeTemplate', 'parentName', 'actions'];
|
||||
|
||||
private _sort!: MatSort;
|
||||
private _paginator!: MatPaginator;
|
||||
|
||||
@ViewChild(MatSort)
|
||||
set matSort(ms: MatSort) {
|
||||
this._sort = ms;
|
||||
if (this.selectedClients) {
|
||||
this.selectedClients.sort = this._sort;
|
||||
}
|
||||
}
|
||||
|
||||
@ViewChild(MatPaginator)
|
||||
set matPaginator(mp: MatPaginator) {
|
||||
this._paginator = mp;
|
||||
if (this.selectedClients) {
|
||||
this.selectedClients.paginator = this._paginator;
|
||||
}
|
||||
}
|
||||
@ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent;
|
||||
@ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent;
|
||||
|
||||
private subscriptions: Subscription = new Subscription();
|
||||
|
||||
constructor(
|
||||
private dataService: DataService,
|
||||
public dialog: MatDialog,
|
||||
private toastService: ToastrService,
|
||||
private _bottomSheet: MatBottomSheet,
|
||||
private http: HttpClient,
|
||||
private joyrideService: JoyrideService
|
||||
) {}
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private dataService: DataService,
|
||||
public dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
private joyrideService: JoyrideService,
|
||||
private toastr: ToastrService
|
||||
) {
|
||||
this.treeFlattener = new MatTreeFlattener<TreeNode, FlatNode>(
|
||||
this.transformer,
|
||||
(node) => node.level,
|
||||
(node) => node.expandable,
|
||||
(node) => node.children
|
||||
);
|
||||
|
||||
this.treeControl = new FlatTreeControl<FlatNode>(
|
||||
(node) => node.level,
|
||||
(node) => node.expandable
|
||||
);
|
||||
|
||||
this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.search();
|
||||
this.getFilters();
|
||||
this.updateGridCols();
|
||||
window.addEventListener('resize', this.updateGridCols);
|
||||
|
||||
this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => {
|
||||
const lowerTerm = filter.toLowerCase();
|
||||
return (
|
||||
client.name.toLowerCase().includes(lowerTerm) ||
|
||||
client.ip?.toLowerCase().includes(lowerTerm) ||
|
||||
client.status?.toLowerCase().includes(lowerTerm) ||
|
||||
client.mac?.toLowerCase().includes(lowerTerm)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent;
|
||||
@ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent;
|
||||
ngOnDestroy(): void {
|
||||
window.removeEventListener('resize', this.updateGridCols);
|
||||
this.subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
onTabChange(event: MatTabChangeEvent) {
|
||||
switch (event.index) {
|
||||
case 2:
|
||||
this.clientTabComponent.search();
|
||||
break;
|
||||
case 3:
|
||||
this.organizationalUnitTabComponent.search();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
private transformer = (node: TreeNode, level: number): FlatNode => ({
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
level,
|
||||
expandable: !!node.children?.length,
|
||||
hasClients: node.hasClients,
|
||||
ip: node.ip,
|
||||
'@id': node['@id'],
|
||||
});
|
||||
|
||||
toggleView(view: 'card' | 'list'): void {
|
||||
this.currentView = view;
|
||||
}
|
||||
|
||||
updateGridCols = (): void => {
|
||||
const width = window.innerWidth;
|
||||
this.cols = width <= 600 ? 1 : width <= 960 ? 2 : width <= 1280 ? 3 : 4;
|
||||
};
|
||||
|
||||
clearSelection(): void {
|
||||
this.selectedUnidad = null;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
getFilters(): void {
|
||||
this.dataService.getFilters().subscribe(
|
||||
data => {
|
||||
this.savedFilterNames = data.map((filter: any) => [filter.name, filter.uuid]);
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching filters:', error);
|
||||
}
|
||||
this.subscriptions.add(
|
||||
this.dataService.getFilters().subscribe(
|
||||
(data) => {
|
||||
this.savedFilterNames = data.map((filter: { name: string; uuid: string; }) => [filter.name, filter.uuid]);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching filters:', error);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
loadSelectedFilter(savedFilter: [string, string]): void {
|
||||
this.subscriptions.add(
|
||||
this.dataService.getFilter(savedFilter[1]).subscribe(
|
||||
(response) => {
|
||||
if (response) {
|
||||
console.log('Filter:', response.filters);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
search(): void {
|
||||
this.loading = true;
|
||||
this.dataService.getOrganizationalUnits(this.searchTerm).subscribe(
|
||||
data => {
|
||||
this.organizationalUnits = data;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching unidades organizativas', error);
|
||||
this.loading = false;
|
||||
}
|
||||
this.subscriptions.add(
|
||||
this.dataService.getOrganizationalUnits(this.searchTerm).subscribe(
|
||||
(data) => {
|
||||
this.organizationalUnits = data;
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching organizational units', 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);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
|
||||
const processHierarchy = (nodes: UnidadOrganizativa[]): UnidadOrganizativa[] => {
|
||||
return nodes.map((node) => ({
|
||||
...node,
|
||||
children: node.children ? processHierarchy(node.children) : [],
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
...this.selectedUnidad!,
|
||||
children: childrenData ? processHierarchy(childrenData) : [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading children:', error);
|
||||
return this.selectedUnidad!;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
this.selectedDetail = target;
|
||||
this.loadChildrenAndClients(target.id);
|
||||
|
||||
private convertToTreeData(data: UnidadOrganizativa): TreeNode[] {
|
||||
const processNode = (node: UnidadOrganizativa): TreeNode => ({
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
'@id': node['@id'],
|
||||
children: node.children?.map(processNode) || [],
|
||||
hasClients: (node.clients?.length ?? 0) > 0,
|
||||
});
|
||||
return [processNode(data)];
|
||||
}
|
||||
|
||||
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 = [];
|
||||
onNodeClick(node: TreeNode): void {
|
||||
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();
|
||||
}
|
||||
this.loadingChildren = false
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching clients', error);
|
||||
this.clientsData = [];
|
||||
this.children = [];
|
||||
this.loadingChildren = false
|
||||
(error) => {
|
||||
console.error('Error fetching clients:', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching children', error);
|
||||
this.children = [];
|
||||
this.loadingChildren = false
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.selectedClients.data = [];
|
||||
this.selectedClients._updateChangeSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
getNodeIcon(node: TreeNode): string {
|
||||
switch (node.type) {
|
||||
case NodeType.OrganizationalUnit:
|
||||
return 'apartment';
|
||||
case NodeType.ClassroomsGroup:
|
||||
return 'doors';
|
||||
case NodeType.Classroom:
|
||||
return 'school';
|
||||
case NodeType.ClientsGroup:
|
||||
return 'lan';
|
||||
case NodeType.Client:
|
||||
return 'computer';
|
||||
default:
|
||||
return 'group';
|
||||
}
|
||||
}
|
||||
|
||||
addOU(event: MouseEvent, parent: TreeNode | null = null): void {
|
||||
event.stopPropagation();
|
||||
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, {
|
||||
data: { parent },
|
||||
width: '900px',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.refreshOrganizationalUnits();
|
||||
});
|
||||
}
|
||||
|
||||
addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
||||
event.stopPropagation();
|
||||
const dialogRef = this.dialog.open(CreateClientComponent, {
|
||||
data: { organizationalUnit },
|
||||
width: '900px',
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.refreshOrganizationalUnits();
|
||||
if (organizationalUnit && organizationalUnit['@id']) {
|
||||
this.refreshClientsForNode(organizationalUnit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private refreshOrganizationalUnits(): void {
|
||||
this.subscriptions.add(
|
||||
this.dataService.getOrganizationalUnits().subscribe(
|
||||
(data) => {
|
||||
this.organizationalUnits = data;
|
||||
if (this.selectedUnidad) {
|
||||
this.loadChildrenAndClients(this.selectedUnidad?.id || '').then((updatedData) => {
|
||||
this.selectedUnidad = updatedData;
|
||||
const treeData = this.convertToTreeData(updatedData);
|
||||
this.originalTreeData = treeData[0]?.children || [];
|
||||
this.treeDataSource.data = [...this.originalTreeData];
|
||||
});
|
||||
}
|
||||
},
|
||||
(error) => console.error('Error fetching organizational units', error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
addOU(event: MouseEvent, parent:any = null): void {
|
||||
onEditNode(event: MouseEvent, node: TreeNode | 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
|
||||
},
|
||||
error => console.error('Error fetching unidades organizativas', error)
|
||||
);
|
||||
});
|
||||
}
|
||||
const uuid = node ? this.extractUuid(node['@id']) : null;
|
||||
if (!uuid) return;
|
||||
|
||||
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;
|
||||
|
||||
if (organizationalUnit && organizationalUnit.id) {
|
||||
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'])
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
if (node && node.type !== NodeType.Client) {
|
||||
this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' });
|
||||
} 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'])
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
|
||||
}
|
||||
}
|
||||
|
||||
onEditClick(event: MouseEvent, type: any, uuid: string): void {
|
||||
onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void {
|
||||
event.stopPropagation();
|
||||
if (type != "client") {
|
||||
const dialogRef = this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px'});
|
||||
} else {
|
||||
const dialogRef = this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' } );
|
||||
}
|
||||
}
|
||||
const uuid = node ? this.extractUuid(node['@id']) : null;
|
||||
if (!uuid) return;
|
||||
|
||||
onShowClick(event: MouseEvent, data: any): void {
|
||||
event.stopPropagation();
|
||||
if (data.type != "client") {
|
||||
const dialogRef = this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px'});
|
||||
}
|
||||
}
|
||||
if (!node) return;
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '400px',
|
||||
data: { name: node.name },
|
||||
});
|
||||
|
||||
onTreeClick(event: MouseEvent, data: any): void {
|
||||
event.stopPropagation();
|
||||
if (data.type != "client") {
|
||||
const dialogRef = this.dialog.open(TreeViewComponent, { data: { data }, width: '800px'});
|
||||
}
|
||||
}
|
||||
|
||||
onExecuteCommand(event: MouseEvent, child: any, name: string, type:string): void {
|
||||
console.log('Executing command on:', child);
|
||||
|
||||
this.dialog.open(ExecuteCommandOuComponent, {
|
||||
width: '50%',
|
||||
data: { childUnitUuid: child }
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
console.log('Comando ejecutado con éxito');
|
||||
} else {
|
||||
console.log('Ejecución de comando cancelada');
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result === true) {
|
||||
this.deleteEntity(uuid, node.type, node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openSnackBar(isError: boolean, message: string) {
|
||||
if (isError) {
|
||||
this.toastService.error(' Error al eliminar la entidad: ' + message, 'Error');
|
||||
} else
|
||||
this.toastService.success(message, 'Éxito');
|
||||
private deleteEntity(uuid: string, type: string, node: TreeNode): void {
|
||||
this.subscriptions.add(
|
||||
this.dataService.deleteElement(uuid, type).subscribe(
|
||||
() => {
|
||||
this.refreshOrganizationalUnits();
|
||||
if (type === NodeType.Client) {
|
||||
this.refreshClientsForNode(node);
|
||||
}
|
||||
this.toastr.success('Entidad eliminada exitosamente');
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error deleting entity:', error);
|
||||
this.toastr.error('Error al eliminar la entidad', error.message);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private refreshClientsForNode(node: TreeNode): void {
|
||||
if (!node['@id']) {
|
||||
this.selectedClients.data = [];
|
||||
return;
|
||||
}
|
||||
this.fetchClientsForNode(node);
|
||||
}
|
||||
|
||||
onEditClick(event: MouseEvent, type: string, uuid: string): void {
|
||||
event.stopPropagation();
|
||||
if (type !== NodeType.Client) {
|
||||
this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' });
|
||||
} else {
|
||||
this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
|
||||
}
|
||||
}
|
||||
|
||||
onRoomMap(room: TreeNode | null): void {
|
||||
if (!room || !room['@id']) return;
|
||||
this.subscriptions.add(
|
||||
this.http.get<{ clients: Client[] }>(`${this.baseUrl}${room['@id']}`).subscribe(
|
||||
(response) => {
|
||||
this.dialog.open(ClassroomViewDialogComponent, {
|
||||
width: '90vw',
|
||||
data: { clients: response.clients },
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching room data:', error);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fetchCommands(): void {
|
||||
this.commandsLoading = true;
|
||||
this.subscriptions.add(
|
||||
this.http.get<{ 'hydra:member': Command[] }>(`${this.baseUrl}/commands?page=1&itemsPerPage=30`).subscribe(
|
||||
(response) => {
|
||||
this.commands = response['hydra:member'];
|
||||
this.commandsLoading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching commands:', error);
|
||||
this.commandsLoading = false;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
executeCommand(command: Command, selectedNode: TreeNode | null): void {
|
||||
|
||||
if (!selectedNode) {
|
||||
this.toastr.error('No hay un nodo seleccionado.');
|
||||
return;
|
||||
} else {
|
||||
this.toastr.success(`Ejecutando comando: ${command.name} en ${selectedNode.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
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 } });
|
||||
}
|
||||
|
||||
onShowDetailsClick(event: MouseEvent, data: TreeNode | null): void {
|
||||
event.stopPropagation();
|
||||
if (data && data.type !== NodeType.Client) {
|
||||
this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px' });
|
||||
} else {
|
||||
if (data) {
|
||||
this.router.navigate(['clients', this.extractUuid(data['@id'])], { state: { clientData: data } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTreeClick(event: MouseEvent, data: TreeNode): void {
|
||||
event.stopPropagation();
|
||||
if (data.type !== NodeType.Client) {
|
||||
this.dialog.open(TreeViewComponent, { data: { data }, width: '800px' });
|
||||
}
|
||||
}
|
||||
|
||||
openBottomSheet(): void {
|
||||
this._bottomSheet.open(LegendComponent);
|
||||
}
|
||||
|
||||
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, 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.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) {
|
||||
this.selectedElements.push(uuid);
|
||||
} else {
|
||||
const index = this.selectedElements.indexOf(name);
|
||||
if (index > -1) {
|
||||
this.selectedElements.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.isAllSelected = this.selectedElements.length === this.filteredResults.length;
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
this.isAllSelected = !this.isAllSelected;
|
||||
|
||||
if (this.isAllSelected) {
|
||||
this.selectedElements = this.filteredResults.map(result => result.uuid);
|
||||
} else {
|
||||
this.selectedElements = [];
|
||||
}
|
||||
}
|
||||
|
||||
isSelected(name: string): boolean {
|
||||
return this.selectedElements.includes(name);
|
||||
|
||||
}
|
||||
|
||||
|
||||
sendActions() {
|
||||
const dialogRef = this.dialog.open(AcctionsModalComponent, { data: { selectedElements: this.selectedElements }, width: '700px'});
|
||||
this.bottomSheet.open(LegendComponent);
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: ['groupsTitleStepText', 'addStep', 'keyStep', 'unitStep', 'elementsStep', 'tabsStep'],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
themeColor: '#3f51b5',
|
||||
});
|
||||
}
|
||||
|
||||
hasChild = (_: number, node: FlatNode): boolean => node.expandable;
|
||||
isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable;
|
||||
|
||||
filterTree(searchTerm: string, filterType: string): void {
|
||||
const filterNodes = (nodes: TreeNode[]): TreeNode[] => {
|
||||
const filteredNodes: TreeNode[] = [];
|
||||
for (const node of nodes) {
|
||||
const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesType = filterType ? node.type.toLowerCase() === filterType.toLowerCase() : true;
|
||||
const filteredChildren = node.children ? filterNodes(node.children) : [];
|
||||
|
||||
if ((matchesName && matchesType) || filteredChildren.length > 0) {
|
||||
filteredNodes.push({ ...node, children: filteredChildren });
|
||||
}
|
||||
}
|
||||
return filteredNodes;
|
||||
};
|
||||
|
||||
const filteredData = filterNodes(this.originalTreeData);
|
||||
this.treeDataSource.data = filteredData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
onTreeFilterInput(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const searchTerm = input?.value || '';
|
||||
this.filterTree(searchTerm, this.selectedTreeFilter);
|
||||
}
|
||||
|
||||
onClientFilterInput(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const searchTerm = input?.value || '';
|
||||
this.filterClients(searchTerm);
|
||||
}
|
||||
|
||||
filterClients(searchTerm: string): void {
|
||||
this.searchTerm = searchTerm.trim().toLowerCase();
|
||||
this.selectedClients.filter = this.searchTerm;
|
||||
}
|
||||
|
||||
|
||||
public setSelectedNode(node: TreeNode): void {
|
||||
this.selectedNode = node;
|
||||
}
|
||||
|
||||
getStatus(client: Client): void {
|
||||
if (!client.uuid || !client['@id']) return;
|
||||
|
||||
this.syncingClientId = client.uuid;
|
||||
this.syncStatus = true;
|
||||
|
||||
this.subscriptions.add(
|
||||
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.toastr.error('Error de conexión con el cliente');
|
||||
this.syncStatus = false;
|
||||
this.syncingClientId = null;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private extractUuid(idPath: string | undefined): string | null {
|
||||
return idPath ? idPath.split('/').pop() || null : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,13 @@ export interface Aula {
|
|||
|
||||
export interface UnidadOrganizativa {
|
||||
id: string;
|
||||
name: string;
|
||||
uuid: string;
|
||||
name: string;
|
||||
type: string;
|
||||
parent: UnidadOrganizativa[];
|
||||
'@id': string;
|
||||
clients?: Client[];
|
||||
children?: UnidadOrganizativa[];
|
||||
parent?: UnidadOrganizativa;
|
||||
}
|
||||
|
||||
export interface OrganizationalUnit {
|
||||
|
@ -26,6 +29,9 @@ export interface OrganizationalUnit {
|
|||
}
|
||||
|
||||
export interface Client {
|
||||
mac: any;
|
||||
status: any;
|
||||
ip: any;
|
||||
"@id": string;
|
||||
"@type": string;
|
||||
id: number;
|
||||
|
@ -38,6 +44,7 @@ export interface Client {
|
|||
createdAt: string;
|
||||
createdBy: string;
|
||||
uuid: string;
|
||||
parentName?: string;
|
||||
}
|
||||
|
||||
export interface ClientCollection {
|
||||
|
@ -51,3 +58,35 @@ export interface ClientCollection {
|
|||
"@type": string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TreeNode {
|
||||
name: string;
|
||||
type: string;
|
||||
'@id'?: string;
|
||||
children?: TreeNode[];
|
||||
hasClients?: boolean;
|
||||
clients?: Client[];
|
||||
ip?: string;
|
||||
}
|
||||
|
||||
export interface FlatNode {
|
||||
name: string;
|
||||
type: string;
|
||||
level: number;
|
||||
expandable: boolean;
|
||||
hasClients?: boolean;
|
||||
ip?: string;
|
||||
'@id'?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface Command {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
name: string;
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -86,10 +86,46 @@ export class DataService {
|
|||
);
|
||||
}
|
||||
|
||||
getOgLives(): Observable<any[]> {
|
||||
const url = `${this.baseUrl}/og-lives`;
|
||||
return this.http.get<any>(url).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);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getRepositories(): Observable<any[]> {
|
||||
const url = `${this.baseUrl}/image-repositories`;
|
||||
return this.http.get<any>(url).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);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteElement(uuid: string, type: string): Observable<void> {
|
||||
const url = type === 'client'
|
||||
? `${this.baseUrl}/clients/${uuid}`
|
||||
: `${this.baseUrl}/organizational-units/${uuid}`;
|
||||
|
||||
console.log('DELETE URL:', url); // Depuración
|
||||
return this.http.delete<void>(url).pipe(
|
||||
catchError(error => {
|
||||
console.error('Error deleting element', error);
|
||||
|
@ -98,6 +134,7 @@ export class DataService {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
changeParent(uuid: string): Observable<void> {
|
||||
const url = `${this.baseUrl}/organizational-units/${uuid}/change-parent`;
|
||||
// @ts-ignore
|
||||
|
@ -184,5 +221,15 @@ export class DataService {
|
|||
);
|
||||
}
|
||||
|
||||
getOrganizationalUnitById(id: string): Observable<any> {
|
||||
const url = `${this.baseUrl}/organizational-units/${id}`;
|
||||
return this.http.get<any>(url).pipe(
|
||||
catchError(error => {
|
||||
console.error('Error fetching organizational unit', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -31,18 +31,11 @@ mat-card {
|
|||
}
|
||||
|
||||
.client-info {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 5px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
color: black;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
margin-top: 5px;
|
||||
font-size: medium;
|
||||
color: gray;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
|
|
|
@ -10,12 +10,9 @@
|
|||
<mat-card appearance="outlined">
|
||||
<div class="client-image-container">
|
||||
<img mat-card-image src="assets/images/client.png" alt="{{ 'clientAlt' | translate }}" class="client-image"/>
|
||||
<div class="client-info">
|
||||
<div class="client-name">{{ client.name }}</div>
|
||||
<div class="client-details">
|
||||
<span>{{ client.ip }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="client-info">
|
||||
<span>{{ client.name }}</span>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
|
|
@ -1,63 +1,161 @@
|
|||
h1 {
|
||||
text-align: center;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
color: #3f51b5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.network-form {
|
||||
.create-client-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
padding: 16px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
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 {
|
||||
padding: 50px;
|
||||
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;
|
||||
}
|
||||
|
||||
button {
|
||||
text-transform: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
.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);
|
||||
}
|
||||
|
||||
.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 {
|
||||
.client-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,92 +1,136 @@
|
|||
<div class="create-client-container">
|
||||
<h1 mat-dialog-title>{{ 'addClientDialogTitle' | translate }}</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>{{ '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>
|
||||
<div class="unit-path">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<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>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name">
|
||||
</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>{{ '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>
|
||||
<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>{{ 'serialNumberLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="serialNumber">
|
||||
</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>{{ '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>
|
||||
<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>{{ '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>
|
||||
<!-- Añadir uon cliente -->
|
||||
|
||||
<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>
|
||||
<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>{{ '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>{{ '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>{{ '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>{{ 'serialNumberLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="serialNumber">
|
||||
</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>
|
||||
<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>{{ '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>{{ '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>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button [disabled]="!clientForm.valid" (click)="onSubmit()">{{ 'addButton' | translate }}</button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@ 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,16 +19,19 @@ export class CreateClientComponent implements OnInit {
|
|||
hardwareProfiles: any[] = [];
|
||||
ogLives: any[] = [];
|
||||
templates: any[] = [];
|
||||
private errorForm: boolean = false;
|
||||
uploadedClients: any[] = [];
|
||||
loading: boolean = false;
|
||||
displayedColumns: string[] = ['name', 'ip'];
|
||||
isSingleClientForm: boolean = false;
|
||||
showTextarea: boolean = true;
|
||||
protected netifaceTypes = [
|
||||
{ "name": 'Eth0', "value": "eth0" },
|
||||
{ "name": 'Eth1', "value": "eth1" },
|
||||
{ "name": 'Eth2', "value": "eth2" },
|
||||
{ name: 'Eth0', value: 'eth0' },
|
||||
{ name: 'Eth1', value: 'eth1' },
|
||||
{ name: 'Eth2', value: 'eth2' }
|
||||
];
|
||||
protected netDriverTypes = [
|
||||
{ "name": 'Generic', "value": "generic" },
|
||||
{ name: 'Generic', value: 'generic' }
|
||||
];
|
||||
loading: boolean = false;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
|
@ -37,16 +41,22 @@ export class CreateClientComponent implements OnInit {
|
|||
private toastService: ToastrService,
|
||||
private dataService: DataService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) { }
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
console.log(this.data);
|
||||
this.initForm();
|
||||
this.loadParentUnits();
|
||||
this.loadHardwareProfiles();
|
||||
this.loadOgLives();
|
||||
this.loadPxeTemplates()
|
||||
this.loadPxeTemplates();
|
||||
}
|
||||
|
||||
initForm(): void {
|
||||
this.clientForm = this.fb.group({
|
||||
organizationalUnit: [this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null, Validators.required],
|
||||
organizationalUnit: [
|
||||
this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null,
|
||||
Validators.required
|
||||
],
|
||||
name: ['', Validators.required],
|
||||
serialNumber: [''],
|
||||
netiface: null,
|
||||
|
@ -54,25 +64,14 @@ export class CreateClientComponent implements OnInit {
|
|||
mac: ['', Validators.required],
|
||||
ip: ['', Validators.required],
|
||||
template: [null],
|
||||
hardwareProfile: [this.data.organizationalUnit && this.data.organizationalUnit.networkSettings && this.data.organizationalUnit.networkSettings.hardwareProfile ? this.data.organizationalUnit.networkSettings.hardwareProfile['@id'] : null],
|
||||
hardwareProfile: [
|
||||
this.data.organizationalUnit?.networkSettings?.hardwareProfile?.['@id'] || null
|
||||
],
|
||||
ogLive: [null]
|
||||
});
|
||||
}
|
||||
|
||||
loadHardwareProfiles(): void {
|
||||
this.dataService.getHardwareProfiles().subscribe(
|
||||
(data: any[]) => {
|
||||
this.hardwareProfiles = data;
|
||||
this.loading = false;
|
||||
},
|
||||
(error: any) => {
|
||||
console.error('Error fetching hardware profiles', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadParentUnits() {
|
||||
loadParentUnits(): void {
|
||||
this.loading = true;
|
||||
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`;
|
||||
|
||||
|
@ -88,9 +87,22 @@ export class CreateClientComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
loadOgLives() {
|
||||
loadHardwareProfiles(): void {
|
||||
this.dataService.getHardwareProfiles().subscribe(
|
||||
(data: any[]) => {
|
||||
this.hardwareProfiles = data;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching hardware profiles:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadOgLives(): void {
|
||||
const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.ogLives = response['hydra:member'];
|
||||
|
@ -109,39 +121,123 @@ export class CreateClientComponent implements OnInit {
|
|||
this.templates = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
console.error('Error fetching PXE templates:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.clientForm.valid) {
|
||||
this.errorForm = false;
|
||||
const formData = this.clientForm.value;
|
||||
formData.ogLive = formData.ogLive;
|
||||
this.http.post(`${this.baseUrl}/clients`, formData).subscribe(
|
||||
response => {
|
||||
this.dialogRef.close(response);
|
||||
this.openSnackBar(false, 'Cliente creado exitosamente');
|
||||
},
|
||||
error => {
|
||||
console.error('Error during POST:', error);
|
||||
this.errorForm = true;
|
||||
this.openSnackBar(true, 'Error al crear el cliente: ' + error.error['hydra:description']);
|
||||
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.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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleClientForm(): void {
|
||||
this.isSingleClientForm = !this.isSingleClientForm;
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
openSnackBar(isError: boolean, message: string) {
|
||||
if (isError) {
|
||||
this.toastService.error(' Error al crear el cliente: ' + message, 'Error');
|
||||
} else {
|
||||
this.toastService.success(message, 'Éxito');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,4 +19,14 @@
|
|||
<mat-icon matListItemIcon>computer</mat-icon>
|
||||
<div matListItemTitle>{{ 'clientTitle' | translate }}</div>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon style="color: green;">school</mat-icon>
|
||||
<div matListItemTitle>Disponible acceso remoto</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon style="color: rgb(209, 5, 5);">school</mat-icon>
|
||||
<div matListItemTitle>No disponible acceso remoto</div>
|
||||
</mat-list-item>
|
||||
|
||||
</mat-list>
|
||||
|
|
|
@ -84,6 +84,22 @@
|
|||
<mat-step *ngIf="generalFormGroup.value.type === 'classroom'" [stepControl]="networkSettingsFormGroup">
|
||||
<form [formGroup]="networkSettingsFormGroup">
|
||||
<ng-template matStepLabel>{{ 'networkSettingsStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="oglive" (selectionChange)="onOgLiveChange($event)">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'nextServerLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="nextServer">
|
||||
|
|
|
@ -37,6 +37,8 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
parentUnits: any[] = [];
|
||||
hardwareProfiles: any[] = [];
|
||||
calendars: any[] = [];
|
||||
ogLives: any[] = [];
|
||||
repositories: any[] = [];
|
||||
selectedCalendarUuid: string | null = null;
|
||||
|
||||
@Output() unitAdded = new EventEmitter();
|
||||
|
@ -59,6 +61,8 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
comments: [''],
|
||||
});
|
||||
this.networkSettingsFormGroup = this._formBuilder.group({
|
||||
ogLive: [null],
|
||||
ogRepository: [null],
|
||||
nextServer: [''],
|
||||
bootFileName: [''],
|
||||
proxy: [''],
|
||||
|
@ -89,6 +93,8 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
this.loadParentUnits();
|
||||
this.loadHardwareProfiles();
|
||||
this.loadCalendars();
|
||||
this.loadOgLives();
|
||||
this.loadRepositories();
|
||||
}
|
||||
|
||||
loadParentUnits() {
|
||||
|
@ -106,6 +112,20 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
loadOgLives() {
|
||||
this.dataService.getOgLives().subscribe(
|
||||
(data: any[]) => this.ogLives = data,
|
||||
error => console.error('Error fetching ogLives', error)
|
||||
);
|
||||
}
|
||||
|
||||
loadRepositories() {
|
||||
this.dataService.getRepositories().subscribe(
|
||||
(data: any[]) => this.repositories = data,
|
||||
error => console.error('Error fetching repositories', error)
|
||||
);
|
||||
}
|
||||
|
||||
loadCalendars() {
|
||||
const apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=30`;
|
||||
this.http.get<any>(apiUrl).subscribe(
|
||||
|
@ -169,14 +189,25 @@ export class CreateOrganizationalUnitComponent implements OnInit {
|
|||
comments: additionalInfoFormValues.comments,
|
||||
networkSettings: { ...networkSettingsFormValues },
|
||||
menu: networkSettingsFormValues.menu || null,
|
||||
ogLive: networkSettingsFormValues.ogLive || null,
|
||||
ogRepository: networkSettingsFormValues.ogRepository || null,
|
||||
hardwareProfile: networkSettingsFormValues.hardwareProfile || null,
|
||||
};
|
||||
}
|
||||
|
||||
onCalendarChange(event: any) {
|
||||
this.generalFormGroup.value.remoteCalendar = event.value;
|
||||
this.selectedCalendarUuid = event.value;
|
||||
}
|
||||
|
||||
onOgLiveChange(event: any) {
|
||||
this.networkSettingsFormGroup.value.ogLive = event.value;
|
||||
}
|
||||
|
||||
onRepositoryChange(event: any) {
|
||||
this.networkSettingsFormGroup.value.ogRepository = event
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
|
|
@ -81,6 +81,22 @@
|
|||
<mat-step [stepControl]="networkSettingsFormGroup">
|
||||
<form [formGroup]="networkSettingsFormGroup">
|
||||
<ng-template matStepLabel>{{ 'networkSettingsStepLabel' | translate }}</ng-template>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'proxyUrlLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="proxy">
|
||||
|
@ -143,12 +159,12 @@
|
|||
<mat-slide-toggle formControlName="validation">{{ 'validationToggle' | translate }}</mat-slide-toggle>
|
||||
<div>
|
||||
<button mat-button matStepperPrevious>{{ 'backButton' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid">{{ 'submitButton' | translate }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid">{{ 'submitButton' | translate }}</button>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { CreateOrganizationalUnitComponent } from '../create-organizational-unit/create-organizational-unit.component';
|
||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||
import {Component, EventEmitter, Inject, OnInit, Output} from '@angular/core';
|
||||
import {FormBuilder, FormGroup} from '@angular/forms';
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
|
||||
import {CreateOrganizationalUnitComponent} from '../create-organizational-unit/create-organizational-unit.component';
|
||||
import {DataService} from "../../../services/data.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
|
@ -23,6 +23,8 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
hardwareProfiles: any[] = [];
|
||||
isEditMode: boolean;
|
||||
currentCalendar: any = [];
|
||||
ogLives: any[] = [];
|
||||
repositories: any[] = [];
|
||||
protected p2pModeOptions = [
|
||||
{"name": 'Leecher', "value": "p2p-mode-leecher"},
|
||||
{"name": 'Peer', "value": "p2p-mode-peer"},
|
||||
|
@ -57,6 +59,8 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
});
|
||||
|
||||
this.networkSettingsFormGroup = this._formBuilder.group({
|
||||
ogLive: [null],
|
||||
repository: [null],
|
||||
proxy: [null],
|
||||
dns: [null],
|
||||
netmask: [null],
|
||||
|
@ -90,6 +94,8 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
this.loadParentUnits();
|
||||
this.loadHardwareProfiles();
|
||||
this.loadCalendars();
|
||||
this.loadOgLives();
|
||||
this.loadRepositories();
|
||||
}
|
||||
|
||||
loadParentUnits() {
|
||||
|
@ -116,6 +122,20 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
loadOgLives() {
|
||||
this.dataService.getOgLives().subscribe(
|
||||
(data: any[]) => this.ogLives = data,
|
||||
error => console.error('Error fetching ogLives', error)
|
||||
);
|
||||
}
|
||||
|
||||
loadRepositories() {
|
||||
this.dataService.getRepositories().subscribe(
|
||||
(data: any[]) => this.repositories = data,
|
||||
error => console.error('Error fetching repositories', error)
|
||||
);
|
||||
}
|
||||
|
||||
loadCalendars() {
|
||||
const apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=30`;
|
||||
this.http.get<any>(apiUrl).subscribe(
|
||||
|
@ -138,9 +158,15 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
);}
|
||||
|
||||
onCalendarChange(event: any) {
|
||||
const selectedCalendarId = event.value;
|
||||
console.log('Selected calendar ID:', selectedCalendarId);
|
||||
this.generalFormGroup.value.remoteCalendar = selectedCalendarId;
|
||||
this.generalFormGroup.value.remoteCalendar = event.value;
|
||||
}
|
||||
|
||||
onOgLiveChange(event: any) {
|
||||
this.networkSettingsFormGroup.value.ogLive = event.value;
|
||||
}
|
||||
|
||||
onRepositoryChange(event: any) {
|
||||
this.networkSettingsFormGroup.value.repository = event.value
|
||||
}
|
||||
|
||||
loadData(uuid: string) {
|
||||
|
@ -171,6 +197,8 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
mcastMode: data.networkSettings.mcastMode,
|
||||
menu: data.networkSettings.menu ? data.networkSettings.menu['@id'] : null,
|
||||
hardwareProfile: data.networkSettings.hardwareProfile ? data.networkSettings.hardwareProfile['@id'] : null,
|
||||
ogLive: data.networkSettings.ogLive ? data.networkSettings.ogLive['@id'] : null,
|
||||
repository: data.networkSettings.repository ? data.networkSettings.repository['@id'] : null,
|
||||
validation: data.networkSettings.validation
|
||||
});
|
||||
this.classroomInfoFormGroup.patchValue({
|
||||
|
@ -245,5 +273,5 @@ export class EditOrganizationalUnitComponent implements OnInit {
|
|||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@ export class ShowOrganizationalUnitComponent {
|
|||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
displayedColumns: string[] = ['property', 'value'];
|
||||
currentCalendar: any;
|
||||
ou: any;
|
||||
generalData: any[] = [];
|
||||
networkData = [
|
||||
];
|
||||
networkData: any[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
|
@ -22,11 +22,22 @@ export class ShowOrganizationalUnitComponent {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data.data.remoteCalendar) {
|
||||
this.loadCurrentCalendar(this.data.data.remoteCalendar.uuid);
|
||||
} else {
|
||||
this.initializeGeneralData();
|
||||
}
|
||||
this.loadOrganizationalUnit(this.data.data['@id']);
|
||||
}
|
||||
|
||||
loadOrganizationalUnit(uuid: string): void {
|
||||
console.log(this.data['@id'])
|
||||
const apiUrl = `${this.baseUrl}${uuid}`;
|
||||
this.http.get<any>(apiUrl).subscribe(
|
||||
response => {
|
||||
this.ou = response;
|
||||
console.log('Organizational unit', this.ou);
|
||||
this.initializeData();
|
||||
},
|
||||
error => {
|
||||
console.error('Error loading organizational unit', error);
|
||||
this.initializeData();
|
||||
});
|
||||
}
|
||||
|
||||
loadCurrentCalendar(uuid: string): void {
|
||||
|
@ -34,26 +45,39 @@ export class ShowOrganizationalUnitComponent {
|
|||
this.http.get<any>(apiUrl).subscribe(
|
||||
response => {
|
||||
this.currentCalendar = response.name;
|
||||
this.initializeGeneralData();
|
||||
this.initializeData();
|
||||
},
|
||||
error => {
|
||||
console.error('Error loading current calendar', error);
|
||||
this.initializeGeneralData();
|
||||
this.initializeData();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
initializeGeneralData(): void {
|
||||
initializeData(): void {
|
||||
this.generalData = [
|
||||
{ property: 'Nombre', value: this.data.data.name },
|
||||
{ property: 'Uuid', value: this.data.data.uuid },
|
||||
{ property: 'Descripción', value: this.data.data.description },
|
||||
{ property: 'Comentarios', value: this.data.data.comments },
|
||||
{ property: 'Tipo', value: this.data.data.type },
|
||||
{ property: 'Unidad organizativa superior', value: this.data.data.parent ? this.data.data.parent.name : '-' },
|
||||
{ property: 'Creado por', value: this.data.data.createdBy },
|
||||
{ property: 'Creado el', value: this.data.data.createdAt },
|
||||
{ property: 'Calendario asociado', value: this.data.data.remoteCalendar ? this.currentCalendar : '-' }
|
||||
{ property: 'Nombre', value: this.ou.name },
|
||||
{ property: 'Uuid', value: this.ou.uuid },
|
||||
{ property: 'Descripción', value: this.ou.description },
|
||||
{ property: 'Comentarios', value: this.ou.comments },
|
||||
{ property: 'Tipo', value: this.ou.type },
|
||||
{ property: 'Unidad organizativa superior', value: this.ou.parent ? this.ou.parent.name : '-' },
|
||||
{ property: 'Creado el', value: this.ou.createdAt },
|
||||
];
|
||||
|
||||
this.networkData = [
|
||||
{ property: 'Calendario asociado', value: this.ou.remoteCalendar ? this.currentCalendar : '-' },
|
||||
{ property: 'Aforo', value: this.ou.capacity },
|
||||
{ property: 'Localización', value: this.ou.location },
|
||||
{ property: 'Calendario', value: this.ou.calendar ? this.ou.calendar.name : '-' },
|
||||
{ property: 'Proyector', value: this.ou.projector },
|
||||
{ property: 'Pizarra', value: this.ou.board },
|
||||
{ property: 'OgLive', value: this.ou.networkSettings.ogLive ? this.ou.networkSettings.ogLive.name : '-' },
|
||||
{ property: 'Repositorio', value: this.ou.networkSettings.repository ? this.ou.networkSettings.repository.name : '-' },
|
||||
{ property: 'OgLog', value: this.ou.networkSettings.oglog },
|
||||
{ property: 'OgShare', value: this.ou.networkSettings.ogshare },
|
||||
{ property: 'Perfil de hardware', value: this.ou.networkSettings.hardwareProfile ? this.ou.networkSettings.hardwareProfile.name : '-' },
|
||||
{ property: 'Máscara de red', value: this.ou.networkSettings.netmask },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,19 +71,23 @@ table {
|
|||
margin: 8px 8px 8px 0;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: #4caf50; /* Verde */
|
||||
.chip-failed {
|
||||
background-color: #e87979 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: #f44336; /* Rojo */
|
||||
.chip-success {
|
||||
background-color: #46c446 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #ff9800; /* Naranja */
|
||||
.chip-pending {
|
||||
background-color: lightgrey !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.chip-in-progress {
|
||||
background-color: #f5a623 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -32,7 +32,12 @@
|
|||
</mat-icon>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'status'">
|
||||
<mat-chip >
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-failed': image.status === 'failed',
|
||||
'chip-success': image.status === 'success',
|
||||
'chip-pending': image.status === 'pending',
|
||||
'chip-in-progress': image.status === 'in-progress'
|
||||
}">
|
||||
{{ getStatusLabel(image[column.columnDef]) }}
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
|
@ -54,7 +59,7 @@
|
|||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item [disabled]="!image.imageFullSum" (click)="toggleAction(image, 'get-aux')">Obtener ficheros auxiliares</button>
|
||||
<button mat-menu-item (click)="toggleAction(image, 'get-aux')">Obtener ficheros auxiliares</button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'" (click)="toggleAction(image, 'delete-trash')">Eliminar imagen temporalmente</button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'trash'" (click)="toggleAction(image, 'recover')">Recuperar imagen de la papelera</button>
|
||||
|
||||
|
|
|
@ -8,30 +8,14 @@
|
|||
</div>
|
||||
|
||||
<div [formGroup]="taskForm" class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="selectUnitStep" text="{{ 'selectUnitDescription' | translate }}">
|
||||
<mat-label>{{ 'selectUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
|
||||
<mat-option *ngIf="loadingUnits" disabled>{{ 'loadingUnitsOption' | translate }}</mat-option>
|
||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="taskForm.get('organizationalUnit')?.invalid">
|
||||
{{ 'requiredFieldError' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="selectClassStep" text="{{ 'selectClassDescription' | translate }}">
|
||||
<mat-label>{{ 'selectClassLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedChild" (selectionChange)="onChildChange()">
|
||||
<mat-option *ngIf="selectedUnitChildren.length === 0" disabled>
|
||||
{{ 'noClassesOption' | translate }}
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let child of selectedUnitChildren" [value]="child['@id']">
|
||||
{{ child.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div [formGroup]="taskForm" class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="selectClassStep" text="{{ 'selectClassDescription' | translate }}">
|
||||
<mat-label>{{ 'selectClassLabel' | translate }}</mat-label>
|
||||
<mat-select (selectionChange)="loadChildUnits($event.value)">
|
||||
<mat-option *ngFor="let unit of units" [value]="unit.uuid">{{ unit.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
|
@ -49,7 +33,7 @@
|
|||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
[disabled]="selectedUnitChildren.length === 0"
|
||||
[disabled]="units.length === 0"
|
||||
(click)="saveOgLiveTemplates()"
|
||||
joyrideStep="saveButtonStep"
|
||||
text="{{ 'saveButtonDescription' | translate }}">
|
||||
|
@ -68,6 +52,16 @@
|
|||
<mat-cell *matCellDef="let element"> {{ element.name }} </mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="ip">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'nameColumnHeader' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element"> {{ element.ip }} </mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="mac">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'nameColumnHeader' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element"> {{ element.mac }} </mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="ogLive">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'templateColumnHeader' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let client">
|
||||
|
|
|
@ -19,7 +19,7 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
selectedUnitChildren: any[] = [];
|
||||
dataSource: any[] = [];
|
||||
taskForm: FormGroup;
|
||||
loadingUnits: boolean = false;
|
||||
units: any[] = [];
|
||||
ogLiveOptions: any[] = [];
|
||||
globalOgLive: string | null = null;
|
||||
length: number = 0;
|
||||
|
@ -27,7 +27,7 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
page: number = 0;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||
filters: any = {};
|
||||
displayedColumns: string[] = ['id', 'name', 'ogLive'];
|
||||
displayedColumns: string[] = ['id', 'ip', 'mac', 'name', 'ogLive'];
|
||||
|
||||
|
||||
constructor(
|
||||
|
@ -43,23 +43,17 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.fetchOrganizationalUnits();
|
||||
this.fetchPxeTemplates();
|
||||
this.loadUnits()
|
||||
}
|
||||
|
||||
fetchOrganizationalUnits(): void {
|
||||
this.loadingUnits = true;
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.availableOrganizationalUnits = response['hydra:member'].filter((item: any) => item.type === 'organizational-unit');
|
||||
this.loadingUnits = false;
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error al cargar las unidades organizativas');
|
||||
this.loadingUnits = false;
|
||||
}
|
||||
);
|
||||
loadUnits() {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?type=classroom&page=1&itemsPerPage=50`).subscribe(
|
||||
response => {
|
||||
this.units = response['hydra:member'];
|
||||
},
|
||||
error => console.error('Error fetching organizational units:', error)
|
||||
);
|
||||
}
|
||||
|
||||
fetchPxeTemplates(): void {
|
||||
|
@ -75,45 +69,15 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
onOrganizationalUnitChange(): void {
|
||||
const selectedUnitId = this.taskForm.get('organizationalUnit')?.value;
|
||||
const selectedUnit = this.availableOrganizationalUnits.find(unit => unit['@id'] === selectedUnitId);
|
||||
|
||||
if (selectedUnit) {
|
||||
this.selectedUnitChildren = this.getAllClassrooms(selectedUnit);
|
||||
} else {
|
||||
this.selectedUnitChildren = [];
|
||||
}
|
||||
loadChildUnits(unitId: string) {
|
||||
this.http.get<any>(`${this.baseUrl}/clients?parent.id${unitId}`).subscribe(
|
||||
response => {
|
||||
this.dataSource = response['hydra:member'];
|
||||
},
|
||||
error => console.error('Error fetching child units:', error)
|
||||
);
|
||||
}
|
||||
|
||||
getAllClassrooms(unit: any): any[] {
|
||||
let classrooms: any[] = [];
|
||||
if (unit.type === 'classroom') {
|
||||
classrooms.push(unit);
|
||||
}
|
||||
if (unit.children) {
|
||||
for (const child of unit.children) {
|
||||
classrooms = classrooms.concat(this.getAllClassrooms(child));
|
||||
}
|
||||
}
|
||||
return classrooms;
|
||||
}
|
||||
|
||||
|
||||
onChildChange(): void {
|
||||
const selectedChildId = this.taskForm.get('selectedChild')?.value;
|
||||
const selectedChild = this.selectedUnitChildren.find(child => child['@id'] === selectedChildId);
|
||||
if (selectedChild && selectedChild.clients) {
|
||||
this.dataSource = selectedChild.clients.map((client: { template: { [x: string]: any; }; }) => ({
|
||||
...client,
|
||||
ogLive: client.template ? client.template['@id'] : null
|
||||
}));
|
||||
} else {
|
||||
this.dataSource = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
applyToAll(): void {
|
||||
this.dataSource = this.dataSource.map(client => ({
|
||||
...client,
|
||||
|
@ -152,12 +116,6 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.fetchPxeTemplates();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
|
@ -173,5 +131,5 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -42,7 +42,6 @@ table {
|
|||
align-items: center;
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
|
@ -55,21 +54,15 @@ table {
|
|||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.example-headers-align .mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.images-button-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-top: 16px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.example-headers-align .mat-mdc-form-field + .mat-mdc-form-field {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: table-cell;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.button-row .mat-mdc-button-base {
|
||||
margin: 8px 8px 8px 0;
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,3 @@
|
|||
<mat-accordion class="example-headers-align">
|
||||
<mat-expansion-panel hideToggle>
|
||||
<mat-expansion-panel-header joyrideStep="serverInfoStep" [text]="'serverInfoDescription' | translate">
|
||||
<mat-panel-title>{{ 'serverInfoTitle' | translate }}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="button-row">
|
||||
<button mat-flat-button color="primary" (click)="syncOgBoot()">{{ 'syncDatabaseButton' | translate }}</button>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button mat-flat-button color="accent" (click)="openSubnetInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
|
||||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
|
@ -20,6 +6,7 @@
|
|||
{{ 'adminImagesTitle' | translate }}
|
||||
</h2>
|
||||
<div class="images-button-row">
|
||||
<button mat-flat-button color="accent" (click)="openSubnetInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
<button mat-flat-button color="primary" (click)="addImage()" joyrideStep="addImageStep" [text]="'addImageButtonDescription' | translate">
|
||||
{{ 'addImageButton' | translate }}
|
||||
</button>
|
||||
|
@ -35,7 +22,7 @@
|
|||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="searchDefaultImageStep" [text]="'searchDefaultDescription' | translate">
|
||||
<mat-label>{{ 'searchDefaultLabel' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="filters['isDefault']" (selectionChange)="search()" [placeholder]="'selectOptionPlaceholder' | translate">
|
||||
|
@ -55,7 +42,8 @@
|
|||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" [text]="'tableDescription' | translate">
|
||||
<mat-spinner *ngIf="loading"></mat-spinner>
|
||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" [text]="'tableDescription' | translate">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
|
@ -78,7 +66,7 @@
|
|||
|
||||
<ng-container *ngIf="column.columnDef === 'name'">
|
||||
<span matTooltip="{{ image.name }}">
|
||||
{{ image.name ? image.name.substring(0, 20) + '...' : '' }}
|
||||
{{ image.name }}
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
|
@ -95,8 +83,8 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'actionsColumnHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let image" joyrideStep="actionsStep" [text]="'actionsDescription' | translate">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actionsColumnHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let image" style="text-align: center;" joyrideStep="actionsStep" [text]="'actionsDescription' | translate">
|
||||
<button mat-icon-button color="info" (click)="showOgLive($event, image)">
|
||||
<mat-icon>{{ 'viewIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
|
@ -110,7 +98,7 @@
|
|||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="toggleAction(image, 'install')">{{ 'installOption' | translate }}</button>
|
||||
<button mat-menu-item [disabled]="image.installed" (click)="toggleAction(image, 'install')">{{ 'installOption' | translate }}</button>
|
||||
<button mat-menu-item [disabled]="!image.installed" (click)="toggleAction(image, 'uninstall')">
|
||||
{{ 'uninstallOption' | translate }}
|
||||
</button>
|
||||
|
|
|
@ -44,11 +44,6 @@ export class PXEimagesComponent implements OnInit {
|
|||
header: 'Nombre de imagen',
|
||||
cell: (user: any) => `${user.name}`
|
||||
},
|
||||
{
|
||||
columnDef: 'downloadUrl',
|
||||
header: 'Url descarga',
|
||||
cell: (user: any) => `${user.downloadUrl}`
|
||||
},
|
||||
{
|
||||
columnDef: 'isDefault',
|
||||
header: 'Imagen por defecto',
|
||||
|
@ -56,7 +51,7 @@ export class PXEimagesComponent implements OnInit {
|
|||
},
|
||||
{
|
||||
columnDef: 'installed',
|
||||
header: 'Imagen instalada en ogBoot',
|
||||
header: 'Imagen instalada',
|
||||
cell: (user: any) => `${user.installed}`
|
||||
},
|
||||
{
|
||||
|
@ -83,8 +78,11 @@ export class PXEimagesComponent implements OnInit {
|
|||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
this.search();
|
||||
this.loadAlert();
|
||||
this.syncOgBoot()
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
addImage(): void {
|
||||
|
@ -99,15 +97,12 @@ export class PXEimagesComponent implements OnInit {
|
|||
}
|
||||
|
||||
search(): void {
|
||||
this.loading = true;
|
||||
this.dataService.getImages(this.filters).subscribe(
|
||||
data => {
|
||||
this.dataSource.data = data;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching og lives', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -250,7 +245,7 @@ export class PXEimagesComponent implements OnInit {
|
|||
syncOgBoot(): void {
|
||||
this.http.post(`${this.apiUrl}/sync`, {})
|
||||
.subscribe(response => {
|
||||
this.toastService.success('Sincronización completada');
|
||||
this.toastService.success('Sincronización con oGBoot exitosa');
|
||||
this.search()
|
||||
}, error => {
|
||||
console.error('Error al sincronizar', error);
|
||||
|
@ -261,7 +256,6 @@ export class PXEimagesComponent implements OnInit {
|
|||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'serverInfoStep',
|
||||
'titleStep',
|
||||
'addImageStep',
|
||||
'searchNameStep',
|
||||
|
@ -275,5 +269,5 @@ export class PXEimagesComponent implements OnInit {
|
|||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mat-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
<h2 mat-dialog-title>{{ 'addClientsTitle' | translate: { subnetName: data.subnetName } }}</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<mat-form-field appearance="fill" class="search-select">
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
[formControl]="clientControl"
|
||||
[matAutocomplete]="clientAuto"
|
||||
[placeholder]="'selectClientPlaceholder' | translate">
|
||||
<mat-autocomplete
|
||||
#clientAuto="matAutocomplete"
|
||||
[displayWith]="displayFnClient"
|
||||
(optionSelected)="onOptionClientSelected($event.option.value)">
|
||||
<mat-option
|
||||
*ngFor="let client of filteredClients | async"
|
||||
[value]="client">
|
||||
{{ client.name }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<div *ngIf="selectedClients.length > 0">
|
||||
<h3>{{ 'selectedClientsTitle' | translate }}</h3>
|
||||
<ul>
|
||||
<li *ngFor="let client of selectedClients">
|
||||
{{ client.name }}
|
||||
<button mat-icon-button color="warn" (click)="removeClient(client)">
|
||||
<mat-icon>{{ 'deleteIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="close()">{{ 'cancelButton' | translate }}</button>
|
||||
<button mat-button (click)="save()">{{ 'addButton' | translate }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -1,69 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddClientsToPxeComponent } from './add-clients-to-pxe.component';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
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 { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatProgressSpinner, MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('AddClientsToPxeComponent', () => {
|
||||
let component: AddClientsToPxeComponent;
|
||||
let fixture: ComponentFixture<AddClientsToPxeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AddClientsToPxeComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatTabsModule,
|
||||
MatAutocompleteModule,
|
||||
MatListModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {} }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddClientsToPxeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
import {Component, Inject} from '@angular/core';
|
||||
import {Observable, startWith} from "rxjs";
|
||||
import {FormControl} from "@angular/forms";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {map} from "rxjs/operators";
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-clients-to-pxe',
|
||||
templateUrl: './add-clients-to-pxe.component.html',
|
||||
styleUrl: './add-clients-to-pxe.component.css'
|
||||
})
|
||||
export class AddClientsToPxeComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
clients: any[] = [];
|
||||
selectedClients: any[] = [];
|
||||
loading: boolean = true;
|
||||
filters: { [key: string]: string } = {};
|
||||
filteredClients!: Observable<any[]>;
|
||||
clientControl = new FormControl();
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<AddClientsToPxeComponent>,
|
||||
private toastService: ToastrService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { subnetUuid: string, subnetName: string }
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
|
||||
this.loadClients();
|
||||
|
||||
this.filteredClients = this.clientControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map(value => (typeof value === 'string' ? value : value?.name)),
|
||||
map(name => (name ? this._filterClients(name) : this.clients.slice()))
|
||||
);
|
||||
}
|
||||
|
||||
loadClients() {
|
||||
this.http.get<any>( `${this.baseUrl}/clients?&page=1&itemsPerPage=10000&exists[template]=false`).subscribe(
|
||||
response => {
|
||||
this.clients = response['hydra:member'];
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching parent units:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
save() {
|
||||
const postData = {
|
||||
clients: this.selectedClients.map(client => client['@id'])
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/pxe-templates/${this.data.subnetUuid}/add-clients`, postData).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Clientes asignados correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
);
|
||||
|
||||
this.dialogRef.close(this.selectedClients);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
removeClient(client: any) {
|
||||
const index = this.selectedClients.indexOf(client);
|
||||
if (index >= 0) {
|
||||
this.selectedClients.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private _filterClients(name: string): any[] {
|
||||
const filterValue = name.toLowerCase();
|
||||
return this.clients.filter(client => client.name.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
displayFnClient(client: any): string {
|
||||
return client && client.name ? client.name : '';
|
||||
}
|
||||
|
||||
onOptionClientSelected(client: any) {
|
||||
if (!this.selectedClients.includes(client)) {
|
||||
this.selectedClients.push(client);
|
||||
}
|
||||
this.clientControl.setValue('');
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
|
||||
mat-dialog-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.green-icon {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.red-icon {
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
||||
.spacing-container {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
display: flex;
|
||||
align-items: flex-start; /* Alinea el contenido al inicio */
|
||||
justify-content: space-between; /* Espacio entre los textos y los íconos */
|
||||
width: 100%; /* Asegúrate de que el contenido ocupe todo el ancho */
|
||||
}
|
||||
|
||||
.text-content {
|
||||
flex-grow: 1;
|
||||
margin-right: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right-icon {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
<h2 mat-dialog-title>{{ 'manageClientsTitle' | translate }}</h2>
|
||||
<mat-dialog-content>
|
||||
<mat-list>
|
||||
<ng-container *ngFor="let client of clients">
|
||||
<mat-list-item>
|
||||
<div class="list-item-content">
|
||||
<mat-icon matListItemIcon [ngClass]="{'red-icon': client.pxeSync === false || !client.pxeSync, 'green-icon': client.pxeSync === true}">
|
||||
computer
|
||||
</mat-icon>
|
||||
<div class="text-content">
|
||||
<div matListItemTitle>{{ client.name }}</div>
|
||||
<div matListItemLine>{{ client.mac }}</div>
|
||||
</div>
|
||||
<div class="icon-container">
|
||||
<button mat-icon-button color="info" (click)="showInfo(client)">
|
||||
<mat-icon>{{ 'viewIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="addClientToTemplate(client)">
|
||||
<mat-icon>{{ 'syncIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" class="right-icon" (click)="deleteClient(client)">
|
||||
<mat-icon>{{ 'deleteIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</ng-container>
|
||||
</mat-list>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button type="button" (click)="onCancel()">{{ 'cancelButton' | translate }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -1,70 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ClientsComponent } from './clients.component';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatProgressSpinner, MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('ClientsComponent', () => {
|
||||
let component: ClientsComponent;
|
||||
let fixture: ComponentFixture<ClientsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ClientsComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatTabsModule,
|
||||
MatAutocompleteModule,
|
||||
MatListModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {data: {id: 123}} }
|
||||
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ClientsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,107 +0,0 @@
|
|||
import {Component, Inject} from '@angular/core';
|
||||
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {Observable} from "rxjs";
|
||||
import {
|
||||
ServerInfoDialogComponent
|
||||
} from "../../../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-clients',
|
||||
templateUrl: './clients.component.html',
|
||||
styleUrl: './clients.component.css'
|
||||
})
|
||||
export class ClientsComponent {
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
templateForm!: FormGroup;
|
||||
clients: any[] = [];
|
||||
alertMessage: string | null = null;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ClientsComponent>,
|
||||
public dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.getPxeClients()
|
||||
}
|
||||
|
||||
getPxeClients(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/clients?template.id=${this.data.data.id}`).subscribe({
|
||||
next: data => {
|
||||
this.clients = data['hydra:member']
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error al obtener los clientes PXE:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addClientToTemplate(client: any): void {
|
||||
const postData = {
|
||||
client: client['@id']
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/pxe-templates/${this.data.data.uuid}/sync-client`, postData).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Clientes asignados correctamente');
|
||||
this.getPxeClients()
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadAlert(client: any): Observable<any> {
|
||||
return this.http.post<any>(`${this.baseUrl}/clients/server/${client.uuid}/get-pxe`, {});
|
||||
}
|
||||
|
||||
showInfo(client:any) {
|
||||
this.loadAlert(client).subscribe(
|
||||
response => {
|
||||
this.alertMessage = response.message;
|
||||
|
||||
this.dialog.open(ServerInfoDialogComponent, {
|
||||
width: '600px',
|
||||
data: {
|
||||
message: this.alertMessage
|
||||
}
|
||||
});
|
||||
},
|
||||
error => {
|
||||
console.error('Error al cargar la información del alert', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteClient(client: any): void {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: client.name }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.http.post(`${this.baseUrl}/pxe-templates/${this.data.data.uuid}/delete-client`, { client: client['@id'] }).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Cliente eliminado exitosamente');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}})
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ set ISODIR ogLive
|
|||
kernel http://__SERVERIP__/tftpboot/\${ISODIR}/ogvmlinuz \${kernelargs}
|
||||
initrd http://__SERVERIP__/tftpboot/\${ISODIR}/oginitrd.img
|
||||
boot`,
|
||||
|
||||
|
||||
disco: `#!ipxe
|
||||
|
||||
iseq \${platform} efi && goto uefi_boot || goto bios_boot
|
||||
|
@ -104,8 +104,7 @@ exit`
|
|||
this.dialogRef.close(true);
|
||||
},
|
||||
error: error => {
|
||||
this.toastService.error('Error al crear la plantilla PXE');
|
||||
this.dialogRef.close(false);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ table {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
@ -41,27 +42,21 @@ table {
|
|||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.template-button-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-top: 16px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.example-headers-align .mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.example-headers-align .mat-mdc-form-field + .mat-mdc-form-field {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.example-button-row {
|
||||
display: table-cell;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.example-button-row .mat-mdc-button-base {
|
||||
margin: 8px 8px 8px 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,10 @@
|
|||
<mat-accordion class="example-headers-align">
|
||||
<mat-expansion-panel hideToggle>
|
||||
<mat-expansion-panel-header joyrideStep="serverInfoStep" text="{{ 'serverInfoDescription' | translate }}">
|
||||
<mat-panel-title>{{ 'serverInfoTitle' | translate }}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="example-button-row">
|
||||
<button mat-flat-button color="primary" (click)="syncTemplates()">{{ 'syncDatabaseButton' | translate }}</button>
|
||||
</div>
|
||||
<div class="example-button-row">
|
||||
<button mat-flat-button color="accent" (click)="openSubnetInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
|
||||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2 class="title" joyrideStep="titleStep" text="{{ 'adminPxeDescription' | translate }}">{{ 'adminPxeTitle' | translate }}</h2>
|
||||
<div class="pxe-button-row">
|
||||
<div class="template-button-row">
|
||||
<button mat-flat-button color="accent" (click)="openInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
<button mat-flat-button color="primary" (click)="addPxeTemplate()" joyrideStep="addTemplateStep" text="{{ 'addTemplateButtonDescription' | translate }}">{{ 'addTemplateButton' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,7 +28,8 @@
|
|||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableDescription' | translate }}">
|
||||
<mat-spinner *ngIf="loading"></mat-spinner>
|
||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableDescription' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
|
@ -57,26 +45,17 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'actionsColumn' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let template">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actionsColumn' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let template" style="text-align: center;">
|
||||
<button mat-icon-button color="info" (click)="showTemplate($event, template)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="info" [disabled]="template.clientsLength === 0" (click)="editClients($event, template)">
|
||||
<mat-icon>computer</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="editPxeTemplate(template)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu">
|
||||
<mat-icon>menu</mat-icon>
|
||||
<button mat-icon-button color="warn" (click)="toggleAction(template, 'delete')">
|
||||
<mat-icon>{{ 'deleteIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="toggleAction(template, 'create')">{{ 'createServerButton' | translate }}</button>
|
||||
<button mat-menu-item (click)="addClientsToPxe(template)">{{ 'addClientButton' | translate }}</button>
|
||||
<button mat-menu-item (click)="toggleAction(template, 'sync')">{{ 'syncDatabaseButton' | translate }}</button>
|
||||
<button mat-menu-item (click)="toggleAction(template, 'delete')">{{ 'deleteButton' | translate }}</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -6,21 +6,12 @@ import { MatTableDataSource } from '@angular/material/table';
|
|||
import { PageEvent } from '@angular/material/paginator';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { DataService } from './data.service';
|
||||
import {
|
||||
ShowOrganizationalUnitComponent
|
||||
} from "../../groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component";
|
||||
import {ShowTemplateContentComponent} from "./show-template-content/show-template-content.component";
|
||||
import {ServerInfoDialogComponent} from "../../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component";
|
||||
import {
|
||||
AddClientsToSubnetComponent
|
||||
} from "../../ogdhcp/og-dhcp-subnets/add-clients-to-subnet/add-clients-to-subnet.component";
|
||||
import {Subnet} from "../../ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component";
|
||||
import {AddClientsToPxeComponent} from "./add-clients-to-pxe/add-clients-to-pxe.component";
|
||||
import {Observable} from "rxjs";
|
||||
import {ClientsComponent} from "./clients/clients.component";
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-pxe',
|
||||
|
@ -78,19 +69,20 @@ export class PxeComponent {
|
|||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
this.search();
|
||||
this.loadAlert()
|
||||
this.syncTemplates()
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
search(): void {
|
||||
this.loading = true;
|
||||
this.dataService.getPxeTemplates(this.filters).subscribe(
|
||||
data => {
|
||||
this.dataSource.data = data;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching pxe templates', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -105,6 +97,7 @@ export class PxeComponent {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
editPxeTemplate(template: any) {
|
||||
const dialogRef = this.dialog.open(CreatePxeTemplateComponent, {
|
||||
data: template, // Pasa los datos del template para edición
|
||||
|
@ -118,40 +111,26 @@ export class PxeComponent {
|
|||
|
||||
toggleAction(image: any, action: string): void {
|
||||
switch (action) {
|
||||
case 'create':
|
||||
this.http.post(`${this.apiUrl}/server/${image.uuid}/post`, {}).subscribe({
|
||||
next: (response) => {
|
||||
this.search();
|
||||
// @ts-ignore
|
||||
this.toastService.success(response.success);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error.error);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'sync':
|
||||
this.http.get(`${this.apiUrl}/server/${image.uuid}/get`, {}).subscribe({
|
||||
next: (response) => {
|
||||
this.search();
|
||||
// @ts-ignore
|
||||
this.toastService.success(response.success);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error.error);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'delete':
|
||||
this.http.post(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Plantilla eliminada correctamente');
|
||||
this.search();
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error.error);
|
||||
}
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: image.name }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.http.post(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Plantilla eliminada exitosamente');
|
||||
this.search();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al eliminar la subred', error);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
break;
|
||||
default:
|
||||
console.error('Acción no soportada:', action);
|
||||
|
@ -164,11 +143,6 @@ export class PxeComponent {
|
|||
const dialogRef = this.dialog.open(ShowTemplateContentComponent, { data: { data }, width: '700px'});
|
||||
}
|
||||
|
||||
editClients(event: MouseEvent, data: any): void {
|
||||
event.stopPropagation();
|
||||
const dialogRef = this.dialog.open(ClientsComponent, { data: { data }, width: '700px'});
|
||||
}
|
||||
|
||||
syncTemplates() {
|
||||
this.http.post(`${this.apiUrl}/sync`, {})
|
||||
.subscribe(response => {
|
||||
|
@ -180,17 +154,6 @@ export class PxeComponent {
|
|||
});
|
||||
}
|
||||
|
||||
addClientsToPxe(template: Subnet) {
|
||||
const dialogRef = this.dialog.open(AddClientsToPxeComponent, {
|
||||
width: '600px',
|
||||
data: { subnetUuid: template.uuid, subnetName: template.name }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
this.search();
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter() {
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({
|
||||
next: (response) => {
|
||||
|
@ -207,7 +170,7 @@ export class PxeComponent {
|
|||
return this.http.get<any>(`${this.apiUrl}/server/get-collection`);
|
||||
}
|
||||
|
||||
openSubnetInfoDialog() {
|
||||
openInfoDialog() {
|
||||
this.loadAlert().subscribe(
|
||||
response => {
|
||||
this.alertMessage = response.message;
|
||||
|
@ -248,5 +211,5 @@ export class PxeComponent {
|
|||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@
|
|||
<mat-label>Boot File Name</mat-label>
|
||||
<input matInput [(ngModel)]="bootFileName" placeholder="Boot File Name">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Router</mat-label>
|
||||
<input matInput [(ngModel)]="router" placeholder="Router">
|
||||
</mat-form-field>
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
@ -60,5 +64,5 @@
|
|||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onNoClick()">Cancelar</button>
|
||||
<button mat-button (click)="addNetworkConfig()" cdkFocusInitial>Guardar</button>
|
||||
<button mat-button (click)="save()" cdkFocusInitial>Guardar</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -17,6 +17,7 @@ export class CreateSubnetComponent implements OnInit {
|
|||
ipAddress: string = '';
|
||||
nextServer: string = '';
|
||||
bootFileName: string = '';
|
||||
router: string = '';
|
||||
syncronized: boolean = false;
|
||||
serverId: number = 0;
|
||||
clients: any[] = [];
|
||||
|
@ -44,6 +45,7 @@ export class CreateSubnetComponent implements OnInit {
|
|||
this.ipAddress = this.data.ipAddress;
|
||||
this.nextServer = this.data.nextServer;
|
||||
this.bootFileName = this.data.bootFileName;
|
||||
this.router = this.data.router;
|
||||
this.syncronized = this.data.syncronized;
|
||||
this.serverId = this.data.serverId;
|
||||
this.clients = this.data.clients
|
||||
|
@ -53,13 +55,14 @@ export class CreateSubnetComponent implements OnInit {
|
|||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
addNetworkConfig(): void {
|
||||
save(): void {
|
||||
const payload = {
|
||||
name: this.name,
|
||||
netmask: this.netmask,
|
||||
ipAddress: this.ipAddress,
|
||||
nextServer: this.nextServer || null,
|
||||
bootFileName: this.bootFileName || null
|
||||
router: this.router || null,
|
||||
nextServer: this.nextServer || null,
|
||||
bootFileName: this.bootFileName || null
|
||||
};
|
||||
|
||||
if (!this.data){
|
||||
|
|
|
@ -38,7 +38,6 @@
|
|||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
<mat-spinner *ngIf="loading"></mat-spinner>
|
||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="Visualiza y administra las subredes listadas según los filtros aplicados.">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
|
|
|
@ -18,6 +18,7 @@ export interface Subnet {
|
|||
netmask: string;
|
||||
ipAddress: string;
|
||||
nextServer: string;
|
||||
router: string;
|
||||
bootFileName: string;
|
||||
synchronized: boolean;
|
||||
serverId: number;
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
export const FILESYSTEM_TYPES = [
|
||||
{ id: 1, name: 'EMPTY', description: 'EMPTY', active: 0 },
|
||||
{ id: 2, name: 'CACHE', description: 'CACHE', active: 0 },
|
||||
{ id: 3, name: 'BTRFS', description: 'BTRFS', active: 0 },
|
||||
{ id: 4, name: 'EXT2', description: 'EXT2', active: 0 },
|
||||
{ id: 5, name: 'EXT3', description: 'EXT3', active: 0 },
|
||||
{ id: 6, name: 'EXT4', description: 'EXT4', active: 0 },
|
||||
{ id: 7, name: 'FAT12', description: 'FAT12', active: 0 },
|
||||
{ id: 8, name: 'FAT16', description: 'FAT16', active: 0 },
|
||||
{ id: 9, name: 'FAT32', description: 'FAT32', active: 0 },
|
||||
{ id: 10, name: 'HFS', description: 'HFS', active: 0 },
|
||||
{ id: 11, name: 'HFSPLUS', description: 'HFSPLUS', active: 0 },
|
||||
{ id: 12, name: 'JFS', description: 'JFS', active: 0 },
|
||||
{ id: 13, name: 'NTFS', description: 'NTFS', active: 0 },
|
||||
{ id: 14, name: 'REISERFS', description: 'REISERFS', active: 0 },
|
||||
{ id: 15, name: 'REISER4', description: 'REISER4', active: 0 },
|
||||
{ id: 16, name: 'UFS', description: 'UFS', active: 0 },
|
||||
{ id: 17, name: 'XFS', description: 'XFS', active: 0 },
|
||||
{ id: 18, name: 'EXFAT', description: 'EXFAT', active: 0 },
|
||||
{ id: 19, name: 'LINUX-SWAP', description: 'LINUX-SWAP', active: 0 },
|
||||
{ id: 20, name: 'F2FS', description: 'F2FS', active: 0 },
|
||||
{ id: 21, name: 'NILFS2', description: 'NILFS2', active: 0 },
|
||||
{ id: 22, name: '0', description: '', active: 0 },
|
||||
{ id: 23, name: 'LINUX-LVM', description: '', active: 0 },
|
||||
{ id: 24, name: 'ISO9660', description: '', active: 0 },
|
||||
];
|
|
@ -0,0 +1,72 @@
|
|||
export const PARTITION_TYPES = [
|
||||
{ code: 0, name: 'EMPTY', active: false },
|
||||
{ code: 1, name: 'FAT12', active: true },
|
||||
{ code: 5, name: 'EXTENDED', active: false },
|
||||
{ code: 6, name: 'FAT16', active: true },
|
||||
{ code: 7, name: 'NTFS', active: true },
|
||||
{ code: 11, name: 'FAT32', active: true },
|
||||
{ code: 17, name: 'HFAT12', active: true },
|
||||
{ code: 22, name: 'HFAT16', active: true },
|
||||
{ code: 23, name: 'HNTFS', active: true },
|
||||
{ code: 27, name: 'HFAT32', active: true },
|
||||
{ code: 130, name: 'LINUX-SWAP', active: false },
|
||||
{ code: 131, name: 'LINUX', active: true },
|
||||
{ code: 142, name: 'LINUX-LVM', active: true },
|
||||
{ code: 165, name: 'FREEBSD', active: true },
|
||||
{ code: 166, name: 'OPENBSD', active: true },
|
||||
{ code: 169, name: 'NETBSD', active: true },
|
||||
{ code: 175, name: 'HFS', active: true },
|
||||
{ code: 190, name: 'SOLARIS-BOOT', active: true },
|
||||
{ code: 191, name: 'SOLARIS', active: true },
|
||||
{ code: 202, name: 'CACHE', active: false },
|
||||
{ code: 218, name: 'DATA', active: true },
|
||||
{ code: 238, name: 'GPT', active: false },
|
||||
{ code: 239, name: 'EFI', active: true },
|
||||
{ code: 251, name: 'VMFS', active: true },
|
||||
{ code: 253, name: 'LINUX-RAID', active: true },
|
||||
{ code: 1792, name: 'WINDOWS', active: true },
|
||||
{ code: 3073, name: 'WIN-RESERV', active: true },
|
||||
{ code: 9984, name: 'WIN-RECOV', active: true },
|
||||
{ code: 32512, name: 'CHROMEOS-KRN', active: true },
|
||||
{ code: 32513, name: 'CHROMEOS', active: true },
|
||||
{ code: 32514, name: 'CHROMEOS-RESERV', active: true },
|
||||
{ code: 33280, name: 'LINUX-SWAP', active: false },
|
||||
{ code: 33536, name: 'LINUX', active: true },
|
||||
{ code: 33537, name: 'LINUX-RESERV', active: true },
|
||||
{ code: 33538, name: 'LINUX', active: true },
|
||||
{ code: 36352, name: 'LINUX-LVM', active: true },
|
||||
{ code: 42240, name: 'FREEBSD-DISK', active: false },
|
||||
{ code: 42241, name: 'FREEBSD-BOOT', active: true },
|
||||
{ code: 42242, name: 'FREEBSD-SWAP', active: false },
|
||||
{ code: 42243, name: 'FREEBSD', active: true },
|
||||
{ code: 42244, name: 'FREEBSD', active: true },
|
||||
{ code: 43265, name: 'NETBSD-SWAP', active: false },
|
||||
{ code: 43266, name: 'NETBSD', active: true },
|
||||
{ code: 43267, name: 'NETBSD', active: true },
|
||||
{ code: 43268, name: 'NETBSD', active: true },
|
||||
{ code: 43269, name: 'NETBSD', active: true },
|
||||
{ code: 43270, name: 'NETBSD-RAID', active: true },
|
||||
{ code: 43776, name: 'HFS-BOOT', active: true },
|
||||
{ code: 44800, name: 'HFS', active: true },
|
||||
{ code: 44801, name: 'HFS-RAID', active: true },
|
||||
{ code: 44802, name: 'HFS-RAID', active: true },
|
||||
{ code: 48640, name: 'SOLARIS-BOOT', active: true },
|
||||
{ code: 48896, name: 'SOLARIS', active: true },
|
||||
{ code: 48897, name: 'SOLARIS', active: true },
|
||||
{ code: 48898, name: 'SOLARIS-SWAP', active: false },
|
||||
{ code: 48899, name: 'SOLARIS-DISK', active: true },
|
||||
{ code: 48900, name: 'SOLARIS', active: true },
|
||||
{ code: 48901, name: 'SOLARIS', active: true },
|
||||
{ code: 51712, name: 'CACHE', active: false },
|
||||
{ code: 61184, name: 'EFI', active: true },
|
||||
{ code: 61185, name: 'MBR', active: false },
|
||||
{ code: 61186, name: 'BIOS-BOOT', active: false },
|
||||
{ code: 64256, name: 'VMFS', active: true },
|
||||
{ code: 64257, name: 'VMFS-RESERV', active: true },
|
||||
{ code: 64258, name: 'VMFS-KRN', active: true },
|
||||
{ code: 64768, name: 'LINUX-RAID', active: true },
|
||||
{ code: 65535, name: 'UNKNOWN', active: true },
|
||||
{ code: 65536, name: 'LVM-LV', active: true },
|
||||
{ code: 65552, name: 'ZFS-VOL', active: true },
|
||||
{ code: 39, name: 'HNTFS-WINRE', active: true },
|
||||
];
|
|
@ -6,7 +6,7 @@
|
|||
"loginError": "Login error: {{error}}",
|
||||
"labelUsers": "Users",
|
||||
"labelRoles": "Roles",
|
||||
"adminImagesTitle": "Manage clients",
|
||||
"adminImagesTitle": "Manage images",
|
||||
"addUser": "Add users",
|
||||
"searchLabel": "Search image name",
|
||||
"searchPlaceholder": "Search",
|
||||
|
@ -399,6 +399,7 @@
|
|||
"ogLive": "ogLive",
|
||||
"TOOLTIP_PXE_IMAGES": "View available PXE boot images",
|
||||
"pxeTemplates": "PXE Templates",
|
||||
"pxeTemplate" : "Plantilla",
|
||||
"TOOLTIP_PXE_TEMPLATES": "Manage PXE boot templates",
|
||||
"pxeBootFiles": "PXE Boot Files",
|
||||
"TOOLTIP_PXE_BOOT_FILES": "Configure PXE boot files",
|
||||
|
@ -419,5 +420,30 @@
|
|||
"menus": "Menus",
|
||||
"TOOLTIP_MENUS": "Menu management (option disabled)",
|
||||
"search": "Search",
|
||||
"TOOLTIP_SEARCH": "Search function (option disabled)"
|
||||
"TOOLTIP_SEARCH": "Search function (option disabled)",
|
||||
"detailsOf": "Details of",
|
||||
"editUnitMenu": "Edit",
|
||||
"addInternalUnitMenu": "Add",
|
||||
"addClientMenu": "Add client",
|
||||
"filters": "Filters",
|
||||
"searchClient": "Search client",
|
||||
"searchTree": "Search in tree",
|
||||
"filterByType": "Filter by type",
|
||||
"all": "All",
|
||||
"classroomsGroup": "Classroom groups",
|
||||
"classrooms": "Classrooms",
|
||||
"computerGroups": "PC groups",
|
||||
"executeCommand": "Execute command",
|
||||
"roomMap": "Classroom map",
|
||||
"addOrganizationalUnit": "Add organizational unit",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"clients": "Clients",
|
||||
"sync": "Sync",
|
||||
"viewDetails": "View details",
|
||||
"name": "Name",
|
||||
"maintenance": "Maintenance",
|
||||
"subnet": "Subnet",
|
||||
"parent": "Parent",
|
||||
"adminUsersTitle": "Manage users"
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"loginError": "Login error: {{error}}",
|
||||
"labelUsers": "Usuarios",
|
||||
"labelRoles": "Roles",
|
||||
"adminImagesTitle": "Administrar clientes",
|
||||
"adminImagesTitle": "Administrar OgLives",
|
||||
"addUser": "Añadir usuarios",
|
||||
"searchLabel": "Buscar nombre de imagen",
|
||||
"searchPlaceholder": "Búsqueda",
|
||||
|
@ -233,6 +233,7 @@
|
|||
"editClientDialogTitle": "Editar Cliente",
|
||||
"organizationalUnitLabel": "Padre",
|
||||
"ogLiveLabel": "OgLive",
|
||||
"repositoryLabel": "Repositorio",
|
||||
"serialNumberLabel": "Número de Serie",
|
||||
"netifaceLabel": "Interfaz de red",
|
||||
"netDriverLabel": "Controlador de red",
|
||||
|
@ -400,6 +401,7 @@
|
|||
"ogLive": "ogLive",
|
||||
"TOOLTIP_PXE_IMAGES": "Ver imágenes disponibles para arranque PXE",
|
||||
"pxeTemplates": "Plantillas PXE",
|
||||
"pxeTemplate": "Plantilla",
|
||||
"TOOLTIP_PXE_TEMPLATES": "Gestionar plantillas de arranque PXE",
|
||||
"pxeBootFiles": "Arranque PXE",
|
||||
"TOOLTIP_PXE_BOOT_FILES": "Configurar archivos de arranque PXE",
|
||||
|
@ -420,5 +422,30 @@
|
|||
"menus": "Menús",
|
||||
"TOOLTIP_MENUS": "Gestión de menús (opción deshabilitada)",
|
||||
"search": "Buscar",
|
||||
"TOOLTIP_SEARCH": "Función de búsqueda (opción deshabilitada)"
|
||||
"TOOLTIP_SEARCH": "Función de búsqueda (opción deshabilitada)",
|
||||
"detailsOf": "Detalles de",
|
||||
"editUnitMenu": "Editar",
|
||||
"addInternalUnitMenu": "Añadir",
|
||||
"addClientMenu": "Añadir cliente",
|
||||
"filters": "Filtros",
|
||||
"searchClient": "Buscar cliente",
|
||||
"searchTree": "Buscar en árbol",
|
||||
"filterByType": "Filtrar por tipo",
|
||||
"all": "Todos",
|
||||
"classroomsGroup": "Grupos de aulas",
|
||||
"classrooms": "Aulas",
|
||||
"computerGroups": "Grupos de PCs",
|
||||
"executeCommand": "Ejecutar comando",
|
||||
"roomMap": "Plano de aula",
|
||||
"addOrganizationalUnit": "Añadir unidad organizativa",
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
"clients": "Clientes",
|
||||
"sync": "Sincronizar",
|
||||
"viewDetails": "Ver detalles",
|
||||
"name": "Nombre",
|
||||
"maintenance": "Mantenimiento",
|
||||
"subnet": "Subred",
|
||||
"parent": "Padre",
|
||||
"adminUsersTitle": "Administrar usuarios"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue