Updated remote PC and bug status classrom
testing/ogGui-multibranch/pipeline/pr-main Build queued... Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details

pull/76/head
Manuel Aranda Rosales 2026-04-27 11:47:57 +02:00
commit 1978cf41e7
258 changed files with 10252 additions and 4337 deletions

View File

@ -1,5 +1,28 @@
# Changelog
## [1.5.4] - 2026-04-24
### Improved
- Modificado script de post instalación debian para añadir a config.json las nuevas variables.
## [1.5.3] - 2026-04-24
### Improved
- Corregidas traducciones
## [1.5.2] - 2026-04-24
### Added
- Añadido campo resolución en los ajustes de red de aulas y grupos para propagarlo a los equipos.
- Rediseño del menú de configuracion de una Unidad Organizativa
## [1.5.1] - 2026-04-23
### Improved
- Creadas claves de traducción para idiomas, añadido menu para cambiar de idioma en la barra superior
## [1.5.0] - 2026-04-21
### Added
- Añadida gestión de múltiples ogBoot Servers
- Si es un ogBoot existente, permite sincronizar su información sobre oglives y plantillas pxe.
- En los Ajustes de red de equipos, aulas y grupos, ahora se debe seleccionar servidor ogBoot y luego oglive y plantilla dentro de dicho servidor.
## [1.4.0] - 2026-04-16
### Added
- Añadida gestión de múltiples Dhcps

18
debian/oggui.postinst vendored
View File

@ -22,11 +22,19 @@ restore_config_if_modified() {
if [ -f "$backup" ]; then
if ! cmp -s "$new" "$backup"; then
echo ">>> Archivo modificado por el usuario detectado en $new"
echo " - Guardando archivo nuevo como ${new}.new"
mv -f "$new" "${new}.new"
echo " - Restaurando archivo anterior desde backup"
mv -f "$backup" "$new"
if [ "$new" = "$CONFIG_FILE" ]; then
echo ">>> Fusionando configuración de usuario en $new"
# El operador '*' realiza un merge recursivo:
# Preserva los valores del usuario (.[1]) y añade solo las claves nuevas del paquete (.[0])
jq -s '.[0] * .[1]' "$new" "$backup" > "${new}.tmp" && mv "${new}.tmp" "$new"
rm -f "$backup"
else
echo ">>> Archivo modificado por el usuario detectado en $new"
echo " - Guardando archivo nuevo como ${new}.new"
mv -f "$new" "${new}.new"
echo " - Restaurando archivo anterior desde backup"
mv -f "$backup" "$new"
fi
else
echo ">>> El archivo $new no ha cambiado desde la última versión, eliminando backup"
rm -f "$backup"

View File

@ -36,6 +36,7 @@ import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component
import { MenusComponent } from "./components/menus/menus.component";
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component';
import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component';
import {
RunScriptAssistantComponent
@ -45,6 +46,7 @@ import { LogoutGuard } from './guards/logout.guard';
import { HardwareComponent } from './components/hardware/hardware.component';
import { HardwareProfileComponent } from './components/hardware-profile/hardware-profile.component';
import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component';
import { BootServerDashboardComponent } from './components/ogboot/boot-server-dashboard/boot-server-dashboard.component';
const routes: Routes = [
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
{
@ -60,6 +62,8 @@ const routes: Routes = [
{ path: 'ogboot-status', component: OgbootStatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'subnets', component: OgDhcpSubnetsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'dhcp-servers', component: DhcpServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'boot-servers', component: BootServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'boot-servers/:uuid', component: BootServerDashboardComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },

View File

@ -13,6 +13,6 @@ export class AppComponent {
this.translateService.use(savedLanguage);
}
ngOnInit() {
sessionStorage.setItem('language', 'es');
// Language is already handled in constructor
}
}

View File

@ -58,10 +58,10 @@ import { MatProgressSpinner } from "@angular/material/progress-spinner";
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu";
import { MatAutocomplete, MatAutocompleteTrigger } from "@angular/material/autocomplete";
import { MatChip, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule } from "@angular/material/chips";
import { MatChipsModule } from "@angular/material/chips";
import { ClientViewComponent } from './components/groups/shared/client-view/client-view.component';
import { MatTab, MatTabGroup } from "@angular/material/tabs";
import { MatTooltip } from "@angular/material/tooltip";
import { MatTabsModule } from "@angular/material/tabs";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatExpansionModule } from '@angular/material/expansion';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { ToastrModule } from 'ngx-toastr';
@ -193,10 +193,19 @@ import { HardwareCollectionModalComponent } from './components/hardware-profile/
import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component';
import { ShowDetailsComponent } from './components/hardware-type/show-details/show-details.component';
import { DiskPartitionSelectorComponent } from './components/groups/shared/disk-partition-selector/disk-partition-selector.component';
import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component';
import { ManageBootServerComponent } from './components/ogboot/manage-boot-server/manage-boot-server.component';
import { BootServerDashboardComponent } from './components/ogboot/boot-server-dashboard/boot-server-dashboard.component';
import { OgcoreStatusComponent } from './components/global-status/ogcore-status/ogcore-status.component';
import { GlobalOgbootStatusComponent } from './components/global-status/ogboot-status/ogboot-status.component';
import { OgrepositoryStatusComponent } from './components/global-status/ogrepository-status/ogrepository-status.component';
import { OgdhcpStatusComponent } from './components/global-status/ogdhcp-status/ogdhcp-status.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
}
import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component';
export function initializeApp(configService: ConfigService) {
return () => configService.loadConfig();
}
@ -324,7 +333,15 @@ registerLocaleData(localeEs, 'es-ES');
HardwareCollectionModalComponent,
HardwareTypeComponent,
ShowDetailsComponent,
AssetManagerModalComponent
AssetManagerModalComponent,
BootServersComponent,
ManageBootServerComponent,
BootServerDashboardComponent,
OgcoreStatusComponent,
GlobalOgbootStatusComponent,
OgrepositoryStatusComponent,
OgdhcpStatusComponent,
PageNotFoundComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,
@ -349,7 +366,7 @@ registerLocaleData(localeEs, 'es-ES');
MatProgressBarModule,
MatStepperModule,
DragDropModule,
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule, MatChip, MatProgressSpinner, MatTabGroup, MatTab, MatTooltip,
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipsModule, MatProgressSpinner, MatTabsModule, MatTooltipModule,
MatExpansionModule,
NgxChartsModule,
MatDatepickerModule,

View File

@ -1,20 +1,20 @@
<div class="env-settings">
<div class="header-container">
<div class="header-container-title">
<h2>Editar Variables de Entorno</h2>
<h2>{{ 'editEnvVarsTitle' | translate }}</h2>
</div>
</div>
<mat-table [dataSource]="envVars" class="mat-elevation-z8">
<!-- Nombre de la variable -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef> Variable </mat-header-cell>
<mat-header-cell *matHeaderCellDef> {{ 'labelVariable' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let variable">{{ variable.name }}</mat-cell>
</ng-container>
<!-- Valor de la variable -->
<ng-container matColumnDef="value">
<mat-header-cell *matHeaderCellDef> Valor </mat-header-cell>
<mat-header-cell *matHeaderCellDef> {{ 'labelValue' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let variable">
<!-- Si es booleano, usamos checkbox -->
<mat-checkbox *ngIf="isBoolean(variable.value)"
@ -35,7 +35,7 @@
</mat-table>
<div class="actions">
<button class="action-button" (click)="loadEnvVars()">Recargar</button>
<button class="submit-button" (click)="saveEnvVars()">Guardar Cambios</button>
<button class="action-button" (click)="loadEnvVars()">{{ 'buttonReload' | translate }}</button>
<button class="submit-button" (click)="saveEnvVars()">{{ 'buttonSaveChanges' | translate }}</button>
</div>
</div>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { ToastrService } from "ngx-toastr";
@ -9,6 +11,7 @@ import { ConfigService } from '@services/config.service';
styleUrl: './env-vars.component.css'
})
export class EnvVarsComponent {
public translate = inject(TranslateService);
envVars: { name: string; value: string }[] = [];
displayedColumns: string[] = ['name', 'value'];
private apiUrl: string;
@ -31,7 +34,7 @@ export class EnvVarsComponent {
this.envVars = Object.entries(response.vars).map(([name, value]) => ({ name, value }));
},
error: (err) => {
this.toastService.error('No se pudieron cargar las variables de entorno.');
this.toastService.error(this.translate.instant('couldNotBeLoaded'));
}
});
}
@ -48,11 +51,11 @@ export class EnvVarsComponent {
this.http.post(this.apiUrl, { vars }).subscribe({
next: () => {
this.toastService.success('Variables de entorno guardadas correctamente.');
this.toastService.success(this.translate.instant('environmentVariablesSavedCorrectly'));
},
error: (err) => {
console.error('Error al guardar las variables de entorno:', err);
this.toastService.error('No se pudieron cargar las variables de entorno.');
console.error(this.translate.instant('errorSavingVariables'), err);
this.toastService.error(this.translate.instant('couldNotBeLoaded'));
}
});
}

View File

@ -12,7 +12,7 @@
<p><mat-checkbox formControlName="orgAdmin">{{ 'checkboxOrgAdmin' | translate }}</mat-checkbox></p>
<p><mat-checkbox formControlName="orgOperator">{{ 'checkboxOrgOperator' | translate }}</mat-checkbox></p>
<p><mat-checkbox formControlName="orgMinimal">{{ 'checkboxOrgMinimal' | translate }}</mat-checkbox></p>
<p><mat-checkbox formControlName="userRole">{{ 'checkboxUserRole' | translate }}</mat-checkbox></p>
<p><mat-checkbox formControlName="userRole">{{ 'labelUser' | translate }}</mat-checkbox></p>
</section>
</form>
</mat-dialog-content>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
@ -12,6 +14,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./add-role-modal.component.css']
})
export class AddRoleModalComponent {
public translate = inject(TranslateService);
roleForm: FormGroup<any>;
roleId: string | null = null;
baseUrl: string;
@ -56,7 +59,7 @@ export class AddRoleModalComponent {
this.roleId = response['@id'];
},
error: (err) => {
console.error('Error fetching remote calendar:', err);
console.error(this.translate.instant('errorFetchingRemoteCalendar'), err);
}
});
}
@ -85,23 +88,23 @@ export class AddRoleModalComponent {
if (this.roleId) {
this.http.put(`${this.baseUrl}${this.roleId}`, payload).subscribe(
(response) => {
this.toastService.success('Rol actualizado correctamente');
this.toastService.success(this.translate.instant('roleUpdatedSuccessfully'));
this.dialogRef.close();
},
(error) => {
this.toastService.error('Error al editar el rol:', error);
console.error('Error al editar el rol', error);
this.toastService.error(this.translate.instant('errorEditingRole1'), error);
console.error(this.translate.instant('errorEditingRole'), error);
}
);
} else {
this.http.post(`${this.baseUrl}/user-groups`, payload).subscribe(
(response) => {
this.toastService.success('Rol añadido correctamente');
this.toastService.success(this.translate.instant('roleAddedSuccessfully'));
this.dialogRef.close();
},
(error) => {
this.toastService.error('Error al añadir rol:', error);
console.error('Error al añadir rol', error);
this.toastService.error(this.translate.instant('errorAddingRole1'), error);
console.error(this.translate.instant('errorAddingRole'), error);
}
);
}

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Injectable } from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
@ -8,6 +10,7 @@ import { ConfigService } from '@services/config.service';
providedIn: 'root'
})
export class DataService {
public translate = inject(TranslateService);
baseUrl: string;
private apiUrl: string;
@ -31,7 +34,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching user groups', error);
console.error(this.translate.instant('errorFetchingUserGroups'), error);
return throwError(error);
})
);
@ -47,7 +50,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching user group', error);
console.error(this.translate.instant('errorFetchingUserGroup'), error);
return throwError(error);
})
);

View File

@ -11,8 +11,8 @@
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-label>{{ 'searchRoleLabel' | translate }}</mat-label>
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
<mat-label>{{ 'labelSearchRole' | translate }}</mat-label>
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']"
(keyup.enter)="search()">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
@ -24,12 +24,12 @@
<div *ngIf="!loading">
<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>
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
<td mat-cell *matCellDef="let role"> {{ column.cell(role) }} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
<td mat-cell *matCellDef="let role" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editRole(role)">
<mat-icon>edit</mat-icon>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
@ -15,6 +17,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./roles.component.css']
})
export class RolesComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string = this.configService.apiUrl;
dataSource = new MatTableDataSource<any>();
filters: { [key: string]: string } = {};
@ -27,17 +30,17 @@ export class RolesComponent implements OnInit {
columns = [
{
columnDef: 'id',
header: 'ID',
header: 'labelId',
cell: (role: any) => `${role.id}`
},
{
columnDef: 'name',
header: 'Nombre',
header: 'labelName',
cell: (role: any) => `${role.name}`
},
{
columnDef: 'permissions',
header: 'Permisos',
header: 'permissions',
cell: (role: any) => `${role.permissions.join(', ')}`
}
];
@ -66,7 +69,7 @@ export class RolesComponent implements OnInit {
this.loading = false;
},
(error) => {
console.error('Error fetching roles', error);
console.error(this.translate.instant('errorFetchingRoles'), error);
this.loading = false;
}
);
@ -108,10 +111,10 @@ export class RolesComponent implements OnInit {
this.http.delete(apiUrl).subscribe({
next: () => {
this.search();
this.toastService.success('Role deleted successfully');
this.toastService.success(this.translate.instant('roleDeletedSuccessfully'));
},
error: (error) => {
this.toastService.error('Error deleting role:', error);
this.toastService.error(this.translate.instant('errorDeletingRole'), error);
}
});
}

View File

@ -4,11 +4,11 @@
<mat-dialog-content class="form-container">
<form [formGroup]="userForm" class="user-form">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'addUserlabelUsername' | translate }}</mat-label>
<mat-label>{{ 'labelAddUserUsername' | translate }}</mat-label>
<input matInput formControlName="username" required>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'addUserlabelPassword' | translate }}</mat-label>
<mat-label>{{ 'labelAddUserPassword' | translate }}</mat-label>
<input matInput formControlName="password" type="password" required>
</mat-form-field>
@ -31,7 +31,7 @@
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Vista tarjetas</mat-label>
<mat-label>{{ 'cardView' | translate }}</mat-label>
<mat-select formControlName="groupsView" required>
<mat-option *ngFor="let option of views" [value]="option.value">
{{ option.name }}
@ -40,9 +40,9 @@
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Clientes por página</mat-label>
<input matInput formControlName="clientsPerPage" type="number" min="1" placeholder="Por defecto (20)">
<mat-hint>Número de clientes mostrados por página en el listado</mat-hint>
<mat-label>{{ 'clientsPerPage' | translate }}</mat-label>
<input matInput formControlName="clientsPerPage" type="number" min="1" [placeholder]="'default20' | translate">
<mat-hint>{{ 'clientsPerPageHint' | translate }}</mat-hint>
</mat-form-field>
</form>
</mat-dialog-content>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@ -18,6 +20,7 @@ interface UserGroup {
styleUrls: ['./add-user-modal.component.css']
})
export class AddUserModalComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
@Output() userAdded = new EventEmitter<void>();
@Output() userEdited = new EventEmitter<void>();
@ -94,7 +97,7 @@ export class AddUserModalComponent implements OnInit {
},
error: (err) => {
this.loading = false;
console.error('Error fetching user:', err);
console.error(this.translate.instant('fetchingUserError'), err);
}
});
}
@ -125,7 +128,7 @@ export class AddUserModalComponent implements OnInit {
if (this.userId) {
this.http.put(`${this.baseUrl}${this.userId}`, payload).subscribe(
(response) => {
this.toastService.success('Usuario editado correctamente');
this.toastService.success(this.translate.instant('userEditedSuccessfully'));
this.userEdited.emit();
this.dialogRef.close();
this.loading = false;
@ -138,7 +141,7 @@ export class AddUserModalComponent implements OnInit {
} else {
this.http.post(`${this.baseUrl}/users`, payload).subscribe(
(response) => {
this.toastService.success('Usuario añadido correctamente');
this.toastService.success(this.translate.instant('userAddedSuccessfully'));
this.userAdded.emit();
this.dialogRef.close();
this.loading = false;

View File

@ -1,4 +1,4 @@
<h1 mat-dialog-title>{{ 'dialogTitleChangePassword' | translate }}</h1>
<h1 mat-dialog-title>{{ 'changePassword' | translate }}</h1>
<mat-dialog-content class="form-container">
<form [formGroup]="userForm" class="user-form">
<mat-form-field class="form-field">

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, EventEmitter, Inject, Output } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
@ -10,6 +12,7 @@ import {ToastrService} from "ngx-toastr";
styleUrl: './change-password-modal.component.css'
})
export class ChangePasswordModalComponent {
public translate = inject(TranslateService);
@Output() userEdited = new EventEmitter<void>();
userForm: FormGroup;
passwordMismatch: boolean = false;
@ -55,10 +58,10 @@ export class ChangePasswordModalComponent {
next: () => {
this.userEdited.emit();
this.dialogRef.close();
this.toastService.success('Contraseña cambiada con éxito');
this.toastService.success(this.translate.instant('passwordChangedSuccessfully'));
},
error: (err) => {
console.error('Error changing password:', err);
console.error(this.translate.instant('errorChangingPassword'), err);
this.toastService.error(err.error['hydra:description']);
this.resetPasswordError = err.error.message;
this.updateError = true;
@ -66,7 +69,7 @@ export class ChangePasswordModalComponent {
}
});
} else {
console.error('Form is invalid');
console.error(this.translate.instant('formIsInvalid'));
this.passwordMismatch = this.userForm.hasError('mismatch');
this.updateError = true;
this.loading = false

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Injectable } from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
@ -9,6 +11,7 @@ import { ConfigService } from '@services/config.service';
providedIn: 'root'
})
export class DataService {
public translate = inject(TranslateService);
baseUrl: string;
private apiUrl: string;
@ -32,7 +35,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching users', error);
console.error(this.translate.instant('fetchingUsersError'), error);
return throwError(error);
})
);
@ -48,7 +51,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching user group', error);
console.error(this.translate.instant('errorFetchingUserGroup'), error);
return throwError(error);
})
);
@ -64,7 +67,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching user groups', error);
console.error(this.translate.instant('errorFetchingUserGroups'), error);
return throwError(error);
})
);
@ -80,7 +83,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching organizational units', error);
console.error(this.translate.instant('errorFetchingOrganizationalUnits'), error);
return throwError(error);
})
);

View File

@ -11,8 +11,8 @@
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
<mat-label>{{ 'labelSearch' | translate }}</mat-label>
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']"
(keyup.enter)="search()">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
@ -24,7 +24,7 @@
<div *ngIf="!loading">
<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>
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
<td mat-cell *matCellDef="let user">
<ng-container *ngIf="column.columnDef === 'groupsView'">
<mat-chip>
@ -38,7 +38,7 @@
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
<td mat-cell *matCellDef="let user" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editUser(user)">
<mat-icon>edit</mat-icon>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { MatDialog } from '@angular/material/dialog';
@ -13,6 +15,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./users.component.css']
})
export class UsersComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
private apiUrl: string;
dataSource = new MatTableDataSource<any>();
@ -25,32 +28,32 @@ export class UsersComponent implements OnInit {
columns = [
{
columnDef: 'id',
header: 'ID',
header: 'labelId',
cell: (user: any) => `${user.id}`
},
{
columnDef: 'username',
header: 'Nombre de Usuario',
header: 'userName',
cell: (user: any) => `${user.username}`
},
{
columnDef: 'groupsView',
header: 'Vista de Grupos',
header: 'groupView',
cell: (user: any) => `${user.groupsView}`
},
{
columnDef: 'clientsPerPage',
header: 'Clientes por página',
header: 'clientsPerPage',
cell: (user: any) => user.clientsPerPage != null ? `${user.clientsPerPage}` : '-'
},
{
columnDef: 'allowedOrganizationalUnits',
header: 'Unidades Organizacionales Permitidas',
header: 'allowedOrganizationalUnits',
cell: (user: any) => `${user.allowedOrganizationalUnits.map((unit: { name: any; }) => unit.name).join(', ')}`
},
{
columnDef: 'roles',
header: 'Roles',
header: 'labelRoles',
cell: (user: any) => `${user.roles.join(', ')}`
}
];
@ -79,7 +82,7 @@ export class UsersComponent implements OnInit {
this.loading = false;
},
(error) => {
console.error('Error fetching users', error);
console.error(this.translate.instant('fetchingUsersError'), error);
this.loading = false;
}
);
@ -123,11 +126,11 @@ export class UsersComponent implements OnInit {
this.http.delete(apiUrl).subscribe({
next: () => {
console.log('User deleted successfully');
this.toastService.success('User deleted successfully');
this.toastService.success(this.translate.instant('userDeletedSuccessfully'));
this.search();
},
error: (error) => {
console.error('Error deleting user:', error);
console.error(this.translate.instant('errorDeletingUser'), error);
}
});
}

View File

@ -16,8 +16,8 @@
<div class="search-container">
<mat-form-field joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}" appearance="fill"
class="search-string">
<mat-label>{{ 'searchCalendarLabel' | translate }}</mat-label>
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
<mat-label>{{ 'labelSearchCalendar' | translate }}</mat-label>
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']"
(keyup.enter)="search()">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
@ -30,7 +30,7 @@
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
text="{{ 'tableStepText' | translate }}">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef === 'isDefault' || column.columnDef === 'installed'">
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
@ -45,7 +45,7 @@
</ng-container>
<ng-container matColumnDef="actions" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
<td mat-cell *matCellDef="let calendar" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editCalendar(calendar)">
<mat-icon>edit</mat-icon>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit, signal } from '@angular/core';
import { MatTableDataSource } from "@angular/material/table";
import { DatePipe } from "@angular/common";
@ -17,6 +19,7 @@ import { ConfigService } from '@services/config.service';
styleUrl: './calendar.component.css'
})
export class CalendarComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
private apiUrl: string;
images: { downloadUrl: string; name: string; uuid: string }[] = [];
@ -34,22 +37,22 @@ export class CalendarComponent implements OnInit {
columns = [
{
columnDef: 'id',
header: 'ID',
header: 'labelId',
cell: (remoteCalendar: any) => `${remoteCalendar.id}`,
},
{
columnDef: 'name',
header: 'Nombre',
header: 'labelName',
cell: (remoteCalendar: any) => `${remoteCalendar.name}`
},
{
columnDef: 'rulesLength',
header: 'Nº de reglas',
header: 'numberOfRules',
cell: (remoteCalendar: any) => `${remoteCalendar.remoteCalendarRules.length}`
},
{
columnDef: 'createdAt',
header: 'Fecha de creación',
header: 'creationDate',
cell: (remoteCalendar: any) => `${this.datePipe.transform(remoteCalendar.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
}
];
@ -89,7 +92,7 @@ export class CalendarComponent implements OnInit {
this.loading = false;
},
error => {
console.error('Error fetching calendars', error);
console.error(this.translate.instant('errorFetchingCalendars'), error);
this.loading = false;
}
);
@ -99,7 +102,7 @@ export class CalendarComponent implements OnInit {
this.syncUds = true;
this.http.post(`${this.apiUrl}/${calendar.uuid}/sync-uds`, {}).subscribe({
next: () => {
this.toastService.success('Calendarios sincronizados correctamente');
this.toastService.success(this.translate.instant('calendarsSynchronizedCorrectly'));
this.search();
this.syncUds = false;
},
@ -136,10 +139,10 @@ export class CalendarComponent implements OnInit {
this.http.delete(apiUrl).subscribe({
next: () => {
this.search();
this.toastService.success('Calendar deleted successfully');
this.toastService.success(this.translate.instant('calendarDeletedSuccessfully'));
},
error: () => {
this.toastService.error('Error deleting calendar');
this.toastService.error(this.translate.instant('errorDeletingCalendar'));
}
});
}

View File

@ -8,7 +8,7 @@
<div *ngIf="!isRemoteAvailable" class="form-group">
<mat-label>{{ 'selectWeekDays' | translate }}</mat-label>
<p class="custom-text"> (Los dias y horas seleccionados se marcarán como aula no disponible para remote pc.) </p>
<p class="custom-text"> {{ 'remotePcCalendarRuleInfo' | translate }}</p>
<div class="weekday-toggle-group full-width">
<button
*ngFor="let day of weekDays"
@ -38,31 +38,29 @@
<div class="unavailability-summary" *ngIf="busyFromHour && busyToHour && busyWeekDays && getSelectedDays().length">
<mat-icon style="width: 50px;" color="warn">block</mat-icon>
<span class="summary-text">
El aula estará <strong>no disponible</strong> para Remote PC los días:
<strong>{{ getSelectedDays().join(', ') }}</strong> de
<strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
{{ 'classroomWillBe' | translate }}<strong>{{ 'notAvailableLowercase' | translate }}</strong> {{ 'forRemotePcOnDays' | translate }}<strong>{{ getSelectedDays().join(', ') }}</strong> {{ 'labelOf' | translate }}<strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
</span>
</div>
</div>
<div *ngIf="isRemoteAvailable" class="form-group">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'reasonLabel' | translate }}</mat-label>
<mat-label>{{ 'labelReason' | translate }}</mat-label>
<input matInput [(ngModel)]="availableReason" placeholder="{{ 'reasonPlaceholder' | translate }}" [required]="isRemoteAvailable">
<mat-hint>Razón por la cual el aula SI está disponible para su uso en Remote PC</mat-hint>
<mat-hint>{{ 'reasonAvailableRemotePc' | translate }}</mat-hint>
</mat-form-field>
<div class="time-fields">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'startDate' | translate }}</mat-label>
<input matInput [matDatepicker]="picker1" [(ngModel)]="availableFromDate" [required]="isRemoteAvailable">
<mat-hint>MM/DD/YYYY</mat-hint>
<mat-hint>{{ 'dateFormatPlaceholder' | translate }}</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker1"></mat-datepicker-toggle>
<mat-datepicker #picker1></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'endDate' | translate }}</mat-label>
<input matInput [matDatepicker]="picker2" [(ngModel)]="availableToDate" [required]="isRemoteAvailable">
<mat-hint>MM/DD/YYYY</mat-hint>
<mat-hint>{{ 'dateFormatPlaceholder' | translate }}</mat-hint>
<mat-datepicker-toggle matIconSuffix [for]="picker2"></mat-datepicker-toggle>
<mat-datepicker #picker2></mat-datepicker>
</mat-form-field>
@ -84,11 +82,9 @@
<div class="availability-summary" *ngIf="availableFromDate && availableToDate">
<mat-icon color="primary" style="width: 50px;">info</mat-icon>
<span class="summary-text">
El aula estará <strong>disponible</strong> para reserva desde el
<strong>{{ availableFromDate | date:'fullDate' }}</strong> hasta el
<strong>{{ availableToDate | date:'fullDate' }}</strong>
{{ 'classroomWillBe' | translate }}<strong>{{ 'statusAvailableLowercase' | translate }}</strong> {{ 'forReservationFrom' | translate }}<strong>{{ availableFromDate | date:'fullDate' }}</strong> {{ 'untilThe' | translate }}<strong>{{ availableToDate | date:'fullDate' }}</strong>
<span *ngIf="busyFromHour && busyToHour">
en el horario de <strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
{{ 'inScheduleOf' | translate }}<strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
</span>
</span>
</div>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, Inject } from '@angular/core';
import { ToastrService } from "ngx-toastr";
import { HttpClient } from "@angular/common/http";
@ -10,6 +12,7 @@ import { ConfigService } from '@services/config.service';
styleUrl: './create-calendar-rule.component.css'
})
export class CreateCalendarRuleComponent {
public translate = inject(TranslateService);
baseUrl: string;
name: string = '';
remoteCalendarRules: any[] = [];
@ -98,7 +101,7 @@ export class CreateCalendarRuleComponent {
this.http.put(`${this.baseUrl}${this.ruleId}`, formData)
.subscribe({
next: (response) => {
this.toastService.success('Calendar rule updated successfully');
this.toastService.success(this.translate.instant('calendarRuleUpdatedSuccessfully'));
this.dialogRef.close(true);
},
error: (error) => {
@ -110,7 +113,7 @@ export class CreateCalendarRuleComponent {
this.http.post(`${this.baseUrl}/remote-calendar-rules`, formData)
.subscribe({
next: (response) => {
this.toastService.success('Calendar rule created successfully');
this.toastService.success(this.translate.instant('calendarRuleCreatedSuccessfully'));
this.dialogRef.close(true);
},
error: (error) => {

View File

@ -2,7 +2,7 @@
<mat-dialog-content class="form-container">
<!-- Campo para el nombre -->
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'labelName' | translate }}</mat-label>
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
<input matInput [(ngModel)]="name" required>
<mat-icon *ngIf="isEditMode" matSuffix (click)="submitForm()">mode_edit</mat-icon>
</mat-form-field>
@ -28,9 +28,9 @@
<mat-icon matListItemIcon>event_available</mat-icon>
<div class="text-content">
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('remotePcStatusAvailable' | translate) : ('remotePcStatusUnavailable' | translate) }}</div>
<div matListItemLine *ngIf="!rule.isRemoteAvailable">Días: <strong>{{ rule.busyWeekDaysMap }}</strong></div>
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ 'labelDays' | translate }}<strong>{{ rule.busyWeekDaysMap }}</strong></div>
<div matListItemLine *ngIf="rule.isRemoteAvailable">Razón: {{ rule.availableReason }}</div>
<div matListItemLine *ngIf="rule.isRemoteAvailable">Días: <strong>{{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</strong></div>
<div matListItemLine *ngIf="rule.isRemoteAvailable">{{ 'labelDays' | translate }}<strong>{{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</strong></div>
<div matListItemLine>Horario: {{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
</div>
<div class="icon-container">
@ -50,6 +50,6 @@
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="submitForm()" [disabled]="!name || name === ''" cdkFocusInitial>
{{ 'buttonSave' | translate }}
{{ 'labelSaveButton' | translate }}
</button>
</mat-dialog-actions>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, Inject, OnInit } from '@angular/core';
import { ToastrService } from "ngx-toastr";
import { HttpClient } from "@angular/common/http";
@ -13,6 +15,7 @@ import { ConfigService } from '@services/config.service';
styleUrl: './create-calendar.component.css'
})
export class CreateCalendarComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
name: string = '';
remoteCalendarRules: any[] = [];
@ -50,7 +53,7 @@ export class CreateCalendarComponent implements OnInit {
});
},
error: (err) => {
console.error('Error fetching remote calendar:', err);
console.error(this.translate.instant('errorFetchingRemoteCalendar'), err);
}
});
}
@ -75,7 +78,7 @@ export class CreateCalendarComponent implements OnInit {
this.http.patch(`${this.baseUrl}${this.calendarId}`, payload)
.subscribe({
next: (response) => {
this.toastService.success('Calendar updated successfully');
this.toastService.success(this.translate.instant('calendarUpdatedSuccessfully'));
this.dialogRef.close(true);
this.load()
},
@ -88,7 +91,7 @@ export class CreateCalendarComponent implements OnInit {
this.http.post(`${this.baseUrl}/remote-calendars`, payload)
.subscribe({
next: (response) => {
this.toastService.success('Calendar created successfully');
this.toastService.success(this.translate.instant('calendarCreatedSuccessfully'));
this.dialogRef.close(true);
this.load()
},
@ -129,10 +132,10 @@ export class CreateCalendarComponent implements OnInit {
next: () => {
console.log('Calendar deleted successfully');
this.load();
this.toastService.success('Calendar deleted successfully');
this.toastService.success(this.translate.instant('calendarDeletedSuccessfully'));
},
error: (error) => {
this.toastService.error('Error deleting calendar');
this.toastService.error(this.translate.instant('errorDeletingCalendar'));
}
});
} else {

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
@ -8,6 +10,7 @@ import { ConfigService } from '@services/config.service';
providedIn: 'root'
})
export class DataService {
public translate = inject(TranslateService);
baseUrl: string;
private apiUrl: string;
@ -28,7 +31,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching remote calendars', error);
console.error(this.translate.instant('errorFetchingRemoteCalendars'), error);
return throwError(error);
})
);
@ -44,7 +47,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching calendar', error);
console.error(this.translate.instant('fetchingCalendarError'), error);
return throwError(error);
})
);

View File

@ -14,8 +14,8 @@
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
<mat-form-field appearance="fill" class="search-string">
<mat-label>{{ 'searchGroupNameLabel' | translate }}</mat-label>
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()">
<mat-label>{{ 'labelSearchGroupName' | translate }}</mat-label>
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
</mat-form-field>
@ -28,7 +28,7 @@
<div *ngIf="!loading">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
<td mat-cell *matCellDef="let commandGroup">
<ng-container *ngIf="column.columnDef !== 'commands'">
{{ column.cell(commandGroup) }}
@ -37,7 +37,7 @@
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
<td mat-cell *matCellDef="let client" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
<button mat-icon-button color="info" (click)="viewGroupDetails(client)">
<mat-icon>visibility</mat-icon>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MatDialog } from '@angular/material/dialog';
@ -16,6 +18,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./commands-groups.component.css']
})
export class CommandsGroupsComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
private apiUrl: string;
dataSource = new MatTableDataSource<any>();
@ -29,22 +32,22 @@ export class CommandsGroupsComponent implements OnInit {
columns = [
{
columnDef: 'id',
header: 'ID',
header: 'labelId',
cell: (group: any) => `${group.id}`,
},
{
columnDef: 'name',
header: 'Nombre',
header: 'labelName',
cell: (group: any) => `${group.name}`
},
{
columnDef: 'commands',
header: 'Lista de comandos',
header: 'commandList',
cell: (group: any) => `${group.commands}`
},
{
columnDef: 'createdAt',
header: 'Fecha de creación',
header: 'creationDate',
cell: (group: any) => `${this.datePipe.transform(group.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
}
];
@ -69,7 +72,7 @@ export class CommandsGroupsComponent implements OnInit {
this.loading = false;
},
(error) => {
console.error('Error fetching command groups', error);
console.error(this.translate.instant('fetchingCommandGroupsError'), error);
this.loading = false;
}
);
@ -103,11 +106,11 @@ export class CommandsGroupsComponent implements OnInit {
if (result) {
this.http.delete(`${this.apiUrl}/${group.uuid}`).subscribe({
next: () => {
this.toastService.success('Grupo de comandos eliminado con éxito');
this.toastService.success(this.translate.instant('commandGroupRemovedWith'));
this.search();
},
error: (error) => {
console.error('Error al eliminar el grupo de comandos:', error);
console.error(this.translate.instant('errorDeletingGroup'), error);
}
});
}

View File

@ -4,11 +4,11 @@
<form *ngIf="!loading" class="command-group-form" (ngSubmit)="onSubmit()">
<mat-form-field>
<mat-label>{{ 'groupNameLabel' | translate }}</mat-label>
<mat-label>{{ 'labelGroupName' | translate }}</mat-label>
<input matInput [(ngModel)]="groupName" name="groupName" required />
</mat-form-field>
<mat-slide-toggle [(ngModel)]="enabled" name="enabled">{{ 'enabledToggle' | translate }}</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="enabled" name="enabled">{{ 'labelEnabled' | translate }}</mat-slide-toggle>
<div class="command-selection">
<div class="available-commands">
@ -16,12 +16,12 @@
<div class="table-wrapper">
<table mat-table [dataSource]="availableCommands" class="mat-elevation-z8">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'nameColumn' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'labelRoleName' | translate }} </th>
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef> {{ 'actionsColumn' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'actions' | translate }} </th>
<td mat-cell *matCellDef="let command">
<button mat-icon-button type="button" (click)="addCommand(command)">
<mat-icon>add</mat-icon>
@ -55,5 +55,5 @@
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="onSubmit()" cdkFocusInitial>{{ 'buttonSave' | translate }}</button>
<button class="submit-button" (click)="onSubmit()" cdkFocusInitial>{{ 'labelSaveButton' | translate }}</button>
</mat-dialog-actions>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
@ -11,6 +13,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./create-command-group.component.css']
})
export class CreateCommandGroupComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
availableCommands: any[] = [];
selectedCommands: any[] = [];
@ -48,7 +51,7 @@ export class CreateCommandGroupComponent implements OnInit {
this.loading = false;
},
(error) => {
console.error('Error fetching available commands', error);
console.error(this.translate.instant('errorFetchingAvailableCommands'), error);
this.loading = false;
}
);
@ -88,23 +91,23 @@ export class CreateCommandGroupComponent implements OnInit {
const groupId = this.data.group.uuid;
this.http.patch(`${this.baseUrl}/command-groups/${groupId}`, payload).subscribe({
next: () => {
this.toastService.success('Grupo de comandos actualizado con éxito');
this.toastService.success(this.translate.instant('commandGroupUpdatedWith'));
this.dialogRef.close();
},
error: (error) => {
console.error('Error actualizando el grupo de comandos', error);
this.toastService.error('Error al actualizar el grupo de comandos');
console.error(this.translate.instant('errorUpdatingGroup'), error);
this.toastService.error(this.translate.instant('errorUpdatingGroup'));
}
});
} else {
this.http.post(`${this.baseUrl}/command-groups`, payload).subscribe({
next: () => {
this.toastService.success('Grupo de comandos creado con éxito');
this.toastService.success(this.translate.instant('commandGroupCreatedWith'));
this.dialogRef.close();
},
error: (error) => {
console.error('Error creando el grupo de comandos', error);
this.toastService.error('Error al crear el grupo de comandos');
console.error(this.translate.instant('errorCreatingGroup'), error);
this.toastService.error(this.translate.instant('errorCreatingGroup'));
}
});
}

View File

@ -16,12 +16,12 @@
<h3>{{ 'includedCommands' | translate }}</h3>
<table mat-table [dataSource]="data.commands" class="mat-elevation-z8">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'nameColumn' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'labelRoleName' | translate }} </th>
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
</ng-container>
<ng-container matColumnDef="uuid">
<th mat-header-cell *matHeaderCellDef> UUID </th>
<th mat-header-cell *matHeaderCellDef> {{ 'labelUuid' | translate }}</th>
<td mat-cell *matCellDef="let command"> {{ command.uuid }} </td>
</ng-container>
@ -36,7 +36,7 @@
<form [formGroup]="form">
<h4>{{ 'selectClients' | translate }}</h4>
<mat-form-field appearance="fill">
<mat-label>{{ 'clientsLabel' | translate }}</mat-label>
<mat-label>{{ 'labelClients' | translate }}</mat-label>
<mat-select formControlName="selectedClients" multiple (selectionChange)="onClientSelectionChange($event)">
<mat-option *ngFor="let client of clients" [value]="client.uuid">
{{ client.name }}

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@ -11,6 +13,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./detail-command-group.component.css']
})
export class DetailCommandGroupComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
form!: FormGroup;
clients: any[] = [];
@ -45,7 +48,7 @@ export class DetailCommandGroupComponent implements OnInit {
this.loading = false;
},
error: (error) => {
console.error('Error fetching clients:', error);
console.error(this.translate.instant('fetchingClients1Error'), error);
this.loading = false;
}
});
@ -70,11 +73,11 @@ export class DetailCommandGroupComponent implements OnInit {
this.http.post(apiUrl, payload).subscribe({
next: () => {
this.dialogRef.close();
this.toastService.success('Grupo de comandos ejecutado exitosamente');
this.toastService.success(this.translate.instant('groupOfCommandsExecutedSuccessfully'));
this.loading = false;
},
error: (error) => {
console.error('Error ejecutando grupo de comandos:', error);
console.error(this.translate.instant('errorExecutingGroupOfCommands'), error);
this.loading = false;
}
});

View File

@ -16,8 +16,8 @@
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
<mat-form-field appearance="fill" class="search-string">
<mat-label>{{ 'searchTaskLabel' | translate }}</mat-label>
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
<mat-label>{{ 'labelSearchTask' | translate }}</mat-label>
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
<mat-icon matSuffix>search</mat-icon>
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
</mat-form-field>
@ -26,7 +26,7 @@
<div *ngIf="!loading">
<table mat-table [dataSource]="tasks" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
<td mat-cell *matCellDef="let task">
<ng-container *ngIf="column.columnDef !== 'management' && column.columnDef !== 'scope' && column.columnDef !== 'organizationalUnit'">
{{ column.cell(task) }}
@ -50,14 +50,14 @@
</button>
<button class="action-button script-btn" (click)="openShowScriptDialog(task)">
<mat-icon>code</mat-icon>
<span>{{ 'taskActions' | translate }}</span>
<span>{{ 'actions' | translate }}</span>
</button>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
<td mat-cell *matCellDef="let task" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
<button mat-icon-button color="primary" (click)="editTask(task)">
<mat-icon>edit</mat-icon>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MatDialog } from '@angular/material/dialog';
@ -16,6 +18,7 @@ import {DatePipe} from "@angular/common";
styleUrls: ['./commands-task.component.css']
})
export class CommandsTaskComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
tasks: any[] = [];
filters: { [key: string]: string | boolean } = {};
@ -26,13 +29,13 @@ export class CommandsTaskComponent implements OnInit {
datePipe: DatePipe = new DatePipe('es-ES');
columns = [
{ columnDef: 'id', header: 'ID', cell: (task: any) => task.id },
{ columnDef: 'name', header: 'Nombre de tarea', cell: (task: any) => task.name },
{ columnDef: 'scope', header: 'Ámbito', cell: (task: any) => task.scope },
{ columnDef: 'organizationalUnit', header: 'Unidad Organizativa', cell: (task: any) => task.organizationalUnit?.name || '' },
{ columnDef: 'management', header: 'Gestiones', cell: (task: any) => task.schedules },
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (task: any) => this.datePipe.transform(task.nextExecution, 'dd/MM/yyyy HH:mm:ss') },
{ columnDef: 'createdBy', header: 'Creado por', cell: (task: any) => task.createdBy },
{ columnDef: 'id', header: 'labelId', cell: (task: any) => task.id },
{ columnDef: 'name', header: 'taskName', cell: (task: any) => task.name },
{ columnDef: 'scope', header: 'labelScope', cell: (task: any) => task.scope },
{ columnDef: 'organizationalUnit', header: 'organizationalUnit', cell: (task: any) => task.organizationalUnit?.name || '' },
{ columnDef: 'management', header: 'efforts', cell: (task: any) => task.schedules },
{ columnDef: 'nextExecution', header: 'nextExecution', cell: (task: any) => this.datePipe.transform(task.nextExecution, 'dd/MM/yyyy HH:mm:ss') },
{ columnDef: 'createdBy', header: 'createdBy', cell: (task: any) => task.createdBy },
];
displayedColumns: string[] = ['id', 'name', 'scope', 'management', 'nextExecution', 'createdBy', 'actions'];
@ -65,7 +68,7 @@ export class CommandsTaskComponent implements OnInit {
this.loading = false;
},
(error) => {
console.error('Error fetching tasks', error);
console.error(this.translate.instant('fetchingTasksError'), error);
this.loading = false;
}
);
@ -133,11 +136,11 @@ export class CommandsTaskComponent implements OnInit {
if (result) {
this.http.delete(`${this.apiUrl}/${task.uuid}`).subscribe({
next: () => {
this.toastService.success('Tarea eliminada con éxito');
this.toastService.success(this.translate.instant('taskDeletedSuccessfully'));
this.loadTasks();
},
error: (error) => {
console.error('Error al eliminar la tarea:', error);
console.error(this.translate.instant('errorDeletingTask'), error);
}
});
}

View File

@ -26,7 +26,7 @@
</mat-form-field>
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
<label class="section-label">{{ 'weekDaysLabel' | translate }}:</label>
<label class="section-label">{{ 'labelWeekDays' | translate }}:</label>
<div class="weekday-toggle-group">
<button
*ngFor="let day of weekDays"
@ -40,7 +40,7 @@
</div>
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
<label class="section-label">{{ 'monthsLabel' | translate }}:</label>
<label class="section-label">{{ 'labelMonths' | translate }}:</label>
<div class="month-toggle-row" *ngFor="let row of monthRows">
<button
*ngFor="let month of row"
@ -106,6 +106,6 @@
<button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" [disabled]="!form.valid" (click)="onSubmit()">
<mat-icon>save</mat-icon>
{{ 'buttonSave' | translate }}
{{ 'labelSaveButton' | translate }}
</button>
</mat-dialog-actions>

View File

@ -1,4 +1,6 @@
import {Component, Inject, OnInit} from '@angular/core';
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {Component, Inject, OnInit } from '@angular/core';
import {FormBuilder, FormGroup} from "@angular/forms";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http";
@ -11,12 +13,13 @@ import {ConfigService} from "@services/config.service";
styleUrl: './create-task-schedule.component.css'
})
export class CreateTaskScheduleComponent implements OnInit{
public translate = inject(TranslateService);
form: FormGroup;
baseUrl: string;
apiUrl: string;
recurrenceTypes = [
{ value: 'none', label: 'Sin repetición' },
{ value: 'custom', label: 'Repetición personalizada' }
{ value: 'none', label : this.translate.instant('withoutRepetition') },
{ value: 'custom', label : this.translate.instant('customRepeat') }
];
weekDays: string[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
isSingleDateSelected: boolean = true;
@ -105,7 +108,7 @@ export class CreateTaskScheduleComponent implements OnInit{
}, {});
},
(error) => {
console.error('Error loading schedule data', error);
console.error(this.translate.instant('errorLoadingScheduleData'), error);
}
);
}
@ -173,12 +176,12 @@ export class CreateTaskScheduleComponent implements OnInit{
const selectedMonthNames = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
if (selectedDayNames.length === 0) {
this.toastr.error('Debe seleccionar al menos un día de la semana');
this.toastr.error(this.translate.instant('youMustSelectAtLeastOne1'));
return;
}
if (selectedMonthNames.length === 0) {
this.toastr.error('Debe seleccionar al menos un mes');
this.toastr.error(this.translate.instant('youMustSelectAtLeastOne2'));
return;
}
@ -186,12 +189,12 @@ export class CreateTaskScheduleComponent implements OnInit{
const endDate = formData.recurrenceDetails?.endDate;
if (!initDate || !endDate) {
this.toastr.error('Debe especificar fecha de inicio y fecha de fin');
this.toastr.error(this.translate.instant('youMustSpecifyStartDate'));
return;
}
if (new Date(endDate) <= new Date(initDate)) {
this.toastr.error('La fecha de fin debe ser posterior a la fecha de inicio');
this.toastr.error(this.translate.instant('theEndDateMust'));
return;
}
}
@ -215,21 +218,21 @@ export class CreateTaskScheduleComponent implements OnInit{
const taskId = this.data.task.uuid;
this.http.patch<any>(`${this.baseUrl}${this.data.schedule['@id']}`, payload).subscribe({
next: () => {
this.toastr.success('Programacion de tarea actualizada con éxito');
this.toastr.success(this.translate.instant('updatedTaskScheduleWith'));
this.dialogRef.close(true);
},
error: () => {
this.toastr.error('Error al actualizar la tarea');
this.toastr.error(this.translate.instant('errorUpdatingTask'));
}
});
} else {
this.http.post<any>(this.apiUrl, payload).subscribe({
next: () => {
this.toastr.success('Programacion de tarea creada con éxito');
this.toastr.success(this.translate.instant('taskScheduleCreatedWith'));
this.dialogRef.close(true);
},
error: () => {
this.toastr.error('Error al crear la tarea');
this.toastr.error(this.translate.instant('errorCreatingTask'));
}
});
}

View File

@ -64,7 +64,7 @@
</div>
<div class="info-supported-ops">
<p class="info-supported-label">{{ 'supportedOpsLabel' | translate }}:</p>
<p class="info-supported-label">{{ 'labelSupportedOps' | translate }}:</p>
<ng-container *ngFor="let cat of categories">
<ng-container *ngIf="cat.id === selectedCategory">
<div *ngFor="let action of cat.actions" class="info-op-chip">
@ -131,7 +131,7 @@
<mat-label>{{ 'enterScript' | translate }}</mat-label>
<textarea matInput [(ngModel)]="newScript" rows="6" [placeholder]="'writeScriptHere' | translate"></textarea>
</mat-form-field>
<button mat-flat-button color="primary" (click)="saveNewScript()">{{ 'saveScript' | translate }}</button>
<button mat-flat-button color="primary" (click)="saveNewScript()">{{ 'buttonSaveScript' | translate }}</button>
</div>
<div *ngIf="commandType === 'existing'">
@ -145,7 +145,7 @@
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
<div class="script-content">
<h3>{{ 'scriptLabel' | translate }}:</h3>
<h3>{{ 'labelScript' | translate }}:</h3>
<div class="script-preview" [innerHTML]="scriptContent"></div>
</div>
<div class="script-params" *ngIf="parameterNames.length > 0">
@ -165,5 +165,5 @@
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="onSubmit()" *ngIf="selectedAction">{{ 'buttonSave' | translate }}</button>
<button class="submit-button" (click)="onSubmit()" *ngIf="selectedAction">{{ 'labelSaveButton' | translate }}</button>
</mat-dialog-actions>

View File

@ -200,7 +200,7 @@ export class CreateTaskScriptComponent implements OnInit {
saveNewScript(): void {
if (!this.newScript.trim()) {
this.toastService.error('Debe ingresar un script antes de guardar.');
this.toastService.error(this.translate.instant('youMustEnterAScriptBefore'));
return;
}
const dialogRef = this.dialog.open(SaveScriptComponent, {
@ -209,7 +209,7 @@ export class CreateTaskScriptComponent implements OnInit {
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastService.success('Script guardado correctamente');
this.toastService.success(this.translate.instant('scriptSavedSuccessfully'));
}
});
}
@ -244,7 +244,7 @@ export class CreateTaskScriptComponent implements OnInit {
break;
default:
this.toastService.error('Tipo de acción no reconocido');
this.toastService.error(this.translate.instant('unrecognizedActionType'));
return;
}
@ -269,12 +269,12 @@ export class CreateTaskScriptComponent implements OnInit {
private validateScript(): boolean {
if (this.commandType === 'new') {
if (!this.newScript.trim()) {
this.toastService.error('Debe ingresar un script');
this.toastService.error(this.translate.instant('youMustEnterAScript'));
return false;
}
} else if (this.commandType === 'existing') {
if (!this.selectedScript) {
this.toastService.error('Debe seleccionar un script');
this.toastService.error(this.translate.instant('youMustSelectAScript'));
return false;
}
}

View File

@ -9,7 +9,7 @@
<mat-button-toggle-group *ngIf="data?.source === 'assistant'" [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode" appearance="standard">
<mat-button-toggle value="create">
<mat-icon>add_task</mat-icon>
{{ 'createTaskLabel' | translate }}
{{ 'labelCreateTask' | translate }}
</mat-button-toggle>
<mat-button-toggle value="add">
<mat-icon>playlist_add</mat-icon>
@ -41,13 +41,13 @@
<form *ngIf="taskMode === 'create' && taskForm && !loading" [formGroup]="taskForm" class="task-form">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
<input matInput formControlName="name" [placeholder]="'nameLabel' | translate">
<mat-error *ngIf="taskForm.get('name')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'notesLabel' | translate }}</mat-label>
<mat-label>{{ 'labelNotes' | translate }}</mat-label>
<textarea matInput formControlName="notes" [placeholder]="'notesPlaceholder' | translate"></textarea>
</mat-form-field>
@ -58,19 +58,19 @@
<mat-divider style="margin: 15px 0;"></mat-divider>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'scopeLabel' | translate }}</mat-label>
<mat-label>{{ 'labelScope' | translate }}</mat-label>
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)"
[disabled]="data?.clients && data.clients.length >= 1">
<mat-option value="organizational-unit">{{ 'scopeOrganizationalUnit' | translate }}</mat-option>
<mat-option value="organizational-unit">{{ 'organizationalUnit' | translate }}</mat-option>
<mat-option value="classrooms-group">{{ 'scopeClassroomsGroup' | translate }}</mat-option>
<mat-option value="classroom">{{ 'scopeClassroom' | translate }}</mat-option>
<mat-option value="clients-group">{{ 'scopeClientsGroup' | translate }}</mat-option>
<mat-option value="clients">{{ 'scopeClients' | translate }}</mat-option>
<mat-option value="classroom">{{ 'classrooms' | translate }}</mat-option>
<mat-option value="clients-group">{{ 'clientGroupsTitle' | translate }}</mat-option>
<mat-option value="clients">{{ 'labelClients' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="taskForm.get('scope')?.value !== 'clients'" appearance="fill" class="full-width">
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
<mat-label>{{ 'labelOrganizationalUnit' | translate }}</mat-label>
<mat-select formControlName="organizationalUnit">
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
<div class="unit-name">{{ unit.name }}</div>
@ -121,6 +121,6 @@
class="submit-button"
(click)="taskMode === 'create' ? saveTask() : addToExistingTask()"
>
{{ 'buttonSave' | translate }}
{{ 'labelSaveButton' | translate }}
</button>
</mat-dialog-actions>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
@ -15,6 +17,7 @@ import {ShowTaskScriptComponent} from "../show-task-script/show-task-script.comp
styleUrls: ['./create-task.component.css']
})
export class CreateTaskComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
taskForm: FormGroup;
availableCommandGroups: any[] = [];
@ -109,7 +112,7 @@ export class CreateTaskComponent implements OnInit {
resolve();
},
(error) => {
this.toastr.error('Error al cargar los datos de la tarea');
this.toastr.error(this.translate.instant('errorLoadingData'));
reject(error);
}
);
@ -124,7 +127,7 @@ export class CreateTaskComponent implements OnInit {
resolve();
},
(error) => {
this.toastr.error('Error al cargar las tareas existentes');
this.toastr.error(this.translate.instant('errorLoadingTasks'));
reject(error);
}
);
@ -137,7 +140,7 @@ export class CreateTaskComponent implements OnInit {
this.selectedClients = [...this.data.selectedClients];
this.clients = [...this.data.selectedClients];
} else {
this.toastr.error('No hay clientes pre-seleccionados para este ámbito');
this.toastr.error(this.translate.instant('thereAreNoPreselectedClientsFor'));
this.taskForm.get('scope')?.setValue('');
return;
}
@ -168,7 +171,7 @@ export class CreateTaskComponent implements OnInit {
this.availableOrganizationalUnits = filteredUnits;
resolve();
}, error => {
this.toastr.error('Error al filtrar las unidades organizacionales');
this.toastr.error(this.translate.instant('errorWhenFilteringUnits'));
reject(error);
});
})
@ -187,7 +190,7 @@ export class CreateTaskComponent implements OnInit {
resolve();
},
(error) => {
this.toastr.error('Error al cargar los grupos de comandos');
this.toastr.error(this.translate.instant('errorLoadingGroups'));
reject(error);
});
});
@ -201,7 +204,7 @@ export class CreateTaskComponent implements OnInit {
resolve();
},
(error) => {
this.toastr.error('Error al cargar los comandos individuales');
this.toastr.error(this.translate.instant('errorLoadingCommands'));
reject(error);
});
});
@ -216,7 +219,7 @@ export class CreateTaskComponent implements OnInit {
resolve();
},
(error) => {
this.toastr.error('Error al cargar las unidades organizacionales');
this.toastr.error(this.translate.instant('errorLoadingUnits'));
reject(error);
}
);
@ -225,12 +228,12 @@ export class CreateTaskComponent implements OnInit {
addToExistingTask() {
if (!this.selectedExistingTask) {
this.toastr.error('Debes seleccionar una tarea existente.');
this.toastr.error(this.translate.instant('youMustSelectAnExistingTask'));
return;
}
if (this.executionOrder == null || this.executionOrder < 1) {
this.toastr.error('Debes introducir un orden de ejecución válido (mayor que 0).');
this.toastr.error(this.translate.instant('youMustEnterAnOrderOf'));
return;
}
@ -239,14 +242,14 @@ export class CreateTaskComponent implements OnInit {
executionOrder: this.executionOrder
};
this.toastr.success('Tarea actualizada con éxito');
this.toastr.success(this.translate.instant('taskUpdatedSuccessfully'));
this.dialogRef.close(data);
}
saveTask(): void {
if (this.taskForm.invalid) {
this.toastr.error('Por favor, rellene todos los campos obligatorios');
this.toastr.error(this.translate.instant('pleaseFillOutAll'));
return;
}
@ -254,12 +257,12 @@ export class CreateTaskComponent implements OnInit {
const scope = formData.scope;
if (scope === 'clients' && this.selectedClients.length === 0) {
this.toastr.error('Debe seleccionar al menos un cliente');
this.toastr.error(this.translate.instant('youMustSelectAtLeastOne'));
return;
}
if (scope !== 'clients' && !formData.organizationalUnit) {
this.toastr.error('Debe seleccionar una unidad organizativa');
this.toastr.error(this.translate.instant('youMustSelectAnOrganizationalUnit'));
return;
}
@ -280,17 +283,17 @@ export class CreateTaskComponent implements OnInit {
const taskId = this.data.task.uuid;
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({
next: () => {
this.toastr.success('Tarea actualizada con éxito');
this.toastr.success(this.translate.instant('taskUpdatedSuccessfully'));
this.dialogRef.close(true);
},
error: () => {
this.toastr.error('Error al actualizar la tarea');
this.toastr.error(this.translate.instant('errorUpdatingTask'));
}
});
} else {
this.http.post<any>(this.apiUrl, payload).subscribe({
next: response => {
this.toastr.success('Tarea creada con éxito');
this.toastr.success(this.translate.instant('taskCreatedSuccessfully'));
this.dialogRef.close(response);
if (this.data?.source === 'assistant') {
if (formData.scheduleAfterCreate) {
@ -301,7 +304,7 @@ export class CreateTaskComponent implements OnInit {
}
},
error: () => {
this.toastr.error('Error al crear la tarea');
this.toastr.error(this.translate.instant('errorCreatingTask'));
}
});
}
@ -313,7 +316,7 @@ export class CreateTaskComponent implements OnInit {
data: { task: task }
}).afterClosed().subscribe(result => {
if (result) {
this.toastr.success('Tarea programada correctamente');
this.toastr.success(this.translate.instant('successfullyScheduledTask'));
}
});
}
@ -334,7 +337,7 @@ export class CreateTaskComponent implements OnInit {
data: { task: task }
}).afterClosed().subscribe(result => {
if (result) {
this.toastr.success('Tarea programada correctamente');
this.toastr.success(this.translate.instant('successfullyScheduledTask'));
}
});
}

View File

@ -9,27 +9,26 @@
<mat-card-content>
<p><strong>{{ 'taskId' | translate }}:</strong> {{ task.uuid }}</p>
<p><strong>{{ 'creationDate' | translate }}:</strong> {{ task.createdAt | date: 'short' }}</p>
<p><strong>{{ 'notes' | translate }}:</strong> {{ task.notes }}</p>
<p><strong>{{ 'labelNotes' | translate }}:</strong> {{ task.notes }}</p>
<h3>{{ 'commandsToExecute' | translate }}</h3>
<table mat-table [dataSource]="task.commandTaskScripts || []" class="mat-elevation-z8" *ngIf="task.commandTaskScripts?.length > 0">
<ng-container matColumnDef="order">
<th mat-header-cell *matHeaderCellDef>Orden</th>
<th mat-header-cell *matHeaderCellDef>{{ 'labelOrder' | translate }}</th>
<td mat-cell *matCellDef="let script">{{ script.order }}</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>Tipo</th>
<th mat-header-cell *matHeaderCellDef>{{ 'labelType' | translate }}</th>
<td mat-cell *matCellDef="let script">{{ script.type }}</td>
</ng-container>
<ng-container matColumnDef="content">
<th mat-header-cell *matHeaderCellDef>Contenido</th>
<th mat-header-cell *matHeaderCellDef>{{ 'labelContent' | translate }}</th>
<td mat-cell *matCellDef="let script">
<span *ngIf="script.content">{{ script.content }}</span>
<span *ngIf="!script.content && script.parameters">
(parámetros configurados)
</span>
{{ 'configuredParameters' | translate }}</span>
</td>
</ng-container>
@ -37,7 +36,7 @@
<tr mat-row *matRowDef="let row; columns: ['order', 'type', 'content'];"></tr>
</table>
<p *ngIf="!task.commandTaskScripts?.length">No hay acciones configuradas.</p>
<p *ngIf="!task.commandTaskScripts?.length">{{ 'noActionsConfigured' | translate }}</p>
</mat-card-content>
</mat-card>

View File

@ -12,7 +12,7 @@
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-label>{{ 'searchScheduleLabel' | translate }}</mat-label>
<mat-label>{{ 'labelSearchSchedule' | translate }}</mat-label>
<input matInput [placeholder]="'searchPlaceholder' | translate" [(ngModel)]="filters['name']"
(keyup.enter)="loadData()">
<mat-icon matSuffix>search</mat-icon>
@ -26,7 +26,7 @@
<app-loading [isLoading]="loading"></app-loading>
<table *ngIf="!loading" 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>
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
<td mat-cell *matCellDef="let schedule">
<ng-container *ngIf="column.columnDef === 'recurrenceType'">
@ -97,7 +97,7 @@
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
<td mat-cell *matCellDef="let schedule" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editSchedule(schedule)" [matTooltip]="'editScheduleTooltip' | translate">
<mat-icon>edit</mat-icon>
@ -120,5 +120,5 @@
</mat-dialog-content>
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onNoClick()">{{ 'close' | translate }}</button>
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonClose' | translate }}</button>
</mat-dialog-actions>

View File

@ -1,4 +1,6 @@
import {Component, Inject, OnInit} from '@angular/core';
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {Component, Inject, OnInit } from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {Client} from "../../../groups/model/model";
import {ToastrService} from "ngx-toastr";
@ -15,6 +17,7 @@ import {DatePipe} from "@angular/common";
styleUrl: './show-task-schedule.component.css'
})
export class ShowTaskScheduleComponent implements OnInit{
public translate = inject(TranslateService);
baseUrl: string;
dataSource = new MatTableDataSource<any>([]);
length = 0;
@ -26,13 +29,13 @@ export class ShowTaskScheduleComponent implements OnInit{
datePipe: DatePipe = new DatePipe('es-ES');
columns = [
{ columnDef: 'id', header: 'ID', cell: (schedule: any) => schedule.id },
{ columnDef: 'recurrenceType', header: 'Tipo de Programación', cell: (schedule: any) => this.getRecurrenceTypeDisplay(schedule) },
{ columnDef: 'executionTime', header: 'Hora de ejecución', cell: (schedule: any) => this.formatExecutionTimeForDisplay(schedule.executionTime) },
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (schedule: any) => this.calculateNextExecution(schedule) },
{ columnDef: 'daysOfWeek', header: 'Días de la semana', cell: (schedule: any) => this.formatDaysOfWeek(schedule.recurrenceDetails?.daysOfWeek) },
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => this.formatMonths(schedule.recurrenceDetails?.months) },
{ columnDef: 'enabled', header: 'Estado', cell: (schedule: any) => schedule.enabled }
{ columnDef: 'id', header: 'labelId', cell: (schedule: any) => schedule.id },
{ columnDef: 'recurrenceType', header: 'typeOfProgramming', cell: (schedule: any) => this.getRecurrenceTypeDisplay(schedule) },
{ columnDef: 'executionTime', header: 'executionTime', cell: (schedule: any) => this.formatExecutionTimeForDisplay(schedule.executionTime) },
{ columnDef: 'nextExecution', header: 'nextExecution', cell: (schedule: any) => this.calculateNextExecution(schedule) },
{ columnDef: 'daysOfWeek', header: 'daysOfTheWeek', cell: (schedule: any) => this.formatDaysOfWeek(schedule.recurrenceDetails?.daysOfWeek) },
{ columnDef: 'months', header: 'labelMonths', cell: (schedule: any) => this.formatMonths(schedule.recurrenceDetails?.months) },
{ columnDef: 'enabled', header: 'state', cell: (schedule: any) => schedule.enabled }
];
displayedColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'daysOfWeek', 'months', 'enabled', 'actions'];
@ -67,7 +70,7 @@ export class ShowTaskScheduleComponent implements OnInit{
},
(error) => {
this.loading = false;
this.toastService.error('Error al cargar las programaciones');
this.toastService.error(this.translate.instant('errorLoadingProgramming'));
}
);
}
@ -79,7 +82,7 @@ export class ShowTaskScheduleComponent implements OnInit{
}).afterClosed().subscribe(result => {
if (result) {
this.loadData();
this.toastService.success('Programación creada correctamente');
this.toastService.success(this.translate.instant('programmingCreatedCorrectly'));
}
});
}
@ -91,7 +94,7 @@ export class ShowTaskScheduleComponent implements OnInit{
}).afterClosed().subscribe(result => {
if (result) {
this.loadData();
this.toastService.success('Programación actualizada correctamente');
this.toastService.success(this.translate.instant('programmingUpdatedCorrectly'));
}
});
}
@ -106,7 +109,7 @@ export class ShowTaskScheduleComponent implements OnInit{
if (result) {
this.http.delete(`${this.baseUrl}${schedule['@id']}`).subscribe(
() => {
this.toastService.success('Programación eliminada correctamente');
this.toastService.success(this.translate.instant('programmingRemovedCorrectly'));
this.loadData();
},
(error) => {
@ -317,7 +320,7 @@ export class ShowTaskScheduleComponent implements OnInit{
if (schedule.recurrenceType === 'none') {
return {
type: 'none',
label: 'Ejecución única',
label : this.translate.instant('uniqueExecution'),
icon: 'event',
color: 'primary',
details: schedule.executionDate ?

View File

@ -48,7 +48,7 @@
</ng-container>
<ng-container matColumnDef="content">
<th mat-header-cell *matHeaderCellDef>{{ 'scriptLabel' | translate }}</th>
<th mat-header-cell *matHeaderCellDef>{{ 'labelScript' | translate }}</th>
<td mat-cell *matCellDef="let item">
<div *ngIf="item.content" class="script-content-cell">
<code>{{ item.content }}</code>
@ -72,7 +72,7 @@
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
<td mat-cell *matCellDef="let item" style="text-align: center;">
<button mat-icon-button color="warn" (click)="deleteTaskScript(item)">
<mat-icon>delete</mat-icon>
@ -92,5 +92,5 @@
</mat-dialog-content>
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onNoClick()">{{ 'close' | translate }}</button>
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonClose' | translate }}</button>
</mat-dialog-actions>

View File

@ -1,20 +1,23 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {ToastrService} from "ngx-toastr";
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
import {ConfigService} from "@services/config.service";
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
import {CreateTaskScriptComponent} from "../create-task-script/create-task-script.component";
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
import {ViewParametersModalComponent} from "./view-parameters-modal/view-parameters-modal.component";
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, Inject, OnInit } from '@angular/core';
import { MatTableDataSource } from "@angular/material/table";
import { ToastrService } from "ngx-toastr";
import { HttpClient } from "@angular/common/http";
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material/dialog";
import { ConfigService } from "@services/config.service";
import { CreateTaskScheduleComponent } from "../create-task-schedule/create-task-schedule.component";
import { CreateTaskScriptComponent } from "../create-task-script/create-task-script.component";
import { DeleteModalComponent } from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
import { ViewParametersModalComponent } from "./view-parameters-modal/view-parameters-modal.component";
@Component({
selector: 'app-show-task-script',
templateUrl: './show-task-script.component.html',
styleUrl: './show-task-script.component.css'
})
export class ShowTaskScriptComponent implements OnInit{
export class ShowTaskScriptComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
dataSource = new MatTableDataSource<any>([]);
length = 0;
@ -25,26 +28,26 @@ export class ShowTaskScriptComponent implements OnInit{
filters: { [key: string]: string } = {};
columns = [
{ columnDef: 'id', header: 'ID', cell: (item: any) => item.id },
{ columnDef: 'order', header: 'Orden', cell: (item: any) => item.order },
{ columnDef: 'type', header: 'Tipo', cell: (item: any) => item.type },
{ columnDef: 'parameters', header: 'Parámetros', cell: (item: any) => item.parameters },
{ columnDef: 'content', header: 'Contenido', cell: (item: any) => item.content },
{ columnDef: 'id', header: 'labelId', cell: (item: any) => item.id },
{ columnDef: 'order', header: 'labelOrder', cell: (item: any) => item.order },
{ columnDef: 'type', header: 'labelType', cell: (item: any) => item.type },
{ columnDef: 'parameters', header: 'labelParameters', cell: (item: any) => item.parameters },
{ columnDef: 'content', header: 'labelContent', cell: (item: any) => item.content },
];
displayedColumns: string[] = ['order', 'type', 'content', 'parameters', 'actions'];
typeLabels: { [key: string]: { label: string, icon: string } } = {
'partition-and-format': { label: 'Particionar y formatear', icon: 'storage' },
'partition-assistant': { label: 'Asistente de particionado', icon: 'storage' },
'deploy-image': { label: 'Desplegar imagen', icon: 'cloud_download' },
'create-image': { label: 'Crear imagen', icon: 'cloud_upload' },
'run-script': { label: 'Ejecutar script', icon: 'code' },
'partition-and-format': { label: this.translate.instant('partitionAndFormat'), icon: 'storage' },
'partition-assistant': { label: this.translate.instant('partitionTitle'), icon: 'storage' },
'deploy-image': { label: this.translate.instant('displayImage'), icon: 'cloud_download' },
'create-image': { label: this.translate.instant('createImage'), icon: 'cloud_upload' },
'run-script': { label: this.translate.instant('runScript'), icon: 'code' },
'reboot': { label: 'Reiniciar', icon: 'restart_alt' },
'shutdown': { label: 'Apagar', icon: 'power_off' },
'poweron': { label: 'Encender', icon: 'power' },
'power-on': { label: 'Encender', icon: 'power' },
'login': { label: 'Iniciar sesión', icon: 'login' },
'login': { label: this.translate.instant('login'), icon: 'login' },
};
getTypeDisplay(type: string): { label: string, icon: string } {
@ -94,7 +97,7 @@ export class ShowTaskScriptComponent implements OnInit{
if (result) {
this.http.delete(`${this.baseUrl}${schedule['@id']}`).subscribe(
() => {
this.toastService.success('Eliminado correctamente');
this.toastService.success(this.translate.instant('removedSuccessfully'));
this.loadData();
},
(error) => {

View File

@ -1,7 +1,7 @@
<h2 mat-dialog-title>Parámetros</h2>
<h2 mat-dialog-title>{{ 'labelParameters' | translate }}</h2>
<mat-dialog-content>
<pre>{{ data | json }}</pre>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="close()">Cerrar</button>
<button mat-button (click)="close()">{{ 'buttonClose' | translate }}</button>
</mat-dialog-actions>

View File

@ -12,8 +12,8 @@
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
<mat-form-field appearance="fill" class="search-string">
<mat-label>{{ 'searchCommandLabel' | translate }}</mat-label>
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
<mat-label>{{ 'labelSearchCommand' | translate }}</mat-label>
<input matInput placeholder="{{ 'labelSearch' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
<mat-icon matSuffix>search</mat-icon>
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
</mat-form-field>
@ -26,7 +26,7 @@
<div *ngIf="!loading">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
<td mat-cell *matCellDef="let command">
<ng-container *ngIf="column.columnDef !== 'readOnly'">
{{ column.cell(command) }}
@ -41,7 +41,7 @@
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
<td mat-cell *matCellDef="let command" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
<button mat-icon-button color="info" (click)="viewDetails($event, command)"><mat-icon>visibility</mat-icon></button>
<button mat-icon-button color="primary" [disabled]="command.readOnly" (click)="editCommand($event, command)"><mat-icon>edit</mat-icon></button>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MatDialog } from '@angular/material/dialog';
@ -16,6 +18,7 @@ import { JoyrideService } from 'ngx-joyride';
styleUrls: ['./commands.component.css']
})
export class CommandsComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
private apiUrl: string;
dataSource = new MatTableDataSource<any>();
@ -29,22 +32,22 @@ export class CommandsComponent implements OnInit {
columns = [
{
columnDef: 'id',
header: 'ID',
header: 'labelId',
cell: (command: any) => `${command.id}`,
},
{
columnDef: 'name',
header: 'Nombre',
header: 'labelName',
cell: (command: any) => `${command.name}`
},
{
columnDef: 'readOnly',
header: 'Solo Lectura',
header: 'readOnly',
cell: (command: any) => `${command.readOnly}`
},
{
columnDef: 'createdAt',
header: 'Fecha de creación',
header: 'creationDate',
cell: (command: any) => `${this.datePipe.transform(command.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
}
];
@ -69,7 +72,7 @@ export class CommandsComponent implements OnInit {
this.loading = false;
},
(error) => {
console.error('Error fetching commands', error);
console.error(this.translate.instant('fetchingCommandsError'), error);
this.loading = false;
}
);
@ -106,11 +109,11 @@ export class CommandsComponent implements OnInit {
if (result) {
this.http.delete(`${this.apiUrl}/${command.uuid}`).subscribe({
next: () => {
this.toastService.success('Comando eliminado con éxito');
this.toastService.success(this.translate.instant('commandRemovedSuccessfully'));
this.search();
},
error: (error) => {
console.error('Error al eliminar el comando:', error);
console.error(this.translate.instant('errorDeletingCommand'), error);
}
});
}

View File

@ -2,28 +2,28 @@
<mat-dialog-content class="form-container">
<form [formGroup]="createCommandForm" (ngSubmit)="onSubmit()" class="command-form">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
<input matInput formControlName="name" placeholder="{{ 'commandNamePlaceholder' | translate }}" required>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'scriptLabel' | translate }}</mat-label>
<mat-label>{{ 'labelScript' | translate }}</mat-label>
<textarea matInput formControlName="script" placeholder="{{ 'commandScriptPlaceholder' | translate }}"></textarea>
</mat-form-field>
<div class="checkbox-group">
<mat-checkbox formControlName="readOnly">{{ 'readOnlyLabel' | translate }}</mat-checkbox>
<mat-checkbox formControlName="readOnly">{{ 'labelReadOnly' | translate }}</mat-checkbox>
<mat-checkbox formControlName="enabled">{{ 'enabledLabel' | translate }}</mat-checkbox>
<mat-checkbox formControlName="enabled">{{ 'labelEnabled' | translate }}</mat-checkbox>
<div class="checkbox-with-hint">
<mat-checkbox formControlName="parameters">{{ 'parameters' | translate }}</mat-checkbox>
<span class="hint-text">Si se selecciona esta opción los parámetros deben indicarse en el script con el símbolo &#64;.</span>
<mat-checkbox formControlName="parameters">{{ 'labelParameters' | translate }}</mat-checkbox>
<span class="hint-text">{{ 'scriptParameterHint' | translate }}</span>
</div>
</div>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
<mat-label>{{ 'labelComments' | translate }}</mat-label>
<textarea matInput formControlName="comments" placeholder="{{ 'commentsPlaceholder' | translate }}"></textarea>
</mat-form-field>
</form>
@ -31,5 +31,5 @@
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onCancel($event)">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="onSubmit()" cdkFocusInitial>{{ 'buttonSave' | translate }}</button>
<button class="submit-button" (click)="onSubmit()" cdkFocusInitial>{{ 'labelSaveButton' | translate }}</button>
</mat-dialog-actions>

View File

@ -1,4 +1,6 @@
import {Component, Inject, OnInit} from '@angular/core';
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http';
@ -12,6 +14,7 @@ import { ConfigService } from "@services/config.service";
styleUrls: ['./create-command.component.css']
})
export class CreateCommandComponent implements OnInit{
public translate = inject(TranslateService);
baseUrl: string;
createCommandForm: FormGroup<any>;
commandId: string | null = null;
@ -56,7 +59,7 @@ export class CreateCommandComponent implements OnInit{
this.commandId = response['@id'];
},
error: (err) => {
console.error('Error fetching remote calendar:', err);
console.error(this.translate.instant('errorFetchingRemoteCalendar'), err);
}
});
}
@ -81,7 +84,7 @@ export class CreateCommandComponent implements OnInit{
if (this.commandId) {
this.http.put(`${this.baseUrl}${this.commandId}`, payload).subscribe(
(response) => {
this.toastService.success('Comando editado correctamente');
this.toastService.success(this.translate.instant('commandEditedSuccessfully'));
this.dialogRef.close();
},
(error) => {
@ -91,7 +94,7 @@ export class CreateCommandComponent implements OnInit{
} else {
this.http.post(`${this.baseUrl}/commands`, payload).subscribe(
(response) => {
this.toastService.success('Comando añadido correctamente');
this.toastService.success(this.translate.instant('commandAddedSuccessfully'));
this.dialogRef.close();
},
(error) => {

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { ConfigService } from '@services/config.service';
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
@ -8,6 +10,7 @@ import { catchError, map } from 'rxjs/operators';
providedIn: 'root'
})
export class DataService {
public translate = inject(TranslateService);
baseUrl: string;
private apiUrl: string;
@ -34,7 +37,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching commands', error);
console.error(this.translate.instant('fetchingCommandsError'), error);
return throwError(error);
})
);
@ -50,7 +53,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching user group', error);
console.error(this.translate.instant('errorFetchingUserGroup'), error);
return throwError(error);
})
);

View File

@ -1,29 +1,29 @@
<div class="modal-container">
<h3 class="modal-title">{{ 'commandDetailsTitle' | translate }}</h3>
<div class="modal-content">
<p><strong>{{ 'nameLabel' | translate }}:</strong> {{ data.name }}</p>
<p><strong>{{ 'commentsLabel' | translate }}:</strong> {{ data.comments }}</p>
<p><strong>{{ 'createdByLabel' | translate }}:</strong> {{ data.createdBy }}</p>
<p><strong>{{ 'creationDateLabel' | translate }}:</strong> {{ data.createdAt | date:'medium' }}</p>
<p><strong>{{ 'labelRoleName' | translate }}:</strong> {{ data.name }}</p>
<p><strong>{{ 'labelComments' | translate }}:</strong> {{ data.comments }}</p>
<p><strong>{{ 'labelCreatedBy' | translate }}:</strong> {{ data.createdBy }}</p>
<p><strong>{{ 'labelCreationDate' | translate }}:</strong> {{ data.createdAt | date:'medium' }}</p>
<div class="script-display">
<h4>{{ 'scriptLabel' | translate }}:</h4>
<h4>{{ 'labelScript' | translate }}:</h4>
<pre>{{ data.script }}</pre>
</div>
</div>
<div class="additional-section" *ngIf="showClientSelect">
<form [formGroup]="form">
<h4>{{ 'selectClientsTitle' | translate }}</h4>
<h4>{{ 'selectClients' | translate }}</h4>
<mat-form-field appearance="fill">
<mat-label>{{ 'clientsLabel' | translate }}</mat-label>
<mat-label>{{ 'labelClients' | translate }}</mat-label>
<mat-select formControlName="selectedClients" multiple (selectionChange)="onClientSelectionChange($event)">
<mat-option *ngFor="let client of clients" [value]="client.uuid">
{{ client.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="form.get('selectedClients')?.invalid && form.get('selectedClients')?.touched">
{{ 'selectAtLeastOneClientError' | translate }}
{{ 'selectAtLeastOneClient' | translate }}
</mat-error>
</mat-form-field>
</form>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
@ -12,6 +14,7 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./command-detail.component.css']
})
export class CommandDetailComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
form!: FormGroup;
clients: any[] = [];
@ -56,7 +59,7 @@ export class CommandDetailComponent implements OnInit {
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastService.success('Comando editado');
this.toastService.success(this.translate.instant('editedCommand'));
this.data.command = result;
}
});

View File

@ -1,4 +1,4 @@
<h2 mat-dialog-title> Seleccionar particion para arrancar SO</h2>
<h2 mat-dialog-title> {{ 'selectPartitionToBootOs' | translate }}</h2>
<mat-dialog-content class="dialog-content">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
@ -6,10 +6,9 @@
<div *ngIf="!loading" class="select-container">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title>
<mat-panel-title> {{ 'labelClients' | translate }}</mat-panel-title>
<mat-panel-description>
Listado de clientes para arrancar un SO
<mat-icon>desktop_windows</mat-icon>
{{ 'clientListBootOs' | translate }}<mat-icon>desktop_windows</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header>
@ -42,8 +41,7 @@
color="primary"
[disabled]="!client.selected"
(click)="$event.stopPropagation()">
Modelo
</mat-radio-button>
{{ 'labelModel' | translate }}</mat-radio-button>
</mat-radio-group>
</div>
</div>
@ -57,7 +55,7 @@
<div *ngIf="!loading" class="partition-table-container">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar partición</th>
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">{{ 'buttonSelectPartition' | translate }}</th>
<td mat-cell *matCellDef="let row">
<mat-radio-group
[(ngModel)]="selectedPartition"
@ -70,7 +68,7 @@
</ng-container>
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef !== 'size'">
{{ column.cell(image) }}
@ -95,6 +93,6 @@
</mat-dialog-content>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">Cancelar</button>
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">{{ 'buttonExecute' | translate }}</button>
</div>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { MatTableDataSource } from "@angular/material/table";
@ -11,6 +13,7 @@ import { ToastrService } from "ngx-toastr";
styleUrl: './boot-so-partition.component.css'
})
export class BootSoPartitionComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
selectedPartition: any = null;
dataSource = new MatTableDataSource<any>();
@ -24,32 +27,32 @@ export class BootSoPartitionComponent implements OnInit {
columns = [
{
columnDef: 'diskNumber',
header: 'Disco',
header: 'labelDisk',
cell: (partition: any) => partition.diskNumber
},
{
columnDef: 'partitionNumber',
header: 'Particion',
header: 'partition',
cell: (partition: any) => partition.partitionNumber
},
{
columnDef: 'size',
header: 'Tamaño',
header: 'labelDiskSize',
cell: (partition: any) => `${partition.size} MB`
},
{
columnDef: 'partitionCode',
header: 'Tipo de partición',
header: 'partitionType',
cell: (partition: any) => partition.partitionCode
},
{
columnDef: 'filesystem',
header: 'Sistema de ficheros',
header: 'fileSystem',
cell: (partition: any) => partition.filesystem
},
{
columnDef: 'operativeSystem',
header: 'SO',
header: 'sW',
cell: (partition: any) => partition.operativeSystem?.name
}
];
@ -92,7 +95,7 @@ export class BootSoPartitionComponent implements OnInit {
}
},
(error) => {
console.error('Error al cargar los datos del cliente:', error);
console.error(this.translate.instant('errorLoadingData1'), error);
}
);
}
@ -133,7 +136,7 @@ export class BootSoPartitionComponent implements OnInit {
partition: this.selectedPartition['@id']
}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
this.toastService.success(this.translate.instant('clientUpdatedSuccessfully'));
this.dialogRef.close();
this.loading = false;
},

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, Input, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from "@angular/router";
@ -15,6 +17,7 @@ import {SoftwareProfilePartitionComponent} from "./software-profile-partition/so
styleUrls: ['./execute-command.component.css']
})
export class ExecuteCommandComponent implements OnInit {
public translate = inject(TranslateService);
@Input() runScriptContext: any = null;
@Input() clientState: string = 'off';
@Input() clientData: any[] = [];
@ -197,7 +200,7 @@ export class ExecuteCommandComponent implements OnInit {
clients: this.clientData.map((client: any) => client['@id'])
}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
this.toastService.success(this.translate.instant('clientUpdatedSuccessfully'));
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
@ -210,7 +213,7 @@ export class ExecuteCommandComponent implements OnInit {
clients: this.clientData.map((client: any) => client['@id'])
}).subscribe(
response => {
this.toastService.success('Petición de refresco enviada correctamente');
this.toastService.success(this.translate.instant('refreshmentRequestSentCorrectly'));
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
@ -237,7 +240,7 @@ export class ExecuteCommandComponent implements OnInit {
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastService.success('Petición de arranque de SO enviada correctamente');
this.toastService.success(this.translate.instant('oSBootRequest'));
}
});
}
@ -261,14 +264,14 @@ export class ExecuteCommandComponent implements OnInit {
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastService.success('Petición de borrado de caché de imagen enviada correctamente');
this.toastService.success(this.translate.instant('cacheClearRequest'));
}
});
}
hardwareInventory(): void {
if (this.clientData.length === 0) {
this.toastService.error('No hay clientes seleccionados');
this.toastService.error(this.translate.instant('noClientsSelected'));
return;
}
@ -278,7 +281,7 @@ export class ExecuteCommandComponent implements OnInit {
clients: this.clientData.map((client: any) => client['@id'])
}).subscribe(
response => {
this.toastService.success('Inventario de hardware actualizado correctamente');
this.toastService.success(this.translate.instant('hardwareInventorySuccessfullyUpdated'));
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
@ -288,7 +291,7 @@ export class ExecuteCommandComponent implements OnInit {
softwareInventory(): void {
if (this.clientData.length === 0) {
this.toastService.error('No hay clientes seleccionados');
this.toastService.error(this.translate.instant('noClientsSelected'));
return;
}
@ -320,7 +323,7 @@ export class ExecuteCommandComponent implements OnInit {
clients: this.clientData.map((client: any) => client['@id'])
}).subscribe(
response => {
this.toastService.success('Petición de encendido enviada correctamente');
this.toastService.success(this.translate.instant('ignitionRequestSentCorrectly'));
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
@ -333,7 +336,7 @@ export class ExecuteCommandComponent implements OnInit {
clients: this.clientData.map((client: any) => client['@id'])
}).subscribe(
response => {
this.toastService.success('Petición de apagado enviada correctamente');
this.toastService.success(this.translate.instant('shutdownRequestSentCorrectly'));
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error de conexión con el cliente');
@ -404,7 +407,7 @@ export class ExecuteCommandComponent implements OnInit {
deleteClientLease(): void {
if (this.clientData.length === 0) {
this.toastService.error('No hay clientes seleccionados');
this.toastService.error(this.translate.instant('noClientsSelected'));
return;
}
@ -415,11 +418,11 @@ export class ExecuteCommandComponent implements OnInit {
Promise.allSettled(requests).then(results => {
const errors = results.filter(r => r.status === 'rejected');
if (errors.length === 0) {
this.toastService.success('Lease(s) DHCP eliminada(s) correctamente');
this.toastService.success(this.translate.instant('dhcpLeasesRemovedSuccessfully'));
} else if (errors.length < results.length) {
this.toastService.warning(`${results.length - errors.length} lease(s) eliminada(s), ${errors.length} con error`);
} else {
this.toastService.error('Error al eliminar las leases DHCP');
this.toastService.error(this.translate.instant('errorWhenDeletingLeases'));
}
});
}

View File

@ -1,4 +1,4 @@
<h2 mat-dialog-title> Seleccionar imagen para eliminar de la cache</h2>
<h2 mat-dialog-title> {{ 'selectImageToDeleteFromCache' | translate }}</h2>
<mat-dialog-content class="dialog-content">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
@ -6,10 +6,9 @@
<div *ngIf="!loading" class="select-container">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title>
<mat-panel-title> {{ 'labelClients' | translate }}</mat-panel-title>
<mat-panel-description>
Listado de clientes para arrancar un SO
<mat-icon>desktop_windows</mat-icon>
{{ 'clientListBootOs' | translate }}<mat-icon>desktop_windows</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header>
@ -42,8 +41,7 @@
color="primary"
[disabled]="!client.selected"
(click)="$event.stopPropagation()">
Modelo
</mat-radio-button>
{{ 'labelModel' | translate }}</mat-radio-button>
</mat-radio-group>
</div>
</div>
@ -57,7 +55,7 @@
<div *ngIf="!loading" class="partition-table-container">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar imagen</th>
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">{{ 'buttonSelectImage' | translate }}</th>
<td mat-cell *matCellDef="let row">
<mat-radio-group
[(ngModel)]="selectedPartition"
@ -70,7 +68,7 @@
</ng-container>
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef !== 'size' && column.columnDef !== 'operativeSystem'">
{{ column.cell(image) }}
@ -100,6 +98,6 @@
</mat-dialog-content>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">Cancelar</button>
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">{{ 'buttonExecute' | translate }}</button>
</div>

View File

@ -1,4 +1,6 @@
import {Component, Inject} from '@angular/core';
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {Component, Inject } from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ConfigService} from "@services/config.service";
@ -11,6 +13,7 @@ import {ToastrService} from "ngx-toastr";
styleUrl: './remove-cache-image.component.css'
})
export class RemoveCacheImageComponent {
public translate = inject(TranslateService);
baseUrl: string;
selectedPartition: any = null;
dataSource = new MatTableDataSource<any>();
@ -24,32 +27,32 @@ export class RemoveCacheImageComponent {
columns = [
{
columnDef: 'diskNumber',
header: 'Disco',
header: 'labelDisk',
cell: (partition: any) => partition.diskNumber
},
{
columnDef: 'partitionNumber',
header: 'Particion',
header: 'partition',
cell: (partition: any) => partition.partitionNumber
},
{
columnDef: 'size',
header: 'Tamaño',
header: 'labelDiskSize',
cell: (partition: any) => `${partition.size} MB`
},
{
columnDef: 'partitionCode',
header: 'Tipo de partición',
header: 'partitionType',
cell: (partition: any) => partition.partitionCode
},
{
columnDef: 'filesystem',
header: 'Sistema de ficheros',
header: 'fileSystem',
cell: (partition: any) => partition.filesystem
},
{
columnDef: 'operativeSystem',
header: 'SO',
header: 'sW',
cell: (partition: any) => partition.operativeSystem?.name
}
];
@ -100,7 +103,7 @@ export class RemoveCacheImageComponent {
}
},
(error) => {
console.error('Error al cargar los datos del cliente:', error);
console.error(this.translate.instant('errorLoadingData1'), error);
}
);
}
@ -141,7 +144,7 @@ export class RemoveCacheImageComponent {
partition: this.selectedPartition['@id']
}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
this.toastService.success(this.translate.instant('clientUpdatedSuccessfully'));
this.dialogRef.close();
this.loading = false;
},

View File

@ -1,4 +1,4 @@
<h2 mat-dialog-title> Seleccionar partición para inventariar</h2>
<h2 mat-dialog-title> {{ 'selectPartitionToInventory' | translate }}</h2>
<mat-dialog-content class="dialog-content">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
@ -8,7 +8,7 @@
<div *ngIf="!loading" class="partition-table-container">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar imagen</th>
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">{{ 'buttonSelectImage' | translate }}</th>
<td mat-cell *matCellDef="let row">
<mat-radio-group
[(ngModel)]="selectedPartition"
@ -20,7 +20,7 @@
</ng-container>
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef !== 'size' && column.columnDef !== 'operativeSystem'">
{{ column.cell(image) }}
@ -50,6 +50,6 @@
</mat-dialog-content>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">Cancelar</button>
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">{{ 'buttonExecute' | translate }}</button>
</div>

View File

@ -1,4 +1,6 @@
import {Component, Inject, OnInit} from '@angular/core';
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {Component, Inject, OnInit } from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ConfigService} from "@services/config.service";
@ -11,6 +13,7 @@ import {ToastrService} from "ngx-toastr";
styleUrl: './software-profile-partition.component.css'
})
export class SoftwareProfilePartitionComponent implements OnInit{
public translate = inject(TranslateService);
baseUrl: string;
selectedPartition: any = null;
dataSource = new MatTableDataSource<any>();
@ -24,32 +27,32 @@ export class SoftwareProfilePartitionComponent implements OnInit{
columns = [
{
columnDef: 'diskNumber',
header: 'Disco',
header: 'labelDisk',
cell: (partition: any) => partition.diskNumber
},
{
columnDef: 'partitionNumber',
header: 'Particion',
header: 'partition',
cell: (partition: any) => partition.partitionNumber
},
{
columnDef: 'size',
header: 'Tamaño',
header: 'labelDiskSize',
cell: (partition: any) => `${partition.size} MB`
},
{
columnDef: 'partitionCode',
header: 'Tipo de partición',
header: 'partitionType',
cell: (partition: any) => partition.partitionCode
},
{
columnDef: 'filesystem',
header: 'Sistema de ficheros',
header: 'fileSystem',
cell: (partition: any) => partition.filesystem
},
{
columnDef: 'operativeSystem',
header: 'SO',
header: 'sW',
cell: (partition: any) => partition.operativeSystem?.name
}
];
@ -80,7 +83,7 @@ export class SoftwareProfilePartitionComponent implements OnInit{
}
},
(error) => {
console.error('Error al cargar los datos del cliente:', error);
console.error(this.translate.instant('errorLoadingData1'), error);
}
);
}
@ -96,7 +99,7 @@ export class SoftwareProfilePartitionComponent implements OnInit{
partition: this.selectedPartition['@id'],
}).subscribe(
response => {
this.toastService.success('Inventario de software actualizado correctamente');
this.toastService.success(this.translate.instant('successfullyUpdatedSoftwareInventory'));
this.dialogRef.close(response);
},
error => {

View File

@ -1,6 +1,6 @@
/* ===== HEADER DE BIENVENIDA ===== */
.welcome-header {
background: #3f51b5;
background: linear-gradient(120deg, #3b3080 0%, #2d2b55 50%, #1e1e2f 100%);
color: white;
padding: 1.2rem 1.5rem;
display: flex;
@ -468,7 +468,7 @@ mat-dialog-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
padding: 0.6rem 1.25rem;
background: white;
border-top: 1px solid #e9ecef;
border-radius: 0 0 12px 12px;

View File

@ -5,26 +5,12 @@
</div>
<div class="welcome-text">
<h1 class="welcome-title">{{ 'GlobalStatus' | translate }}</h1>
<p class="welcome-subtitle">Bienvenido a la consola de administración de OpenGnsys</p>
<p class="welcome-subtitle">{{ 'welcomeTitle' | translate }}</p>
<p class="welcome-description">{{ 'generalPlatformStatus' | translate }}</p>
</div>
</div>
<div class="welcome-actions">
@if (releaseInfo) {
<div class="release-panel">
@if (releaseChannel) {
<span class="release-channel">{{ releaseChannel }}</span>
}
<div class="release-chips">
@for (pkg of releasePackages; track pkg.key) {
<span class="release-chip" [matTooltip]="pkg.value">
<span class="chip-name">{{ pkg.key }}</span>
<span class="chip-version">{{ pkg.value.split('~')[0] }}</span>
</span>
}
</div>
</div>
}
<button mat-icon-button class="refresh-button" (click)="refreshAll()" matTooltip="Actualizar datos">
<button mat-icon-button class="refresh-button" (click)="refreshAll()" [matTooltip]="'buttonUpdateData' | translate">
<mat-icon>refresh</mat-icon>
</button>
</div>
@ -35,12 +21,12 @@
<div class="spinner-container">
<div class="loading-content">
<mat-spinner class="loading-spinner"></mat-spinner>
<p class="loading-text">Cargando estado del sistema...</p>
<p class="loading-text">{{ 'loadingSystemStatus' | translate }}</p>
</div>
</div>
}
@if(!loading){
@if(!loading) {
<div class="main-content">
<div class="system-overview">
<div class="overview-card clickable" [ngClass]="{'active': selectedSection === 'repositories'}"
@ -49,8 +35,8 @@
<mat-icon>cloud</mat-icon>
</div>
<div class="overview-content">
<h3>Repositorios</h3>
<p>Total: <span class="status-badge info">{{ repositories.length }}</span></p>
<h3>{{ 'labelRepository' | translate }}</h3>
<p>{{ 'labelTotalColon' | translate }}<span class="status-badge info">{{ repositories.length }}</span></p>
</div>
</div>
@ -60,13 +46,8 @@
<mat-icon>storage</mat-icon>
</div>
<div class="overview-content">
<h3>OgBoot Server</h3>
@if(!errorOgBoot){
<p>Estado: <span class="status-badge online">Operativo</span></p>
}
@else{
<p>Estado: <span class="status-badge offline">Error</span></p>
}
<h3>{{ 'ogbootServers' | translate }}</h3>
<p>{{ 'labelTotalColon' | translate }}<span class="status-badge info">{{ bootServers.length }}</span></p>
</div>
</div>
@ -76,8 +57,8 @@
<mat-icon>router</mat-icon>
</div>
<div class="overview-content">
<h3>Servidores DHCP</h3>
<p>Total: <span class="status-badge info">{{ dhcps.length }}</span></p>
<h3>{{ 'dhcpServers' | translate }}</h3>
<p>{{ 'labelTotalColon' | translate }}<span class="status-badge info">{{ dhcps.length }}</span></p>
</div>
</div>
@ -87,252 +68,31 @@
<mat-icon>dns</mat-icon>
</div>
<div class="overview-content">
<h3>OgCore Server</h3>
<p>Logs del servidor</p>
<h3>{{ 'ogCoreServer' | translate }}</h3>
<p>{{ 'serverLogs' | translate }}</p>
</div>
</div>
</div>
<div class="dynamic-content">
<div *ngIf="selectedSection === 'repositories'" class="section-content">
<div class="repositories-container">
<div *ngIf="repositories.length === 0" class="no-repositories">
<mat-icon class="no-data-icon">cloud_off</mat-icon>
<h3>No hay repositorios disponibles</h3>
<p>No se encontraron repositorios configurados en el sistema.</p>
</div>
<div *ngIf="repositories.length > 0" class="repositories-selector-container">
<div class="repository-selector">
<mat-form-field appearance="outline" class="repository-select-field">
<mat-label>Seleccionar repositorio</mat-label>
<mat-select [(ngModel)]="selectedRepositoryUuid" (selectionChange)="onRepositoryChange($event.value)">
<mat-option *ngFor="let repository of repositories" [value]="repository.uuid">
{{ repository.name }}
</mat-option>
</mat-select>
<mat-icon matSuffix>storage</mat-icon>
</mat-form-field>
</div>
<div
*ngIf="selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]"
class="selected-repository-content">
<div class="repository-item">
<div class="section-header">
<h2>{{ getSelectedRepositoryName() }}</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>Ver logs</span>
</button>
</div>
<div class="repository-content">
<app-status-tab [loading]="loading" [diskUsage]="repositoryStatuses[selectedRepositoryUuid].disk"
[servicesStatus]="repositoryStatuses[selectedRepositoryUuid].services"
[processesStatus]="repositoryStatuses[selectedRepositoryUuid].processes"
[ramUsage]="repositoryStatuses[selectedRepositoryUuid].ram"
[cpuUsage]="repositoryStatuses[selectedRepositoryUuid].cpu" [diskUsageChartData]="[
{ name: 'Usado', value: repositoryStatuses[selectedRepositoryUuid].disk.used },
{ name: 'Disponible', value: repositoryStatuses[selectedRepositoryUuid].disk.available }
]" [ramUsageChartData]="[
{ name: 'Usado', value: repositoryStatuses[selectedRepositoryUuid].ram.used },
{ name: 'Disponible', value: repositoryStatuses[selectedRepositoryUuid].ram.available }
]" [view]="view" [colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels"
[isDhcp]="false" [isRepository]="true">
</app-status-tab>
</div>
</div>
<div class="repository-logs-section" id="repository-logs-section">
<div class="logs-header">
<h3>Datos del repositorio: {{ getSelectedRepositoryName() }}</h3>
<p>Información específica del repositorio seleccionado</p>
</div>
<div class="logs-container">
<iframe [src]="getRepositoryIframeUrl()" width="100%" height="600" frameborder="0"
class="repository-logs-iframe">
</iframe>
</div>
</div>
</div>
<div *ngIf="!selectedRepositoryUuid" class="no-repository-selected">
<mat-icon class="no-data-icon">storage</mat-icon>
<h3>Selecciona un repositorio</h3>
<p>Elige un repositorio de la lista para ver su estado detallado.</p>
</div>
<div *ngIf="selectedRepositoryUuid && errorRepositories[selectedRepositoryUuid]" class="error-container">
<div class="error-card">
<mat-icon class="error-icon">error_outline</mat-icon>
<h3>Error de conexión</h3>
<p>No se pudo conectar con el repositorio seleccionado</p>
<button mat-raised-button color="primary" (click)="retryRepositoryStatus(selectedRepositoryUuid)">
<mat-icon>refresh</mat-icon>
Reintentar
</button>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="selectedSection === 'ogboot'" class="section-content">
<div *ngIf="!errorOgBoot && !loadingOgBoot" class="tab-content">
<div class="section-header">
<h2>OgBoot Server</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>Ver logs</span>
</button>
</div>
<app-status-tab [loading]="loadingOgBoot" [diskUsage]="ogBootDiskUsage"
[servicesStatus]="ogBootServicesStatus" [installedOgLives]="installedOgLives"
[diskUsageChartData]="ogBootDiskUsageChartData" [view]="view" [colorScheme]="colorScheme"
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="isDhcp" [isRepository]="false">
</app-status-tab>
<div class="logs-section" id="ogboot-logs-section">
<div class="logs-header">
<h3>Logs de OgBoot</h3>
<p>Logs en tiempo real del servidor OgBoot</p>
</div>
<div class="logs-container">
<iframe [src]="getOgBootIframeUrl()" width="100%" height="800" frameborder="0" class="ogboot-logs-iframe">
</iframe>
</div>
</div>
</div>
<div *ngIf="loadingOgBoot" class="loading-container">
<div class="loading-content">
<mat-spinner class="loading-spinner"></mat-spinner>
<p class="loading-text">Cargando estado de OgBoot...</p>
</div>
</div>
<div *ngIf="errorOgBoot" class="error-container">
<div class="error-card">
<mat-icon class="error-icon">error_outline</mat-icon>
<h3>Error de conexión</h3>
<p>No se pudo conectar con el servidor OgBoot</p>
<button mat-raised-button color="primary" (click)="loadOgBootStatus()">
<mat-icon>refresh</mat-icon>
Reintentar
</button>
</div>
</div>
</div>
@if(selectedSection === 'dhcp'){
<div class="section-content">
<div class="repositories-container">
@if(dhcps.length === 0){
<div class="no-repositories">
<mat-icon class="no-data-icon">cloud_off</mat-icon>
<h3>No hay dhcps disponibles</h3>
<p>No se encontraron dhcps configurados en el sistema.</p>
</div>
}
@else {
<div class="repositories-selector-container">
<div class="repository-selector">
<mat-form-field appearance="outline" class="repository-select-field">
<mat-label>Seleccionar dhcp</mat-label>
<mat-select [(ngModel)]="selectedDhcpUuid" (selectionChange)="onDhcpChange($event.value)">
<mat-option *ngFor="let dhcp of dhcps" [value]="dhcp.uuid">
{{ dhcp.name }}
</mat-option>
</mat-select>
<mat-icon matSuffix>storage</mat-icon>
</mat-form-field>
</div>
@if(selectedDhcpUuid && !errorDhcps[selectedDhcpUuid] && dhcpStatuses[selectedDhcpUuid]){
<div class="selected-repository-content">
<div class="repository-item">
<div class="section-header">
<h2>{{ getSelectedDhcpName() }}</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>Ver logs</span>
</button>
</div>
<div class="repository-content">
<app-status-tab [loading]="loadingDhcps" [diskUsage]="dhcpStatuses[selectedDhcpUuid].disk"
[servicesStatus]="dhcpStatuses[selectedDhcpUuid].services" [subnets]="subnets"
[diskUsageChartData]="dhcpStatuses[selectedDhcpUuid].diskUsageChartData" [view]="view"
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="true"
[isRepository]="false">
</app-status-tab>
</div>
</div>
<div class="logs-section" id="dhcp-logs-section">
<div class="logs-header">
<h3>Logs de DHCP</h3>
<p>Logs en tiempo real del servidor DHCP</p>
</div>
<div class="logs-container">
<iframe [src]="getDhcpIframeUrl()" width="100%" height="800" frameborder="0" class="dhcp-logs-iframe">
</iframe>
</div>
</div>
</div>
}
<div *ngIf="!selectedDhcpUuid" class="no-repository-selected">
<mat-icon class="no-data-icon">storage</mat-icon>
<h3>Selecciona un dhcp</h3>
<p>Elige un dhcp de la lista para ver su estado detallado.</p>
</div>
<div *ngIf="selectedDhcpUuid && errorDhcps[selectedDhcpUuid]" class="error-container">
<div class="error-card">
<mat-icon class="error-icon">error_outline</mat-icon>
<h3>Error de conexión</h3>
<p>No se pudo conectar con el dhcp seleccionado</p>
<button mat-raised-button color="primary" (click)="retryDhcpStatus(selectedDhcpUuid)">
<mat-icon>refresh</mat-icon>
Reintentar
</button>
</div>
</div>
</div>
}
</div>
</div>
@if(selectedSection === 'repositories') {
<app-ogrepository-status [repositories]="repositories"></app-ogrepository-status>
}
@if(selectedSection === 'ogboot') {
<app-global-ogboot-status [bootServers]="bootServers"></app-global-ogboot-status>
}
@if(selectedSection === 'dhcp') {
<app-ogdhcp-status [dhcps]="dhcps"></app-ogdhcp-status>
}
<div *ngIf="selectedSection === 'ogcore'" class="section-content">
<div class="tab-content">
<div class="section-header">
<h2>OgCore Server</h2>
</div>
<div class="logs-section" id="ogcore-logs-section">
<div class="logs-header">
<h3>Logs de OgCore</h3>
<p>Logs en tiempo real del servidor OgCore</p>
</div>
<div class="logs-container">
<iframe [src]="getOgCoreIframeUrl()" width="100%" height="800" frameborder="0" class="ogcore-logs-iframe">
</iframe>
</div>
</div>
</div>
</div>
@if(selectedSection === 'ogcore') {
<app-ogcore-status></app-ogcore-status>
}
</div>
</div>
}
</mat-dialog-content>
<mat-dialog-actions class="action-container">
@ -342,10 +102,9 @@
<div class="action-buttons">
<button mat-button class="secondary-button" (click)="refreshAll()">
<mat-icon>refresh</mat-icon>
Actualizar
</button>
{{ 'buttonUpdate' | translate }}</button>
<button mat-raised-button color="primary" [mat-dialog-close]="true">
{{ 'closeButton' | translate }}
{{ 'buttonClose' | translate }}
</button>
</div>
</mat-dialog-actions>

View File

@ -1,8 +1,9 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ConfigService } from '@services/config.service';
import { ToastrService } from "ngx-toastr";
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-global-status',
@ -10,15 +11,17 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
styleUrl: './global-status.component.css'
})
export class GlobalStatusComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
loading: boolean = false;
loadingOgBoot: boolean = false;
loadingDhcps: boolean = false;
loadingRepositories: boolean = false;
errorOgBoot: boolean = false;
loadingBootServers: boolean = false;
errorDhcps: { [key: string]: boolean } = {};
errorRepositories: { [key: string]: boolean } = {};
installedOgLives: any[] = [];
errorBootServers: { [key: string]: boolean } = {};
subnets: any[] = [];
showLabels: boolean = true;
isDoughnut: boolean = true;
@ -26,26 +29,28 @@ export class GlobalStatusComponent implements OnInit {
domain: ['#df200d', '#26a700']
};
view: [number, number] = [400, 220];
repositoriesUrl: string;
repositories: any[] = [];
repositoryStatuses: { [key: string]: any } = {};
lastUpdateTime: string = '';
selectedRepositoryUuid: string = '';
selectedSection: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore' = 'ogcore';
ogBootApiUrl: string;
ogBootDiskUsage: any = {};
ogBootServicesStatus: any = {};
ogBootDiskUsageChartData: any[] = [];
dhcpsApiUrl: string;
dhcps: any[] = [];
dhcpDiskUsage: any = {};
dhcpStatuses: { [key: string]: any } = {};
dhcpDiskUsageChartData: any[] = [];
selectedDhcpUuid: string = '';
isDhcp: boolean = false;
isRepository: boolean = false;
bootServersApiUrl: string;
bootServers: any[] = [];
bootServerStatuses: { [key: string]: any } = {};
selectedBootServerUuid: string = '';
lastUpdateTime: string = '';
ogCoreApiUrl: string;
@ -62,30 +67,28 @@ export class GlobalStatusComponent implements OnInit {
constructor(
private configService: ConfigService,
private toastService: ToastrService,
private http: HttpClient,
private sanitizer: DomSanitizer
private http: HttpClient
) {
this.baseUrl = this.configService.apiUrl;
this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
this.dhcpsApiUrl = `${this.baseUrl}/dhcp-servers`;
this.ogCoreApiUrl = `${this.baseUrl}`;
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
this.ogBootDiskUsageChartData = [];
this.dhcpDiskUsageChartData = [];
this.bootServersApiUrl = `${this.baseUrl}/boot-servers`;
this.ogCoreApiUrl = `${this.baseUrl}`;
}
ngOnInit(): void {
this.loading = true;
this.updateLastUpdateTime();
this.loadReleaseInfo();
this.loadOgBootStatus();
this.loadDhcps(false);
this.loadRepositories(false);
this.loadDhcps(true);
this.loadRepositories(true);
this.loadBootServers(true);
this.loadBootServers(false);
this.syncSubnets();
this.syncTemplates();
this.syncOgLives();
}
get releasePackages(): { key: string; value: string }[] {
@ -122,7 +125,7 @@ export class GlobalStatusComponent implements OnInit {
const timeoutId = setTimeout(() => {
if (!hasError) {
this.toastService.error('Error al sincronizar las subredes: tiempo de espera agotado');
this.toastService.error(this.translate.instant('errorSyncingSubnets'));
hasError = true;
}
}, 5000);
@ -133,7 +136,7 @@ export class GlobalStatusComponent implements OnInit {
pending--;
if (pending === 0 && !hasError) {
clearTimeout(timeoutId);
this.toastService.success('Sincronización de subredes de servidores DHCP exitosa');
this.toastService.success(this.translate.instant('serverSubnetSynchronization'));
}
},
error: (error) => {
@ -147,403 +150,151 @@ export class GlobalStatusComponent implements OnInit {
});
});
}, error => {
console.error('Error fetching DHCP servers for subnet sync', error);
console.error(this.translate.instant('errorFetchingDhcpServersFor'), error);
});
}
syncTemplates() {
const timeoutId = setTimeout(() => {
this.toastService.error('Error al sincronizar las plantillas Pxe: tiempo de espera agotado');
}, 3500);
if (this.bootServers.length === 0) return;
this.http.post(`${this.baseUrl}/pxe-templates/sync`, {})
.subscribe(response => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización de las plantillas Pxe completada');
}, error => {
clearTimeout(timeoutId);
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las plantillas Pxe');
let pending = this.bootServers.length;
let hasError = false;
const timeoutId = setTimeout(() => {
if (!hasError) {
this.toastService.error(this.translate.instant('errorSyncingTemplates'));
hasError = true;
}
}, 5000);
this.bootServers.forEach(server => {
this.http.post(`${this.baseUrl}/boot-servers/${server.uuid}/pxe-templates/sync`, {}).subscribe({
next: () => {
pending--;
if (pending === 0 && !hasError) {
clearTimeout(timeoutId);
this.toastService.success(this.translate.instant('successfulPxeTemplateSynchronization'));
}
},
error: (error) => {
pending--;
if (!hasError) {
hasError = true;
clearTimeout(timeoutId);
this.toastService.error(error.error?.['hydra:description'] || `Error al sincronizar plantillas Pxe del servidor ${server.name || server.uuid}`);
}
}
});
});
}
syncOgLives(): void {
const timeoutId = setTimeout(() => {
this.toastService.error('Error al sincronizar las imagenes ogLive : tiempo de espera agotado');
}, 3500);
if (this.bootServers.length === 0) return;
this.http.post(`${this.baseUrl}/og-lives/sync`, {})
.subscribe(response => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización con los ogLives completada');
}, error => {
clearTimeout(timeoutId);
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las imagenes ogLive');
let pending = this.bootServers.length;
let hasError = false;
const timeoutId = setTimeout(() => {
if (!hasError) {
this.toastService.error(this.translate.instant('errorWhenSynchronizingOgliveImages'));
hasError = true;
}
}, 5000);
this.bootServers.forEach(server => {
this.http.post(`${this.baseUrl}/boot-servers/${server.uuid}/oglives/sync`, {}).subscribe({
next: () => {
pending--;
if (pending === 0 && !hasError) {
clearTimeout(timeoutId);
this.toastService.success(this.translate.instant('synchronizationWithOglivesCompleted'));
}
},
error: (error) => {
pending--;
if (!hasError) {
hasError = true;
clearTimeout(timeoutId);
this.toastService.error(error.error?.['hydra:description'] || `Error al sincronizar ogLives del servidor ${server.name || server.uuid}`);
}
}
});
}
[key: string]: any;
loadStatus(apiUrl: string, diskUsage: any, servicesStatus: any, diskUsageChartData: any[], installedOgLives: any[], isDhcp: boolean, errorState: string, showLoading: boolean = true): void {
if (isDhcp) {
this.loadingDhcps = true;
} else {
this.loadingOgBoot = true;
}
if (showLoading) {
this.loading = true;
}
this[errorState] = false;
const timeoutId = setTimeout(() => {
if (showLoading) {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcps = false;
} else {
this.loadingOgBoot = false;
}
this[errorState] = true;
}, 3500);
this.http.get<any>(apiUrl).subscribe({
next: data => {
diskUsage.used = data.message.disk_usage.used;
diskUsage.available = data.message.disk_usage.available;
diskUsage.total = data.message.disk_usage.total;
diskUsage.percentage = data.message.disk_usage.percentage;
Object.assign(servicesStatus, data.message.services_status);
if (isDhcp) {
this.subnets.length = 0;
if (data.message.subnets) {
this.subnets.push(...data.message.subnets);
}
} else {
installedOgLives.length = 0;
if (data.message.installed_oglives) {
installedOgLives.push(...data.message.installed_oglives);
}
this.loadingOgBootOgLives = false;
}
diskUsageChartData.length = 0;
diskUsageChartData.push(
{ name: 'Usado', value: parseFloat(diskUsage.used) },
{ name: 'Disponible', value: parseFloat(diskUsage.available) }
);
if (showLoading) {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcps = false;
} else {
this.loadingOgBoot = false;
}
clearTimeout(timeoutId);
},
error: error => {
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado de ogBoot');
if (showLoading) {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcps = false;
} else {
this.loadingOgBoot = false;
}
this[errorState] = true;
clearTimeout(timeoutId);
}
});
}
loadRepositories(showLoading: boolean = true): void {
if (showLoading) {
this.loading = true;
}
this.errorRepositories = {};
this.http.get<any>(`${this.repositoriesUrl}?page=1&itemsPerPage=10`).subscribe(
data => {
if (showLoading) this.loadingRepositories = true;
this.http.get<any>(`${this.repositoriesUrl}?page=1&itemsPerPage=10`).subscribe({
next: data => {
this.repositories = data['hydra:member'] || [];
if (this.repositories.length === 0) {
if (showLoading) {
this.loading = false;
}
return;
}
let remainingRepositories = this.repositories.length;
const timeoutId = setTimeout(() => {
if (showLoading) {
this.loading = false;
}
this.repositories.forEach(repository => {
if (!(repository.uuid in this.errorRepositories)) {
this.errorRepositories[repository.uuid] = true;
}
});
}, 5000);
this.repositories.forEach(repository => {
this.loadRepositoryStatus(repository.uuid, (errorOccurred: boolean) => {
remainingRepositories--;
this.errorRepositories[repository.uuid] = errorOccurred;
if (remainingRepositories === 0) {
if (showLoading) {
this.loading = false;
}
clearTimeout(timeoutId);
}
});
});
if (showLoading) this.loadingRepositories = false;
this.checkOverallLoading();
},
error => {
this.loading = false;
error: () => {
this.repositories = [];
this.toastService.error('Error al cargar la lista de repositorios');
if (showLoading) this.loadingRepositories = false;
this.checkOverallLoading();
this.toastService.error(this.translate.instant('errorLoadingList2'));
}
);
});
}
loadRepositoryStatus(repositoryUuid: string, callback: (errorOccurred: boolean) => void): void {
console.log(`Cargando estado del repositorio: ${repositoryUuid}`);
const timeoutId = setTimeout(() => {
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/image-repositories/server/${repositoryUuid}/status`).subscribe(
data => {
if (!data.success) {
clearTimeout(timeoutId);
callback(true);
return;
loadBootServers(showLoading: boolean = true): void {
if (showLoading) this.loadingBootServers = true;
this.http.get<any>(`${this.bootServersApiUrl}?page=1&itemsPerPage=10`).subscribe({
next: data => {
this.bootServers = data['hydra:member'] || [];
if (showLoading) this.loadingBootServers = false;
if (this.bootServers.length > 0) {
this.syncTemplates();
this.syncOgLives();
}
const details = data.details;
const { disk, services, ram, cpu, processes } = details;
this.repositoryStatuses[repositoryUuid] = {
disk: {
...disk,
used: parseFloat(disk.used.replace('GB', '')),
available: parseFloat(disk.available.replace('GB', '')),
total: parseFloat(disk.total.replace('GB', '')),
percentage: disk.used_percentage
},
ram: {
...ram,
used: parseFloat(ram.used.replace('GB', '')),
available: parseFloat(ram.available.replace('GB', '')),
total: parseFloat(ram.total.replace('GB', '')),
percentage: ram.used_percentage
},
cpu: {
used_percentage: cpu.used_percentage
},
services: services,
processes: processes
};
clearTimeout(timeoutId);
callback(false);
this.checkOverallLoading();
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del repositorio');
clearTimeout(timeoutId);
callback(true);
error: () => {
this.bootServers = [];
if (showLoading) this.loadingBootServers = false;
this.checkOverallLoading();
this.toastService.error(this.translate.instant('errorLoadingList1'));
}
);
});
}
loadOgBootStatus(): void {
this.isDhcp = false;
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorOgBoot', false);
}
loadDhcps(showLoading: boolean = true): void {
if (showLoading) {
this.loading = true;
}
this.errorDhcps = {};
this.http.get<any>(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe(
data => {
if (showLoading) this.loadingDhcps = true;
this.http.get<any>(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe({
next: data => {
this.dhcps = data['hydra:member'] || [];
if (this.dhcps.length === 0) {
if (showLoading) {
this.loading = false;
}
return;
}
let remainingDhcps = this.dhcps.length;
const timeoutId = setTimeout(() => {
if (showLoading) {
this.loading = false;
}
this.dhcps.forEach(dhcp => {
if (!(dhcp.uuid in this.errorDhcps)) {
this.errorDhcps[dhcp.uuid] = true;
}
});
}, 5000);
this.dhcps.forEach(dhcp => {
this.loadDhcpStatus(dhcp.uuid, (errorOccurred: boolean) => {
remainingDhcps--;
this.errorDhcps[dhcp.uuid] = errorOccurred;
if (remainingDhcps === 0) {
if (showLoading) {
this.loading = false;
}
clearTimeout(timeoutId);
}
});
});
if (showLoading) this.loadingDhcps = false;
this.checkOverallLoading();
},
error => {
this.loading = false;
error: () => {
this.dhcps = [];
this.toastService.error('Error al cargar la lista de Dhcps');
if (showLoading) this.loadingDhcps = false;
this.checkOverallLoading();
this.toastService.error(this.translate.instant('errorLoadingList'));
}
);
});
}
loadDhcpStatus(dhcpUuid: string, callback: (errorOccurred: boolean) => void): void {
console.log(`Cargando estado del Dhcp: ${dhcpUuid}`);
const timeoutId = setTimeout(() => {
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/dhcp-servers/server/${dhcpUuid}/status`).subscribe(
data => {
if (!data.success) {
clearTimeout(timeoutId);
callback(true);
return;
}
const disk = data.message.disk_usage;
const services = data.message.services_status;
let diskUsageChartData: any[] = [];
diskUsageChartData.length = 0;
diskUsageChartData.push(
{ name: 'Usado', value: parseFloat(disk.used) },
{ name: 'Disponible', value: parseFloat(disk.available) }
);
this.subnets.length = 0;
if (data.message.subnets) {
this.subnets.push(...data.message.subnets);
}
this.dhcpStatuses[dhcpUuid] = {
disk: {
...disk,
used: parseFloat(disk.used.replace('GB', '')),
available: parseFloat(disk.available.replace('GB', '')),
total: parseFloat(disk.total.replace('GB', '')),
percentage: disk.used_percentage
},
services: services,
diskUsageChartData: diskUsageChartData
};
clearTimeout(timeoutId);
callback(false);
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del dhcp');
clearTimeout(timeoutId);
callback(true);
}
);
}
onDhcpChange(dhcpUuid: string): void {
this.selectedDhcpUuid = dhcpUuid;
if (dhcpUuid && !this.dhcpStatuses[dhcpUuid]) {
this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
if (errorOccurred) {
this.errorDhcps[dhcpUuid] = true;
}
});
checkOverallLoading(): void {
if (!this.loadingRepositories && !this.loadingBootServers && !this.loadingDhcps) {
this.loading = false;
}
}
getSelectedDhcpName(): string {
const selectedDhcp = this.dhcps.find(dhcp => dhcp.uuid === this.selectedDhcpUuid);
return selectedDhcp ? selectedDhcp.name : '';
}
onRepositoryChange(repositoryUuid: string): void {
this.selectedRepositoryUuid = repositoryUuid;
if (repositoryUuid && !this.repositoryStatuses[repositoryUuid]) {
this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
if (errorOccurred) {
this.errorRepositories[repositoryUuid] = true;
}
});
}
}
getSelectedRepositoryName(): string {
const selectedRepo = this.repositories.find(repo => repo.uuid === this.selectedRepositoryUuid);
return selectedRepo ? selectedRepo.name : '';
}
getRepositoryIframeUrl(): SafeResourceUrl {
const repositoryName = this.getSelectedRepositoryName();
if (repositoryName) {
const encodedName = encodeURIComponent(repositoryName);
const url = `${this.configService.logUrl}/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
return this.sanitizer.bypassSecurityTrustResourceUrl('');
}
getOgBootIframeUrl(): SafeResourceUrl {
const url = `${this.configService.logUrl}/d-solo/ogboot-logs/ogboot-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
getDhcpIframeUrl(): SafeResourceUrl {
const dhcpName = this.getSelectedDhcpName();
if (dhcpName) {
const encodedName = encodeURIComponent(dhcpName);
const url = `${this.configService.logUrl}/d-solo/ogdhcp-logs/ogdhcp-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
return this.sanitizer.bypassSecurityTrustResourceUrl('');
}
getOgCoreIframeUrl(): SafeResourceUrl {
const url = `${this.configService.logUrl}/d-solo/ogcore-logs/ogcore-logs?orgId=1&tab=transformations&theme=dark&panelId=1&__feature.dashboardSceneSolo&now-5m&to=now&timezone=browser`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
refreshAll(): void {
this.loading = true;
this.updateLastUpdateTime();
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, false, 'errorOgBoot', true);
this.loadBootServers(true);
this.loadDhcps(true);
this.loadRepositories(true);
this.syncSubnets();
this.syncTemplates();
this.syncOgLives();
this.toastService.success('Datos actualizados correctamente');
this.toastService.success(this.translate.instant('dataUpdatedCorrectly'));
}
updateLastUpdateTime(): void {
@ -564,88 +315,11 @@ export class GlobalStatusComponent implements OnInit {
});
}
retryRepositoryStatus(repositoryUuid: string): void {
this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
this.errorRepositories[repositoryUuid] = errorOccurred;
if (!errorOccurred) {
this.toastService.success('Estado del repositorio actualizado correctamente');
} else {
this.toastService.error('Error al cargar el estado del repositorio');
}
});
}
retryDhcpStatus(dhcpUuid: string): void {
this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
this.errorDhcps[dhcpUuid] = errorOccurred;
if (!errorOccurred) {
this.toastService.success('Estado del dhcp actualizado correctamente');
} else {
this.toastService.error('Error al cargar el estado del dhcp');
}
});
}
selectSection(section: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore'): void {
this.selectedSection = section;
switch (section) {
case 'repositories':
break;
case 'ogboot':
if (!this.ogBootDiskUsage) {
this.loadOgBootStatus();
}
break;
case 'dhcp':
break;
case 'ogcore':
break;
}
}
scrollToLogs(): void {
let logsSectionId: string;
switch (this.selectedSection) {
case 'repositories':
logsSectionId = 'repository-logs-section';
break;
case 'ogboot':
logsSectionId = 'ogboot-logs-section';
break;
case 'dhcp':
logsSectionId = 'dhcp-logs-section';
break;
case 'ogcore':
logsSectionId = 'ogcore-logs-section';
break;
default:
return;
}
const checkAndScroll = () => {
const logsSection = document.getElementById(logsSectionId);
if (logsSection) {
logsSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
return true;
}
return false;
};
const interval = setInterval(() => {
if (checkAndScroll()) {
clearInterval(interval);
}
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 3000);
}
}

View File

@ -0,0 +1,233 @@
.section-content {
padding: 0.75rem 1rem;
}
.repositories-container {
padding: 0;
}
.repositories-selector-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.repository-selector {
display: flex;
padding: 0.25rem 0 0;
}
.repository-select-field {
min-width: 260px;
max-width: 420px;
width: 100%;
}
.repository-content {
padding: 0;
}
.selected-repository-content {
animation: fadeInUp 0.3s ease-out;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding-bottom: 0.6rem;
border-bottom: 1px solid #e9ecef;
}
.section-header h2 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.4rem;
}
.section-header h2::before {
content: '';
width: 3px;
height: 18px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
flex-shrink: 0;
}
.logs-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.4rem 1rem;
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.35rem;
font-weight: 600;
font-size: 0.82rem;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
}
.logs-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(102, 126, 234, 0.35);
}
.logs-button mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
}
.logs-section {
margin-top: 0.5rem;
padding: 0.75rem 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07);
border: 1px solid #e9ecef;
}
.logs-header {
margin-bottom: 0.5rem;
}
.logs-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.4rem;
}
.logs-header h3::before {
content: '';
width: 3px;
height: 16px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
flex-shrink: 0;
}
.logs-header p {
display: none;
}
.logs-container {
background: #f8f9fa;
border-radius: 6px;
padding: 0.4rem;
border: 1px solid #e9ecef;
}
.ogboot-logs-iframe {
border: none;
border-radius: 4px;
display: block;
}
.no-repositories {
text-align: center;
padding: 2rem 1rem;
color: #6c757d;
}
.no-repositories .no-data-icon {
font-size: 2.5rem;
width: 2.5rem;
height: 2.5rem;
margin-bottom: 0.5rem;
opacity: 0.4;
}
.no-repositories h3 {
margin: 0 0 0.4rem;
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
}
.no-repositories p {
margin: 0;
font-size: 0.85rem;
}
.no-repository-selected {
text-align: center;
padding: 1.5rem 1rem;
color: #6c757d;
}
.no-repository-selected .no-data-icon {
font-size: 2.5rem;
width: 2.5rem;
height: 2.5rem;
margin-bottom: 0.5rem;
opacity: 0.4;
}
.no-repository-selected h3 {
margin: 0 0 0.4rem;
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
}
.no-repository-selected p {
margin: 0;
font-size: 0.85rem;
}
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 160px;
padding: 1rem;
}
.error-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
text-align: center;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #e9ecef;
max-width: 360px;
}
.error-icon {
font-size: 42px;
width: 42px;
height: 42px;
color: #dc3545;
margin-bottom: 0.5rem;
}
.error-card h3 {
margin: 0 0 0.4rem;
color: #2c3e50;
font-weight: 600;
font-size: 1rem;
}
.error-card p {
margin: 0 0 1rem;
color: #6c757d;
font-size: 0.85rem;
line-height: 1.4;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}

View File

@ -0,0 +1,81 @@
<div class="section-content">
<div class="repositories-container">
@if(bootServers.length === 0) {
<div class="no-repositories">
<mat-icon class="no-data-icon">cloud_off</mat-icon>
<h3>{{ 'noBootServersAvailable' | translate }}</h3>
<p>{{ 'noConfiguredBootServers' | translate }}</p>
</div>
} @else {<div class="repositories-selector-container">
<div class="repository-selector">
<mat-form-field appearance="outline" class="repository-select-field">
<mat-label>{{ 'selectOgBootServer' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedBootServerUuid" (selectionChange)="onBootServerChange($event.value)">
@for(server of bootServers; track server.uuid) {
<mat-option [value]="server.uuid">
{{ server.name }}
</mat-option>
}
</mat-select>
<mat-icon matSuffix>storage</mat-icon>
</mat-form-field>
</div>
@if(selectedBootServerUuid && !errorBootServers[selectedBootServerUuid] && bootServerStatuses[selectedBootServerUuid]) {
<div class="selected-repository-content">
<div class="repository-item">
<div class="section-header">
<h2>{{ getSelectedBootServerName() }}</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>{{ 'buttonViewLogs' | translate }}</span>
</button>
</div>
<div class="repository-content">
<app-status-tab [loading]="loadingBootServers" [diskUsage]="bootServerStatuses[selectedBootServerUuid].disk"
[servicesStatus]="bootServerStatuses[selectedBootServerUuid].services"
[installedOgLives]="bootServerStatuses[selectedBootServerUuid].installedOgLives"
[diskUsageChartData]="bootServerStatuses[selectedBootServerUuid].diskUsageChartData" [view]="view" [colorScheme]="colorScheme"
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="false" [isRepository]="false">
</app-status-tab>
</div>
</div>
<div class="logs-section" id="ogboot-logs-section">
<div class="logs-header">
<h3>{{ 'ogbootLogs' | translate }}</h3>
<p>{{ 'realTimeOgbootLogs' | translate }}</p>
</div>
<div class="logs-container">
<iframe [src]="getBootServerIframeUrl()" width="100%" height="800" frameborder="0" class="ogboot-logs-iframe">
</iframe>
</div>
</div>
</div>
}
@if(!selectedBootServerUuid) {
<div class="no-repository-selected">
<mat-icon class="no-data-icon">storage</mat-icon>
<h3>{{ 'selectABootServer' | translate }}</h3>
<p>{{ 'selectBootServerToView' | translate }}</p>
</div>
}
@if(selectedBootServerUuid && errorBootServers[selectedBootServerUuid]) {
<div class="error-container">
<div class="error-card">
<mat-icon class="error-icon">error_outline</mat-icon>
<h3>{{ 'connectionError' | translate }}</h3>
<p>{{ 'cannotConnectBootServer' | translate }}</p>
<button mat-raised-button color="primary" (click)="retryBootServerStatus(selectedBootServerUuid)">
<mat-icon>refresh</mat-icon>
{{ 'buttonRetry' | translate }}</button>
</div>
</div>
}
</div>
}
</div>
</div>

View File

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

View File

@ -0,0 +1,153 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit, Input } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ConfigService } from '@services/config.service';
import { ToastrService } from "ngx-toastr";
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-global-ogboot-status',
templateUrl: './ogboot-status.component.html',
styleUrl: './ogboot-status.component.css'
})
export class GlobalOgbootStatusComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
loadingBootServers: boolean = false;
@Input() bootServers: any[] = [];
bootServerStatuses: { [key: string]: any } = {};
selectedBootServerUuid: string = '';
errorBootServers: { [key: string]: boolean } = {};
showLabels: boolean = true;
isDoughnut: boolean = true;
colorScheme: any = {
domain: ['#df200d', '#26a700']
};
view: [number, number] = [400, 220];
constructor(
private configService: ConfigService,
private toastService: ToastrService,
private http: HttpClient,
private sanitizer: DomSanitizer
) {
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void {
}
loadBootServerStatus(bootServerUuid: string, callback: (errorOccurred: boolean) => void): void {
const timeoutId = setTimeout(() => {
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/boot-servers/server/${bootServerUuid}/status`).subscribe(
data => {
if (data.code && data.code !== 200) {
clearTimeout(timeoutId);
callback(true);
return;
}
const message = data.message || data;
const disk = message.disk_usage;
const services = message.services_status;
const installedOgLives = message.installed_oglives || [];
let diskUsageChartData: any[] = [];
diskUsageChartData.push(
{ name: 'Usado', value: parseFloat(disk.used) },
{ name: 'Disponible', value: parseFloat(disk.available) }
);
this.bootServerStatuses[bootServerUuid] = {
disk: {
...disk,
used: parseFloat(disk.used.replace('GB', '')),
available: parseFloat(disk.available.replace('GB', '')),
total: parseFloat(disk.total.replace('GB', '')),
percentage: disk.used_percentage || disk.percentage
},
services: services,
installedOgLives: installedOgLives,
diskUsageChartData: diskUsageChartData
};
clearTimeout(timeoutId);
callback(false);
},
error => {
this.toastService.error(error.error?.['hydra:description'] || 'Error al cargar el estado del servidor de boot');
clearTimeout(timeoutId);
callback(true);
}
);
}
onBootServerChange(bootServerUuid: string): void {
this.selectedBootServerUuid = bootServerUuid;
if (bootServerUuid && !this.bootServerStatuses[bootServerUuid]) {
this.loadBootServerStatus(bootServerUuid, (errorOccurred: boolean) => {
if (errorOccurred) {
this.errorBootServers[bootServerUuid] = true;
}
});
}
}
getSelectedBootServerName(): string {
const selectedBootServer = this.bootServers.find(server => server.uuid === this.selectedBootServerUuid);
return selectedBootServer ? selectedBootServer.name : '';
}
getBootServerIframeUrl(): SafeResourceUrl {
const bootServerName = this.getSelectedBootServerName();
if (bootServerName) {
const encodedName = encodeURIComponent(bootServerName);
const url = `${this.configService.logUrl}/d-solo/ogboot-logs/ogboot-logs?orgId=1&timezone=browser&refresh=5s&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
return this.sanitizer.bypassSecurityTrustResourceUrl('');
}
retryBootServerStatus(bootServerUuid: string): void {
this.loadBootServerStatus(bootServerUuid, (errorOccurred: boolean) => {
this.errorBootServers[bootServerUuid] = errorOccurred;
if (!errorOccurred) {
this.toastService.success(this.translate.instant('bootServerStatus'));
} else {
this.toastService.error(this.translate.instant('errorLoadingState2'));
}
});
}
scrollToLogs(): void {
const checkAndScroll = () => {
const logsSection = document.getElementById('ogboot-logs-section');
if (logsSection) {
logsSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
return true;
}
return false;
};
const interval = setInterval(() => {
if (checkAndScroll()) {
clearInterval(interval);
}
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 3000);
}
}

View File

@ -0,0 +1,56 @@
.section-content {
padding: 0.75rem 1rem;
}
.tab-content {
padding: 0;
}
.logs-section {
margin-top: 0.5rem;
padding: 0.75rem 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07);
border: 1px solid #e9ecef;
}
.logs-header {
margin-bottom: 0.5rem;
}
.logs-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.4rem;
}
.logs-header h3::before {
content: '';
width: 3px;
height: 16px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
flex-shrink: 0;
}
.logs-header p {
display: none;
}
.logs-container {
background: #f8f9fa;
border-radius: 6px;
padding: 0.4rem;
border: 1px solid #e9ecef;
}
.ogcore-logs-iframe {
border: none;
border-radius: 4px;
display: block;
}

View File

@ -0,0 +1,14 @@
<div class="section-content">
<div class="tab-content">
<div class="logs-section" id="ogcore-logs-section">
<div class="logs-header">
<h3>{{ 'ogcoreLogs' | translate }}</h3>
<p>{{ 'realTimeOgcoreLogs' | translate }}</p>
</div>
<div class="logs-container">
<iframe [src]="getOgCoreIframeUrl()" width="100%" height="800" frameborder="0" class="ogcore-logs-iframe">
</iframe>
</div>
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { ConfigService } from '@services/config.service';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-ogcore-status',
templateUrl: './ogcore-status.component.html',
styleUrl: './ogcore-status.component.css'
})
export class OgcoreStatusComponent {
constructor(
private configService: ConfigService,
private sanitizer: DomSanitizer
) {}
getOgCoreIframeUrl(): SafeResourceUrl {
const url = `${this.configService.logUrl}/d-solo/ogcore-logs/ogcore-logs?orgId=1&tab=transformations&theme=dark&panelId=1&__feature.dashboardSceneSolo&now-5m&to=now&timezone=browser`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
}

View File

@ -0,0 +1,233 @@
.section-content {
padding: 0.75rem 1rem;
}
.repositories-container {
padding: 0;
}
.repositories-selector-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.repository-selector {
display: flex;
padding: 0.25rem 0 0;
}
.repository-select-field {
min-width: 260px;
max-width: 420px;
width: 100%;
}
.repository-content {
padding: 0;
}
.selected-repository-content {
animation: fadeInUp 0.3s ease-out;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding-bottom: 0.6rem;
border-bottom: 1px solid #e9ecef;
}
.section-header h2 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.4rem;
}
.section-header h2::before {
content: '';
width: 3px;
height: 18px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
flex-shrink: 0;
}
.logs-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.4rem 1rem;
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.35rem;
font-weight: 600;
font-size: 0.82rem;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
}
.logs-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(102, 126, 234, 0.35);
}
.logs-button mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
}
.logs-section {
margin-top: 0.5rem;
padding: 0.75rem 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07);
border: 1px solid #e9ecef;
}
.logs-header {
margin-bottom: 0.5rem;
}
.logs-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.4rem;
}
.logs-header h3::before {
content: '';
width: 3px;
height: 16px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
flex-shrink: 0;
}
.logs-header p {
display: none;
}
.logs-container {
background: #f8f9fa;
border-radius: 6px;
padding: 0.4rem;
border: 1px solid #e9ecef;
}
.repository-logs-iframe {
border: none;
border-radius: 4px;
display: block;
}
.no-repositories {
text-align: center;
padding: 2rem 1rem;
color: #6c757d;
}
.no-repositories .no-data-icon {
font-size: 2.5rem;
width: 2.5rem;
height: 2.5rem;
margin-bottom: 0.5rem;
opacity: 0.4;
}
.no-repositories h3 {
margin: 0 0 0.4rem;
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
}
.no-repositories p {
margin: 0;
font-size: 0.85rem;
}
.no-repository-selected {
text-align: center;
padding: 1.5rem 1rem;
color: #6c757d;
}
.no-repository-selected .no-data-icon {
font-size: 2.5rem;
width: 2.5rem;
height: 2.5rem;
margin-bottom: 0.5rem;
opacity: 0.4;
}
.no-repository-selected h3 {
margin: 0 0 0.4rem;
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
}
.no-repository-selected p {
margin: 0;
font-size: 0.85rem;
}
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 160px;
padding: 1rem;
}
.error-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
text-align: center;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #e9ecef;
max-width: 360px;
}
.error-icon {
font-size: 42px;
width: 42px;
height: 42px;
color: #dc3545;
margin-bottom: 0.5rem;
}
.error-card h3 {
margin: 0 0 0.4rem;
color: #2c3e50;
font-weight: 600;
font-size: 1rem;
}
.error-card p {
margin: 0 0 1rem;
color: #6c757d;
font-size: 0.85rem;
line-height: 1.4;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}

View File

@ -0,0 +1,81 @@
<div class="section-content">
<div class="repositories-container">
@if(dhcps.length === 0) {
<div class="no-repositories">
<mat-icon class="no-data-icon">cloud_off</mat-icon>
<h3>{{ 'noDhcpsAvailable' | translate }}</h3>
<p>{{ 'noConfiguredDhcps' | translate }}</p>
</div>
} @else {<div class="repositories-selector-container">
<div class="repository-selector">
<mat-form-field appearance="outline" class="repository-select-field">
<mat-label>{{ 'buttonSelectDhcp' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedDhcpUuid" (selectionChange)="onDhcpChange($event.value)">
@for(dhcp of dhcps; track dhcp.uuid) {
<mat-option [value]="dhcp.uuid">
{{ dhcp.name }}
</mat-option>
}
</mat-select>
<mat-icon matSuffix>storage</mat-icon>
</mat-form-field>
</div>
@if(selectedDhcpUuid && !errorDhcps[selectedDhcpUuid] && dhcpStatuses[selectedDhcpUuid]) {
<div class="selected-repository-content">
<div class="repository-item">
<div class="section-header">
<h2>{{ getSelectedDhcpName() }}</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>{{ 'buttonViewLogs' | translate }}</span>
</button>
</div>
<div class="repository-content">
<app-status-tab [loading]="loadingDhcps" [diskUsage]="dhcpStatuses[selectedDhcpUuid].disk"
[servicesStatus]="dhcpStatuses[selectedDhcpUuid].services" [subnets]="subnets"
[diskUsageChartData]="dhcpStatuses[selectedDhcpUuid].diskUsageChartData" [view]="view"
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="true"
[isRepository]="false">
</app-status-tab>
</div>
</div>
<div class="logs-section" id="dhcp-logs-section">
<div class="logs-header">
<h3>{{ 'dhcpLogs' | translate }}</h3>
<p>{{ 'realTimeDhcpLogs' | translate }}</p>
</div>
<div class="logs-container">
<iframe [src]="getDhcpIframeUrl()" width="100%" height="800" frameborder="0" class="dhcp-logs-iframe">
</iframe>
</div>
</div>
</div>
}
@if(!selectedDhcpUuid) {
<div class="no-repository-selected">
<mat-icon class="no-data-icon">storage</mat-icon>
<h3>{{ 'selectADhcp' | translate }}</h3>
<p>{{ 'selectDhcpToView' | translate }}</p>
</div>
}
@if(selectedDhcpUuid && errorDhcps[selectedDhcpUuid]) {
<div class="error-container">
<div class="error-card">
<mat-icon class="error-icon">error_outline</mat-icon>
<h3>{{ 'connectionError' | translate }}</h3>
<p>{{ 'cannotConnectDhcp' | translate }}</p>
<button mat-raised-button color="primary" (click)="retryDhcpStatus(selectedDhcpUuid)">
<mat-icon>refresh</mat-icon>
{{ 'buttonRetry' | translate }}</button>
</div>
</div>
}
</div>
}
</div>
</div>

View File

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

View File

@ -0,0 +1,156 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit, Input } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ConfigService } from '@services/config.service';
import { ToastrService } from "ngx-toastr";
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-ogdhcp-status',
templateUrl: './ogdhcp-status.component.html',
styleUrl: './ogdhcp-status.component.css'
})
export class OgdhcpStatusComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
loadingDhcps: boolean = false;
@Input() dhcps: any[] = [];
dhcpStatuses: { [key: string]: any } = {};
selectedDhcpUuid: string = '';
errorDhcps: { [key: string]: boolean } = {};
subnets: any[] = [];
showLabels: boolean = true;
isDoughnut: boolean = true;
colorScheme: any = {
domain: ['#df200d', '#26a700']
};
view: [number, number] = [400, 220];
constructor(
private configService: ConfigService,
private toastService: ToastrService,
private http: HttpClient,
private sanitizer: DomSanitizer
) {
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void {
}
loadDhcpStatus(dhcpUuid: string, callback: (errorOccurred: boolean) => void): void {
const timeoutId = setTimeout(() => {
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/dhcp-servers/server/${dhcpUuid}/status`).subscribe(
data => {
if (!data.success && !data.message) {
clearTimeout(timeoutId);
callback(true);
return;
}
const message = data.message || data;
const disk = message.disk_usage;
const services = message.services_status;
let diskUsageChartData: any[] = [];
diskUsageChartData.push(
{ name: 'Usado', value: parseFloat(disk.used) },
{ name: 'Disponible', value: parseFloat(disk.available) }
);
this.subnets.length = 0;
if (message.subnets) {
this.subnets.push(...message.subnets);
}
this.dhcpStatuses[dhcpUuid] = {
disk: {
...disk,
used: parseFloat(disk.used.replace('GB', '')),
available: parseFloat(disk.available.replace('GB', '')),
total: parseFloat(disk.total.replace('GB', '')),
percentage: disk.used_percentage || disk.percentage
},
services: services,
diskUsageChartData: diskUsageChartData
};
clearTimeout(timeoutId);
callback(false);
},
error => {
this.toastService.error(error.error?.['hydra:description'] || 'Error al cargar el estado del dhcp');
clearTimeout(timeoutId);
callback(true);
}
);
}
onDhcpChange(dhcpUuid: string): void {
this.selectedDhcpUuid = dhcpUuid;
if (dhcpUuid && !this.dhcpStatuses[dhcpUuid]) {
this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
if (errorOccurred) {
this.errorDhcps[dhcpUuid] = true;
}
});
}
}
getSelectedDhcpName(): string {
const selectedDhcp = this.dhcps.find(dhcp => dhcp.uuid === this.selectedDhcpUuid);
return selectedDhcp ? selectedDhcp.name : '';
}
getDhcpIframeUrl(): SafeResourceUrl {
const dhcpName = this.getSelectedDhcpName();
if (dhcpName) {
const encodedName = encodeURIComponent(dhcpName);
const url = `${this.configService.logUrl}/d-solo/ogdhcp-logs/ogdhcp-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
return this.sanitizer.bypassSecurityTrustResourceUrl('');
}
retryDhcpStatus(dhcpUuid: string): void {
this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
this.errorDhcps[dhcpUuid] = errorOccurred;
if (!errorOccurred) {
this.toastService.success(this.translate.instant('dhcpStatusUpdatedSuccessfully'));
} else {
this.toastService.error(this.translate.instant('errorLoadingState'));
}
});
}
scrollToLogs(): void {
const checkAndScroll = () => {
const logsSection = document.getElementById('dhcp-logs-section');
if (logsSection) {
logsSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
return true;
}
return false;
};
const interval = setInterval(() => {
if (checkAndScroll()) {
clearInterval(interval);
}
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 3000);
}
}

View File

@ -0,0 +1,233 @@
.section-content {
padding: 0.75rem 1rem;
}
.repositories-container {
padding: 0;
}
.repositories-selector-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.repository-selector {
display: flex;
padding: 0.25rem 0 0;
}
.repository-select-field {
min-width: 260px;
max-width: 420px;
width: 100%;
}
.repository-content {
padding: 0;
}
.selected-repository-content {
animation: fadeInUp 0.3s ease-out;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding-bottom: 0.6rem;
border-bottom: 1px solid #e9ecef;
}
.section-header h2 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.4rem;
}
.section-header h2::before {
content: '';
width: 3px;
height: 18px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
flex-shrink: 0;
}
.logs-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.4rem 1rem;
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.35rem;
font-weight: 600;
font-size: 0.82rem;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
}
.logs-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(102, 126, 234, 0.35);
}
.logs-button mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
}
.logs-section {
margin-top: 0.5rem;
padding: 0.75rem 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07);
border: 1px solid #e9ecef;
}
.logs-header {
margin-bottom: 0.5rem;
}
.logs-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.4rem;
}
.logs-header h3::before {
content: '';
width: 3px;
height: 16px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
flex-shrink: 0;
}
.logs-header p {
display: none;
}
.logs-container {
background: #f8f9fa;
border-radius: 6px;
padding: 0.4rem;
border: 1px solid #e9ecef;
}
.repository-logs-iframe {
border: none;
border-radius: 4px;
display: block;
}
.no-repositories {
text-align: center;
padding: 2rem 1rem;
color: #6c757d;
}
.no-repositories .no-data-icon {
font-size: 2.5rem;
width: 2.5rem;
height: 2.5rem;
margin-bottom: 0.5rem;
opacity: 0.4;
}
.no-repositories h3 {
margin: 0 0 0.4rem;
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
}
.no-repositories p {
margin: 0;
font-size: 0.85rem;
}
.no-repository-selected {
text-align: center;
padding: 1.5rem 1rem;
color: #6c757d;
}
.no-repository-selected .no-data-icon {
font-size: 2.5rem;
width: 2.5rem;
height: 2.5rem;
margin-bottom: 0.5rem;
opacity: 0.4;
}
.no-repository-selected h3 {
margin: 0 0 0.4rem;
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
}
.no-repository-selected p {
margin: 0;
font-size: 0.85rem;
}
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 160px;
padding: 1rem;
}
.error-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
text-align: center;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #e9ecef;
max-width: 360px;
}
.error-icon {
font-size: 42px;
width: 42px;
height: 42px;
color: #dc3545;
margin-bottom: 0.5rem;
}
.error-card h3 {
margin: 0 0 0.4rem;
color: #2c3e50;
font-weight: 600;
font-size: 1rem;
}
.error-card p {
margin: 0 0 1rem;
color: #6c757d;
font-size: 0.85rem;
line-height: 1.4;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}

View File

@ -0,0 +1,89 @@
<div class="section-content">
<div class="repositories-container">
@if(repositories.length === 0) {
<div class="no-repositories">
<mat-icon class="no-data-icon">cloud_off</mat-icon>
<h3>{{ 'noReposAvailable' | translate }}</h3>
<p>{{ 'noConfiguredRepos' | translate }}</p>
</div>
} @else {<div class="repositories-selector-container">
<div class="repository-selector">
<mat-form-field appearance="outline" class="repository-select-field">
<mat-label>{{ 'selectRepository' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedRepositoryUuid" (selectionChange)="onRepositoryChange($event.value)">
@for(repository of repositories; track repository.uuid) {
<mat-option [value]="repository.uuid">
{{ repository.name }}
</mat-option>
}
</mat-select>
<mat-icon matSuffix>storage</mat-icon>
</mat-form-field>
</div>
@if(selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]) {
<div class="selected-repository-content">
<div class="repository-item">
<div class="section-header">
<h2>{{ getSelectedRepositoryName() }}</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>{{ 'buttonViewLogs' | translate }}</span>
</button>
</div>
<div class="repository-content">
<app-status-tab [loading]="loading" [diskUsage]="repositoryStatuses[selectedRepositoryUuid].disk"
[servicesStatus]="repositoryStatuses[selectedRepositoryUuid].services"
[processesStatus]="repositoryStatuses[selectedRepositoryUuid].processes"
[ramUsage]="repositoryStatuses[selectedRepositoryUuid].ram"
[cpuUsage]="repositoryStatuses[selectedRepositoryUuid].cpu" [diskUsageChartData]="[
{ name: 'Usado', value: repositoryStatuses[selectedRepositoryUuid].disk.used },
{ name: 'Disponible', value: repositoryStatuses[selectedRepositoryUuid].disk.available }
]" [ramUsageChartData]="[
{ name: 'Usado', value: repositoryStatuses[selectedRepositoryUuid].ram.used },
{ name: 'Disponible', value: repositoryStatuses[selectedRepositoryUuid].ram.available }
]" [view]="view" [colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels"
[isDhcp]="false" [isRepository]="true">
</app-status-tab>
</div>
</div>
<div class="repository-logs-section" id="repository-logs-section">
<div class="logs-header">
<h3>Datos del repositorio: {{ getSelectedRepositoryName() }}</h3>
<p>{{ 'specificRepoInfo' | translate }}</p>
</div>
<div class="logs-container">
<iframe [src]="getRepositoryIframeUrl()" width="100%" height="600" frameborder="0"
class="repository-logs-iframe">
</iframe>
</div>
</div>
</div>
}
@if(!selectedRepositoryUuid) {
<div class="no-repository-selected">
<mat-icon class="no-data-icon">storage</mat-icon>
<h3>{{ 'selectARepository' | translate }}</h3>
<p>{{ 'selectRepoToView' | translate }}</p>
</div>
}
@if(selectedRepositoryUuid && errorRepositories[selectedRepositoryUuid]) {
<div class="error-container">
<div class="error-card">
<mat-icon class="error-icon">error_outline</mat-icon>
<h3>{{ 'connectionError' | translate }}</h3>
<p>{{ 'cannotConnectRepo' | translate }}</p>
<button mat-raised-button color="primary" (click)="retryRepositoryStatus(selectedRepositoryUuid)">
<mat-icon>refresh</mat-icon>
{{ 'buttonRetry' | translate }}</button>
</div>
</div>
}
</div>
}
</div>
</div>

View File

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

View File

@ -0,0 +1,155 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit, Input } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ConfigService } from '@services/config.service';
import { ToastrService } from "ngx-toastr";
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-ogrepository-status',
templateUrl: './ogrepository-status.component.html',
styleUrl: './ogrepository-status.component.css'
})
export class OgrepositoryStatusComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
loading: boolean = false;
@Input() repositories: any[] = [];
repositoryStatuses: { [key: string]: any } = {};
selectedRepositoryUuid: string = '';
errorRepositories: { [key: string]: boolean } = {};
showLabels: boolean = true;
isDoughnut: boolean = true;
colorScheme: any = {
domain: ['#df200d', '#26a700']
};
view: [number, number] = [400, 220];
constructor(
private configService: ConfigService,
private toastService: ToastrService,
private http: HttpClient,
private sanitizer: DomSanitizer
) {
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void {
// Parent passes repositories list
}
// removed loadRepositories function that fetched from API
loadRepositoryStatus(repositoryUuid: string, callback: (errorOccurred: boolean) => void): void {
const timeoutId = setTimeout(() => {
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/image-repositories/server/${repositoryUuid}/status`).subscribe(
data => {
if (!data.success) {
clearTimeout(timeoutId);
callback(true);
return;
}
const details = data.details;
const { disk, services, ram, cpu, processes } = details;
this.repositoryStatuses[repositoryUuid] = {
disk: {
...disk,
used: parseFloat(disk.used.replace('GB', '')),
available: parseFloat(disk.available.replace('GB', '')),
total: parseFloat(disk.total.replace('GB', '')),
percentage: disk.used_percentage
},
ram: {
...ram,
used: parseFloat(ram.used.replace('GB', '')),
available: parseFloat(ram.available.replace('GB', '')),
total: parseFloat(ram.total.replace('GB', '')),
percentage: ram.used_percentage
},
cpu: {
used_percentage: cpu.used_percentage
},
services: services,
processes: processes
};
clearTimeout(timeoutId);
callback(false);
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del repositorio');
clearTimeout(timeoutId);
callback(true);
}
);
}
onRepositoryChange(repositoryUuid: string): void {
this.selectedRepositoryUuid = repositoryUuid;
if (repositoryUuid && !this.repositoryStatuses[repositoryUuid]) {
this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
if (errorOccurred) {
this.errorRepositories[repositoryUuid] = true;
}
});
}
}
getSelectedRepositoryName(): string {
const selectedRepo = this.repositories.find(repo => repo.uuid === this.selectedRepositoryUuid);
return selectedRepo ? selectedRepo.name : '';
}
getRepositoryIframeUrl(): SafeResourceUrl {
const repositoryName = this.getSelectedRepositoryName();
if (repositoryName) {
const encodedName = encodeURIComponent(repositoryName);
const url = `${this.configService.logUrl}/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
return this.sanitizer.bypassSecurityTrustResourceUrl('');
}
retryRepositoryStatus(repositoryUuid: string): void {
this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
this.errorRepositories[repositoryUuid] = errorOccurred;
if (!errorOccurred) {
this.toastService.success(this.translate.instant('successfullyUpdatedRepositoryStatus'));
} else {
this.toastService.error(this.translate.instant('errorLoadingState1'));
}
});
}
scrollToLogs(): void {
const checkAndScroll = () => {
const logsSection = document.getElementById('repository-logs-section');
if (logsSection) {
logsSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
return true;
}
return false;
};
const interval = setInterval(() => {
if (checkAndScroll()) {
clearInterval(interval);
}
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 3000);
}
}

View File

@ -2,15 +2,15 @@
.dashboard {
display: flex;
flex-direction: column;
gap: 2rem;
gap: 1rem;
padding: 0;
}
/* ===== SECCIÓN DE RECURSOS ===== */
.resources-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 1.5rem;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
/* Layout específico para repositorios - gráficas en una sola fila */
@ -25,8 +25,8 @@
.resource-card {
background: white;
border-radius: 16px;
padding: 1.5rem;
border-radius: 12px;
padding: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
@ -59,8 +59,8 @@
.resource-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.resource-icon {
@ -173,14 +173,14 @@
/* ===== SECCIÓN DE SERVICIOS ===== */
.services-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1rem;
}
.service-card {
background: white;
border-radius: 16px;
padding: 1.5rem;
border-radius: 12px;
padding: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
@ -194,8 +194,8 @@
.service-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.service-icon {
@ -298,13 +298,13 @@
.data-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
gap: 1rem;
}
.data-card {
background: white;
border-radius: 16px;
padding: 1.5rem;
border-radius: 12px;
padding: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
@ -318,8 +318,8 @@
.data-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.data-icon {

View File

@ -17,19 +17,19 @@
</div>
<div class="resource-info">
<div class="info-item">
<span class="info-label">{{ 'totalLabel' | translate }}:</span>
<span class="info-label">{{ 'labelDiskTotal' | translate }}:</span>
<span class="info-value">{{ isRepository ? diskUsage.total : formatBytes(diskUsage.total) }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ 'usedLabel' | translate }}:</span>
<span class="info-label">{{ 'labelDiskUsed' | translate }}:</span>
<span class="info-value">{{ isRepository ? diskUsage.used : formatBytes(diskUsage.used) }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ 'availableLabel' | translate }}:</span>
<span class="info-label">{{ 'labelAvailable' | translate }}:</span>
<span class="info-value">{{ isRepository ? diskUsage.available : formatBytes(diskUsage.available) }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ 'usedPercentageLabel' | translate }}:</span>
<span class="info-label">{{ 'labelDiskUsed' | translate }}:</span>
<span class="info-value usage-percentage">{{ isRepository ? diskUsage.used_percentage : diskUsage.percentage }}</span>
</div>
</div>
@ -51,19 +51,19 @@
</div>
<div class="resource-info">
<div class="info-item">
<span class="info-label">{{ 'totalLabel' | translate }}:</span>
<span class="info-label">{{ 'labelDiskTotal' | translate }}:</span>
<span class="info-value">{{ ramUsage.total }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ 'usedLabel' | translate }}:</span>
<span class="info-label">{{ 'labelDiskUsed' | translate }}:</span>
<span class="info-value">{{ ramUsage.used }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ 'availableLabel' | translate }}:</span>
<span class="info-label">{{ 'labelAvailable' | translate }}:</span>
<span class="info-value">{{ ramUsage.available }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ 'usedPercentageLabel' | translate }}:</span>
<span class="info-label">{{ 'labelDiskUsed' | translate }}:</span>
<span class="info-value usage-percentage">{{ ramUsage.used_percentage }}</span>
</div>
</div>
@ -81,7 +81,7 @@
<div class="cpu-usage-display">
<div class="cpu-circle">
<div class="cpu-percentage">{{ cpuUsage.used_percentage }}</div>
<div class="cpu-label">Uso actual</div>
<div class="cpu-label">{{ 'currentUsage' | translate }}</div>
</div>
</div>
</div>
@ -94,7 +94,7 @@
<div class="service-icon">
<mat-icon>settings</mat-icon>
</div>
<h3 class="service-title">{{ 'servicesTitle' | translate }}</h3>
<h3 class="service-title">{{ 'labelServices' | translate }}</h3>
</div>
<div class="service-list">
<div class="service-item" *ngFor="let service of getServices()">
@ -115,7 +115,7 @@
<div class="service-icon">
<mat-icon>list_alt</mat-icon>
</div>
<h3 class="service-title">{{ 'processes' | translate }}</h3>
<h3 class="service-title">{{ 'labelProcesses' | translate }}</h3>
</div>
<div class="service-list">
<div class="service-item" *ngFor="let process of getProcesses()">
@ -144,10 +144,10 @@
<table class="data-table">
<thead>
<tr>
<th>{{ 'idLabel' | translate }}</th>
<th>{{ 'labelId' | translate }}</th>
<th>{{ 'kernelLabel' | translate }}</th>
<th>{{ 'architectureLabel' | translate }}</th>
<th>{{ 'revisionLabel' | translate }}</th>
<th>{{ 'labelArchitecture' | translate }}</th>
<th>{{ 'labelRevision' | translate }}</th>
</tr>
</thead>
<tbody>
@ -173,11 +173,11 @@
<table class="data-table">
<thead>
<tr>
<th>{{ 'idLabel' | translate }}</th>
<th>{{ 'bootFileNameLabel' | translate }}</th>
<th>{{ 'nextServerLabel' | translate }}</th>
<th>{{ 'ipLabel' | translate }}</th>
<th>{{ 'clientsLabel' | translate }}</th>
<th>{{ 'labelId' | translate }}</th>
<th>{{ 'labelBootFileName' | translate }}</th>
<th>{{ 'labelNextServer' | translate }}</th>
<th>{{ 'labelIp' | translate }}</th>
<th>{{ 'labelClients' | translate }}</th>
</tr>
</thead>
<tbody>

View File

@ -30,7 +30,7 @@
</div>
</div>
<div class="table-header-container">
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
<h2 class="title" i18n="@@adminImagesTitle">{{ 'labelDisksPartitions' | translate }}</h2>
<mat-chip *ngIf="clientData.firmwareType" class="firmware-chip">
<mat-icon>memory</mat-icon>
{{ clientData.firmwareType }}
@ -41,7 +41,7 @@
<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>
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef !== 'size'">
{{ column.cell(image) }}

View File

@ -14,21 +14,20 @@
<div class="destination-badge">
<mat-icon class="destination-icon">cloud_upload</mat-icon>
<div class="destination-content">
<span class="destination-label">Destino</span>
<span class="destination-label">{{ 'labelDestination' | translate }}</span>
<span class="destination-value">{{ selectedRepository?.name || 'No hay repositorio asociado' }}</span>
</div>
</div>
</div>
</div>
<div class="button-row">
<button class="action-button" id="execute-button" [disabled]="!selectedPartition || loading" (click)="save()">Ejecutar</button>
<button class="action-button" id="execute-button" [disabled]="!selectedPartition || loading" (click)="save()">{{ 'buttonExecute' | translate }}</button>
</div>
<div class="button-row">
<button class="action-button" color="accent"
[disabled]="!isFormValid()"
(click)="openScheduleModal()">
Opciones de programación
</button>
{{ 'schedulingOptions' | translate }}</button>
</div>
</div>
@ -37,13 +36,12 @@
<div class="form-section compact-section type-section">
<div class="form-section-title">
<mat-icon>settings</mat-icon>
Tipo de imagen
</div>
{{ 'imageType' | translate }}</div>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Tipo de imagen</mat-label>
<mat-label>{{ 'imageType' | translate }}</mat-label>
<mat-select [(ngModel)]="imageType" class="full-width" (selectionChange)="onImageTypeSelected($event.value)">
<mat-option [value]="'monolithic'">Monolítica</mat-option>
<mat-option [value]="'git'">Git</mat-option>
<mat-option [value]="'monolithic'">{{ 'labelMonolithic' | translate }}</mat-option>
<mat-option [value]="'git'">{{ 'labelGit' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
@ -51,40 +49,39 @@
<div class="form-section compact-section config-section" *ngIf="imageType === 'monolithic'">
<div class="form-section-title">
<mat-icon>image</mat-icon>
Configuración
</div>
{{ 'configurationTitle' | translate }}</div>
<div class="action-chips-container">
<mat-chip-listbox [(ngModel)]="monolithicAction" required class="action-chip-listbox">
<mat-chip-option value="create" class="action-chip create-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'create'})">
<span>Crear imagen</span>
<span>{{ 'buttonCreateImage' | translate }}</span>
</mat-chip-option>
<mat-chip-option value="update" class="action-chip update-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'update'})">
<span>Actualizar imagen</span>
<span>{{ 'buttonUpdateImage' | translate }}</span>
</mat-chip-option>
</mat-chip-listbox>
</div>
<div *ngIf="monolithicAction === 'create'">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Nombre canónico</mat-label>
<input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
<mat-hint>Nombre para la nueva imagen</mat-hint>
<mat-label>{{ 'canonicalName' | translate }}</mat-label>
<input matInput [(ngModel)]="name" [placeholder]="'canonicalNameFormatHint' | translate" required>
<mat-hint>{{ 'labelNewImageName' | translate }}</mat-hint>
</mat-form-field>
</div>
<div *ngIf="monolithicAction === 'update'">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione imagen</mat-label>
<mat-label>{{ 'selectImagePrompt' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required>
<mat-option [value]="null">Seleccionar imagen para actualizar</mat-option>
<mat-option [value]="null">{{ 'selectImageToUpdate' | translate }}</mat-option>
<mat-option *ngFor="let image of images" [value]="image">{{ image?.name }}</mat-option>
</mat-select>
<button *ngIf="selectedImage" mat-icon-button matSuffix aria-label="Clear client search"
<button *ngIf="selectedImage" mat-icon-button matSuffix [attr.aria-label]="'clearClientSearch' | translate"
(click)="selectedImage = null; resetCanonicalName()">
<mat-icon>close</mat-icon>
</button>
<mat-hint>Selecciona la imagen existente que quieres actualizar.</mat-hint>
<mat-hint>{{ 'selectImageToUpdateHint' | translate }}</mat-hint>
</mat-form-field>
</div>
</div>
@ -92,14 +89,13 @@
<div class="form-section compact-section config-section" *ngIf="imageType === 'git'">
<div class="form-section-title">
<mat-icon>code</mat-icon>
Configuración Git
</div>
{{ 'gitConfiguration' | translate }}</div>
<div class="git-info-container" *ngIf="selectedPartition && imageType === 'git' && (hasValidGitData || loadingGitData)">
<div class="git-info-card">
<div class="git-info-header">
<mat-icon class="git-info-icon">info</mat-icon>
<span class="git-info-title">La imagen en caché a actualizar es:</span>
<span class="git-info-title">{{ 'cacheImageToUpdateIs' | translate }}</span>
<mat-spinner *ngIf="loadingGitData" diameter="20" class="git-loading-spinner"></mat-spinner>
</div>
<div *ngIf="!loadingGitData && hasValidGitData">
@ -108,8 +104,7 @@
<mat-form-field appearance="outline" class="git-form-field">
<mat-label>
<mat-icon class="git-item-icon">folder</mat-icon>
Repositorio
</mat-label>
{{ 'labelRepository' | translate }}</mat-label>
<input matInput [value]="gitData.repo" readonly>
</mat-form-field>
</div>
@ -117,8 +112,7 @@
<mat-form-field appearance="outline" class="git-form-field">
<mat-label>
<mat-icon class="git-item-icon">account_tree</mat-icon>
Rama origen
</mat-label>
{{ 'sourceBranch' | translate }}</mat-label>
<input matInput [value]="gitData.branch" readonly>
</mat-form-field>
</div>
@ -126,12 +120,11 @@
<mat-form-field appearance="outline" class="git-form-field">
<mat-label>
<mat-icon class="git-item-icon">call_split</mat-icon>
Rama destino
</mat-label>
{{ 'destinationBranch' | translate }}</mat-label>
<input matInput
[(ngModel)]="destinationBranch"
[readonly]="!isDestinationBranchEditable"
placeholder="Nombre de la rama destino"
[placeholder]="'destinationBranchNameLabel' | translate"
[matTooltip]="isDestinationBranchEditable ? 'Guardar cambios' : 'Editar nombre de la rama destino'">
<button mat-icon-button
matSuffix
@ -146,7 +139,7 @@
<div class="repository-warning" *ngIf="repositoryNotFound">
<mat-icon class="warning-icon">warning</mat-icon>
<div class="warning-content">
<div class="warning-title">Repositorio no encontrado</div>
<div class="warning-title">{{ 'repositoryNotFound' | translate }}</div>
<div class="warning-message">
El repositorio "{{ gitData.repo }}" no existe en el listado de repositorios disponibles.
Es posible que necesites crearlo primero.
@ -155,18 +148,17 @@
</div>
</div>
<div class="git-info-loading" *ngIf="loadingGitData">
<span>Cargando información de Git...</span>
<span>{{ 'loadingGitInfo' | translate }}</span>
</div>
<div class="git-info-empty" *ngIf="!loadingGitData && !hasValidGitData">
<span>No se encontró información de Git para esta partición</span>
<span>{{ 'noGitInfoForPartition' | translate }}</span>
</div>
</div>
</div>
<div class="force-push-container" *ngIf="imageType === 'git' && hasValidGitData">
<mat-checkbox [(ngModel)]="forcePush" class="force-push-checkbox">
Opción 'Forzar push'
</mat-checkbox>
{{ 'forcePushOption' | translate }}</mat-checkbox>
</div>
<div class="repository-header" *ngIf="!hasValidGitData">
@ -175,24 +167,22 @@
(click)="openCreateRepositoryModal()"
[disabled]="creatingRepository">
<mat-icon>add</mat-icon>
<span>Crear nuevo repositorio / SO</span>
<span>{{ 'createNewRepoOs' | translate }}</span>
</button>
</div>
<div class="selector" *ngIf="!hasValidGitData">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccionar repositorio Git</mat-label>
<mat-label>{{ 'selectGitRepository' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositorySelected($event.value)" required>
<mat-option [value]="null">Seleccionar repositorio git / SO</mat-option>
<mat-option [value]="null">{{ 'selectGitRepoOs' | translate }}</mat-option>
<mat-option *ngFor="let repo of gitRepositories" [value]="repo">{{ repo.name }}</mat-option>
</mat-select>
<mat-spinner *ngIf="loadingGitRepositories" matSuffix diameter="20"></mat-spinner>
<mat-hint>
<mat-icon>info</mat-icon>
Selecciona el repositorio git para obtener las imágenes disponibles.
<span *ngIf="gitRepositories.length === 0" class="no-repositories-hint">
No hay repositorios disponibles. Crea uno nuevo para continuar.
</span>
{{ 'selectGitRepoHint' | translate }}<span *ngIf="gitRepositories.length === 0" class="no-repositories-hint">
{{ 'noReposAvailableCreateNew' | translate }}</span>
</mat-hint>
</mat-form-field>
</div>
@ -202,8 +192,7 @@
<div class="form-section" #partitionSection id="partition-selection">
<div class="form-section-title">
<mat-icon>storage</mat-icon>
Selección de partición
</div>
{{ 'partitionSelection' | translate }}</div>
<div class="partition-table-container">
<app-disk-partition-selector

View File

@ -1,4 +1,6 @@
import {Component, EventEmitter, OnInit, Output, ViewChild, ElementRef} from '@angular/core';
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {Component, EventEmitter, OnInit, Output, ViewChild, ElementRef } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { ToastrService } from "ngx-toastr";
import { ActivatedRoute, Router } from "@angular/router";
@ -17,6 +19,7 @@ import {CreateTaskComponent} from "../../../../commands/commands-task/create-tas
styleUrl: './create-image.component.css'
})
export class CreateClientImageComponent implements OnInit{
public translate = inject(TranslateService);
baseUrl: string;
@Output() dataChange = new EventEmitter<any>();
@ViewChild('partitionSection', { static: false }) partitionSection!: ElementRef;
@ -69,27 +72,27 @@ export class CreateClientImageComponent implements OnInit{
columns = [
{
columnDef: 'partitionNumber',
header: 'Particion',
header: 'partition',
cell: (partition: any) => partition.partitionNumber
},
{
columnDef: 'size',
header: 'Tamaño',
header: 'labelDiskSize',
cell: (partition: any) => `${partition.size} MB`
},
{
columnDef: 'partitionCode',
header: 'Tipo de partición',
header: 'partitionType',
cell: (partition: any) => partition.partitionCode
},
{
columnDef: 'filesystem',
header: 'Sistema de ficheros',
header: 'fileSystem',
cell: (partition: any) => partition.filesystem
},
{
columnDef: 'operativeSystem',
header: 'SO',
header: 'sW',
cell: (partition: any) => partition.operativeSystem?.name
}
];
@ -139,7 +142,7 @@ export class CreateClientImageComponent implements OnInit{
}
},
(error) => {
console.error('Error al cargar los datos del cliente:', error);
console.error(this.translate.instant('errorLoadingData1'), error);
}
);
}
@ -155,7 +158,7 @@ export class CreateClientImageComponent implements OnInit{
this.images = response['hydra:member'];
},
(error) => {
console.error('Error al cargar las imágenes:', error);
console.error(this.translate.instant('errorLoadingImages3'), error);
}
);
}
@ -186,7 +189,7 @@ export class CreateClientImageComponent implements OnInit{
this.checkRepositoryExists();
},
(error) => {
console.error('Error al cargar los repositorios git:', error);
console.error(this.translate.instant('errorLoadingRepositories2'), error);
this.loadingGitRepositories = false;
}
);
@ -201,7 +204,7 @@ export class CreateClientImageComponent implements OnInit{
this.loadingGitImageRepositories = false;
},
(error) => {
console.error('Error al cargar las imágenes de repositorio git:', error);
console.error(this.translate.instant('errorLoadingImages'), error);
this.loadingGitImageRepositories = false;
}
);
@ -237,8 +240,8 @@ export class CreateClientImageComponent implements OnInit{
}
},
error => {
console.error('Error fetching branches', error);
this.toastService.error('Error al cargar las ramas del repositorio');
console.error(this.translate.instant('fetchingBranchesError'), error);
this.toastService.error(this.translate.instant('errorLoadingBranches1'));
this.loadingBranches = false;
}
);
@ -261,7 +264,7 @@ export class CreateClientImageComponent implements OnInit{
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastService.success('Rama creada correctamente');
this.toastService.success(this.translate.instant('branchCreatedSuccessfully'));
this.destinationBranch = result.branchName || result;
this.loadBranches();
}
@ -270,7 +273,7 @@ export class CreateClientImageComponent implements OnInit{
}
if (!this.selectedGitRepository) {
this.toastService.error('Debe seleccionar un repositorio primero');
this.toastService.error(this.translate.instant('youMustSelectARepositoryFirst'));
return;
}
@ -285,7 +288,7 @@ export class CreateClientImageComponent implements OnInit{
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastService.success('Rama creada correctamente');
this.toastService.success(this.translate.instant('branchCreatedSuccessfully'));
this.destinationBranch = result.branchName || result;
this.loadBranches();
}
@ -319,9 +322,9 @@ export class CreateClientImageComponent implements OnInit{
this.loadingExistingImages = false;
},
(error) => {
console.error('Error al cargar las imágenes existentes:', error);
console.error(this.translate.instant('errorLoadingImages2'), error);
this.loadingExistingImages = false;
this.toastService.error('Error al cargar las imágenes existentes');
this.toastService.error(this.translate.instant('errorLoadingImages1'));
}
);
}
@ -347,18 +350,18 @@ export class CreateClientImageComponent implements OnInit{
save(): void {
if (!this.selectedPartition) {
this.toastService.error('Debes seleccionar una partición');
this.toastService.error(this.translate.instant('youMustSelectAPartition'));
return;
}
if (this.imageType === 'monolithic') {
if (this.monolithicAction === 'create' && !this.name) {
this.toastService.error('Debes introducir un nombre canónico para la imagen');
this.toastService.error(this.translate.instant('youMustEnterACanonicalName'));
return;
}
if (this.monolithicAction === 'update' && !this.selectedImage) {
this.toastService.error('Debes seleccionar una imagen para actualizar');
this.toastService.error(this.translate.instant('youMustSelectAnImageTo'));
return;
}
}
@ -572,8 +575,8 @@ export class CreateClientImageComponent implements OnInit{
}
},
error: (error) => {
console.error('Error al cargar datos de Git:', error);
this.toastService.error('Error al cargar información de Git');
console.error(this.translate.instant('errorLoadingData'), error);
this.toastService.error(this.translate.instant('errorLoadingInformation'));
this.loadingGitData = false;
}
});
@ -603,7 +606,7 @@ export class CreateClientImageComponent implements OnInit{
this.loadingBranches = false;
},
error => {
console.error('Error fetching branches for Git data', error);
console.error(this.translate.instant('errorFetchingBranchesForGit'), error);
this.loadingBranches = false;
}
);
@ -643,7 +646,7 @@ export class CreateClientImageComponent implements OnInit{
let scope = 'clients';
if (!this.client) {
this.toastService.error('No se ha cargado la información del cliente');
this.toastService.error(this.translate.instant('hasNotBeenLoaded'));
return;
}
@ -663,7 +666,7 @@ export class CreateClientImageComponent implements OnInit{
dialogRef.afterClosed().subscribe((result: any) => {
if (result) {
if (!this.selectedPartition) {
this.toastService.error('Debe seleccionar una partición');
this.toastService.error(this.translate.instant('youMustSelectAPartition'));
return;
}
@ -699,7 +702,7 @@ export class CreateClientImageComponent implements OnInit{
type: 'create-image',
}).subscribe({
next: () => {
this.toastService.success('Tarea de creación de imagen programada con éxito');
this.toastService.success(this.translate.instant('imageCreationTask'));
},
error: (error) => {
this.toastService.error(error.error['hydra:description'] || 'Error al programar la tarea');

View File

@ -1,17 +1,17 @@
<app-loading [isLoading]="loading"></app-loading>
<h2 mat-dialog-title>Crear nuevo repositorio de imágenes git</h2>
<h2 mat-dialog-title>{{ 'createNewGitRepo' | translate }}</h2>
<mat-dialog-content class="dialog-content">
<form [formGroup]="repositoryForm" (ngSubmit)="save()" class="repository-form">
<mat-form-field appearance="fill" class="form-field">
<mat-label>Nombre del repositorio</mat-label>
<mat-label>{{ 'labelRepoName' | translate }}</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
</form>
</mat-dialog-content>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">Cancelar</button>
<button class="submit-button" (click)="save()">Guardar</button>
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="save()">{{ 'labelSaveButton' | translate }}</button>
</div>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit, Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { HttpClient } from "@angular/common/http";
@ -11,6 +13,7 @@ import { ConfigService } from '@services/config.service';
styleUrl: './create-repository-modal.component.css'
})
export class CreateRepositoryModalComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
repositoryForm: FormGroup<any>;
loading: boolean = false;
@ -45,7 +48,7 @@ export class CreateRepositoryModalComponent implements OnInit {
this.http.post(`${this.baseUrl}/git-repositories`, payload).subscribe({
next: (response) => {
this.toastService.success('Repositorio creado correctamente');
this.toastService.success(this.translate.instant('successfullyCreatedRepository'));
this.dialogRef.close(response);
},
error: (error) => {
@ -54,7 +57,7 @@ export class CreateRepositoryModalComponent implements OnInit {
}
});
} else {
this.toastService.error('Por favor, complete todos los campos requeridos');
this.toastService.error(this.translate.instant('pleaseCompleteAll'));
}
}

View File

@ -9,45 +9,40 @@
<div class="destination-badge">
<mat-icon class="destination-icon">cloud_download</mat-icon>
<div class="destination-content">
<span class="destination-label">Destino</span>
<span class="destination-label">{{ 'labelDestination' | translate }}</span>
<span class="destination-value">{{ runScriptTitle }}</span>
</div>
</div>
</div>
</div>
<div class="button-row">
<button class="action-button" id="execute-button" [disabled]="!isFormValid()" (click)="save()">Ejecutar</button>
<button class="action-button" id="execute-button" [disabled]="!isFormValid()" (click)="save()">{{ 'buttonExecute' | translate }}</button>
</div>
<div class="button-row">
<button class="action-button" (click)="generateOgInstructions()">
Generar instrucciones
</button>
{{ 'generateInstructions' | translate }}</button>
</div>
<div class="button-row">
<button class="action-button" color="accent" [disabled]="!isFormValid()" (click)="openScheduleModal()">
Opciones de programación
</button>
{{ 'schedulingOptions' | translate }}</button>
</div>
</div>
<div class="select-container">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title>
<mat-panel-title> {{ 'labelClients' | translate }}</mat-panel-title>
<mat-panel-description>
Listado de clientes donde se desplegará la imagen
<mat-icon>desktop_windows</mat-icon>
{{ 'clientListDeployImage' | translate }}<mat-icon>desktop_windows</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header>
<div class="section-hint" (click)="$event.stopPropagation()">
<mat-icon>info</mat-icon>
<span>
Puedes marcar o desmarcar un cliente haciendo clic en el área del ordenador. Además, selecciona con "Modelo" el
equipo que actuará como modelo.
</span>
{{ 'markClientModelInstruction' | translate }}</span>
</div>
<div class="button-row">
@ -74,8 +69,7 @@
<div class="model-selector" (click)="$event.stopPropagation()">
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="onModelSelected(selectedModelClient)">
<mat-radio-button [value]="client" color="primary" (click)="$event.stopPropagation()">
Modelo
</mat-radio-button>
{{ 'labelModel' | translate }}</mat-radio-button>
</mat-radio-group>
</div>
</div>
@ -92,15 +86,14 @@
<div class="form-section compact-section image-config-section">
<div class="form-section-title">
<mat-icon>image</mat-icon>
Configuración de imagen
</div>
{{ 'imageConfiguration' | translate }}</div>
<div class="selector">
<mat-form-field appearance="fill" class="half-width">
<mat-label>Tipo de imagen</mat-label>
<mat-label>{{ 'imageType' | translate }}</mat-label>
<mat-select [(ngModel)]="imageType" (selectionChange)="onImageTypeSelected($event.value)">
<mat-option [value]="'monolithic'">Monolítica</mat-option>
<mat-option [value]="'git'">Git</mat-option>
<mat-option [value]="'monolithic'">{{ 'labelMonolithic' | translate }}</mat-option>
<mat-option [value]="'git'">{{ 'labelGit' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
@ -108,13 +101,13 @@
<!-- Campos monolítica -->
<div class="monolithic-row" *ngIf="imageType === 'monolithic'">
<mat-form-field appearance="fill" class="half-width">
<mat-label>Seleccione método de deploy</mat-label>
<mat-label>{{ 'selectDeployMethod' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedMethod" name="selectedMethod" (selectionChange)="validateImageSize()">
<mat-option *ngFor="let method of allMethods" [value]="method.value">{{ method.name }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione imagen</mat-label>
<mat-label>{{ 'selectImagePrompt' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedImage" name="selectedImage" [disabled]="!imageType">
<mat-option *ngFor="let image of images" [value]="image">
<div class="unit-name"> {{ image.name }}</div>
@ -127,7 +120,7 @@
<!-- Campos Git -->
<div *ngIf="imageType === 'git'" class="filters-row git-gap">
<mat-form-field appearance="fill" style="width: 300px;">
<mat-label>Seleccionar Repositorio</mat-label>
<mat-label>{{ 'buttonSelectRepository' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositoryChange($event.value)"
[disabled]="loadingRepositories">
<mat-option *ngFor="let repo of repositories" [value]="repo">
@ -138,7 +131,7 @@
</mat-form-field>
<mat-form-field appearance="fill" style="width: 300px;">
<mat-label>Seleccionar Rama</mat-label>
<mat-label>{{ 'buttonSelectBranch' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedBranch" (selectionChange)="onGitBranchChange()"
[disabled]="loadingBranches || !selectedRepository">
<mat-option *ngFor="let branch of branches" [value]="branch">
@ -149,10 +142,10 @@
</mat-form-field>
<mat-form-field appearance="fill" style="width: 200px;">
<mat-label>Tag</mat-label>
<mat-label>{{ 'labelTag' | translate }}</mat-label>
<mat-select [(ngModel)]="showOnlyTagged" (selectionChange)="onTagFilterChange()" [disabled]="!commits.length">
<mat-option [value]="false">No</mat-option>
<mat-option [value]="true">Sí</mat-option>
<mat-option [value]="false">{{ 'buttonNo' | translate }}</mat-option>
<mat-option [value]="true">{{ 'buttonYes' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
@ -175,8 +168,7 @@
<span class="summary-text">{{ getMethodConfigSummary() }}</span>
</div>
</div>
} @else {
<div class="method-config-pending">
} @else {<div class="method-config-pending">
<div class="pending-message">
<mat-icon class="pending-icon">warning</mat-icon>
<span class="pending-text">Se requiere configurar los parámetros de {{ getMethodLabel() }}</span>
@ -204,15 +196,15 @@
<div class="form-section-row deploy-mode-row">
<div class="deploy-mode-label">
<mat-icon>cached</mat-icon>
<span>Modo de operación</span>
<span>{{ 'operationMode' | translate }}</span>
</div>
<mat-radio-group [(ngModel)]="deployMode" name="deployMode" class="deploy-mode-radio-group">
<mat-radio-button value="deploy" color="primary" class="deploy-mode-radio">
<div class="deploy-mode-option">
<mat-icon>storage</mat-icon>
<div>
<span class="deploy-mode-option-title">Despliegue completo</span>
<span class="deploy-mode-option-desc">Actualiza caché y restaura partición</span>
<span class="deploy-mode-option-title">{{ 'fullDeployment' | translate }}</span>
<span class="deploy-mode-option-desc">{{ 'updateCacheRestorePartition' | translate }}</span>
</div>
</div>
</mat-radio-button>
@ -220,8 +212,8 @@
<div class="deploy-mode-option">
<mat-icon>download</mat-icon>
<div>
<span class="deploy-mode-option-title">Solo actualizar caché</span>
<span class="deploy-mode-option-desc">Descarga imagen sin restaurar partición</span>
<span class="deploy-mode-option-title">{{ 'onlyUpdateCache' | translate }}</span>
<span class="deploy-mode-option-desc">{{ 'downloadImageNoRestore' | translate }}</span>
</div>
</div>
</mat-radio-button>
@ -234,15 +226,13 @@
<div class="form-section compact-section partition-select-section" id="partition-selection">
<div class="form-section-title">
<mat-icon>storage</mat-icon>
Selección de partición
</div>
{{ 'partitionSelection' | translate }}</div>
<div class="partition-table-container">
<div *ngIf="showInstructions" class="instructions-box">
<mat-card class="instructions-card">
<mat-card-title>
Instrucciones generadas
<button mat-icon-button (click)="showInstructions = false" style="float: right;">
{{ 'generatedInstructions' | translate }}<button mat-icon-button (click)="showInstructions = false" style="float: right;">
<mat-icon>close</mat-icon>
</button>
</mat-card-title>
@ -267,8 +257,7 @@
<div class="form-section compact-section partition-select-section" id="partition-info">
<div class="form-section-title">
<mat-icon>info</mat-icon>
Estado actual de particiones
</div>
{{ 'currentPartitionState' | translate }}</div>
<div class="partition-table-container">
<app-disk-partition-selector
[(selectedPartition)]="selectedPartition"
@ -288,13 +277,12 @@
<div *ngIf="imageType === 'git' && filteredCommits.length > 0" class="form-section compact-section">
<div class="form-section-title">
<mat-icon>history</mat-icon>
Selección de commit
</div>
{{ 'commitSelection' | translate }}</div>
<div class="commits-table-container">
<table mat-table [dataSource]="filteredCommits" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>Seleccionar</th>
<th mat-header-cell *matHeaderCellDef>{{ 'buttonSelect' | translate }}</th>
<td mat-cell *matCellDef="let commit">
<mat-radio-group [(ngModel)]="selectedCommit" name="selectedCommit">
<mat-radio-button [value]="commit"></mat-radio-button>
@ -302,7 +290,7 @@
</td>
</ng-container>
<ng-container matColumnDef="hexsha">
<th mat-header-cell *matHeaderCellDef>Commit ID</th>
<th mat-header-cell *matHeaderCellDef>{{ 'commitId' | translate }}</th>
<td mat-cell *matCellDef="let commit">
<code style="background-color: #f5f5f5; padding: 2px 4px; border-radius: 3px; font-family: monospace;">
{{ commit.hexsha }}
@ -310,22 +298,22 @@
</td>
</ng-container>
<ng-container matColumnDef="message">
<th mat-header-cell *matHeaderCellDef>Mensaje</th>
<th mat-header-cell *matHeaderCellDef>{{ 'labelMessage' | translate }}</th>
<td mat-cell *matCellDef="let commit">{{ commit.message }}</td>
</ng-container>
<ng-container matColumnDef="committed_date">
<th mat-header-cell *matHeaderCellDef>Fecha</th>
<th mat-header-cell *matHeaderCellDef>{{ 'labelDate' | translate }}</th>
<td mat-cell *matCellDef="let commit">{{ commit.committed_date * 1000 | date:'dd/MM/yyyy HH:mm:ss' }}</td>
</ng-container>
<ng-container matColumnDef="tags">
<th mat-header-cell *matHeaderCellDef>Tags</th>
<th mat-header-cell *matHeaderCellDef>{{ 'labelTags' | translate }}</th>
<td mat-cell *matCellDef="let commit">
<mat-chip-list>
<mat-chip *ngFor="let tag of commit.tags" color="primary" selected class="custom-tag-chip"
style="background: rgba(102, 126, 234, 0.1) !important; color: #667eea !important; border: 1px solid rgba(102, 126, 234, 0.3) !important; border-radius: 8px !important; font-weight: 500 !important; font-size: 10px !important; padding: 2px 6px !important; margin: 1px !important; box-shadow: none !important;">
{{ tag }}
</mat-chip>
<span *ngIf="!commit.tags || commit.tags.length === 0" class="no-tags-text">Sin tags</span>
<span *ngIf="!commit.tags || commit.tags.length === 0" class="no-tags-text">{{ 'noTags' | translate }}</span>
</mat-chip-list>
</td>
</ng-container>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { MatTableDataSource } from "@angular/material/table";
import { SelectionModel } from "@angular/cdk/collections";
@ -16,6 +18,7 @@ import { QueueConfirmationModalComponent } from "../../../../../shared/queue-con
styleUrl: './deploy-image.component.css'
})
export class DeployImageComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
@Output() dataChange = new EventEmitter<any>();
@ -75,22 +78,22 @@ export class DeployImageComponent implements OnInit {
columns = [
{
columnDef: 'partitionNumber',
header: 'Partición',
header: 'labelPartition',
cell: (partition: any) => `${partition.partitionNumber}`
},
{
columnDef: 'size',
header: 'Tamaño',
header: 'labelDiskSize',
cell: (partition: any) => `${partition.size} MB`
},
{
columnDef: 'filesystem',
header: 'Sistema de ficheros',
header: 'fileSystem',
cell: (partition: any) => `${partition.filesystem}`
},
{
columnDef: 'operativeSystem',
header: 'SO',
header: 'sW',
cell: (partition: any) => `${partition.operativeSystem?.name}`
}
];
@ -229,7 +232,7 @@ export class DeployImageComponent implements OnInit {
this.loadImages();
},
(error) => {
console.error('Error al cargar las particiones:', error);
console.error(this.translate.instant('errorLoadingPartitions'), error);
}
);
}
@ -261,7 +264,7 @@ export class DeployImageComponent implements OnInit {
this.images = response?.['hydra:member'] || [];
},
(error) => {
console.error('Error al cargar las imágenes:', error);
console.error(this.translate.instant('errorLoadingImages3'), error);
}
);
}
@ -350,27 +353,27 @@ export class DeployImageComponent implements OnInit {
const needsPartition = this.deployMode !== 'update-cache';
if (!this.selectedClients.length) {
this.toastService.error('Debe seleccionar al menos un cliente');
this.toastService.error(this.translate.instant('youMustSelectAtLeastOne'));
return;
}
if (!this.selectedImage && this.imageType !== 'git') {
this.toastService.error('Debe seleccionar una imagen');
this.toastService.error(this.translate.instant('youMustSelectAnImage'));
return;
}
if (!this.selectedMethod && this.imageType !== 'git') {
this.toastService.error('Debe seleccionar un método');
this.toastService.error(this.translate.instant('youMustSelectAMethod'));
return;
}
if (needsPartition && !this.selectedPartition) {
this.toastService.error('Debe seleccionar una partición');
this.toastService.error(this.translate.instant('youMustSelectAPartition'));
return;
}
if (this.imageType === 'git' && !this.selectedCommit) {
this.toastService.error('Debe seleccionar un commit');
this.toastService.error(this.translate.instant('youMustSelectACommit'));
return;
}
@ -381,7 +384,7 @@ export class DeployImageComponent implements OnInit {
backdropClass: 'non-clickable-backdrop'
});
this.toastService.info('Preparando petición de despliegue');
this.toastService.info(this.translate.instant('preparingDeploymentRequest'));
dialogRef.afterClosed().subscribe(result => {
if (result !== undefined) {
@ -428,7 +431,7 @@ export class DeployImageComponent implements OnInit {
this.http.post(url, payload)
.subscribe({
next: (response) => {
this.toastService.success('Petición de despliegue enviada correctamente');
this.toastService.success(this.translate.instant('deployRequestSentCorrectly'));
this.loading = false;
this.router.navigate(['/commands-logs']);
},
@ -573,7 +576,7 @@ export class DeployImageComponent implements OnInit {
type: 'deploy-image',
}).subscribe({
next: () => {
this.toastService.success('Script añadido con éxito a la tarea');
this.toastService.success(this.translate.instant('scriptSuccessfullyAddedTo'));
},
error: (error) => {
this.toastService.error(error.error['hydra:description']);
@ -632,7 +635,7 @@ export class DeployImageComponent implements OnInit {
}
},
error => {
this.toastService.error('Error al cargar los repositorios git');
this.toastService.error(this.translate.instant('errorLoadingRepositories1'));
this.loadingRepositories = false;
}
);
@ -662,7 +665,7 @@ export class DeployImageComponent implements OnInit {
}
},
error => {
this.toastService.error('Error al cargar las ramas');
this.toastService.error(this.translate.instant('errorLoadingBranches'));
this.loadingBranches = false;
}
);
@ -694,7 +697,7 @@ export class DeployImageComponent implements OnInit {
}
},
error => {
this.toastService.error('Error al cargar los commits');
this.toastService.error(this.translate.instant('errorLoadingCommits'));
this.loadingCommits = false;
}
);

View File

@ -7,17 +7,17 @@
<!-- Multicast options -->
<div *ngIf="isMulticast()" class="config-grid">
<mat-form-field appearance="fill">
<mat-label>Dirección</mat-label>
<mat-label>{{ 'labelAddress' | translate }}</mat-label>
<input matInput [(ngModel)]="mcastIp" required>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Puerto</mat-label>
<mat-label>{{ 'labelPort' | translate }}</mat-label>
<input matInput [(ngModel)]="mcastPort" type="number" required>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Modo Multicast</mat-label>
<mat-label>{{ 'labelMcastMode' | translate }}</mat-label>
<mat-select [(ngModel)]="mcastMode" required>
<mat-option *ngFor="let option of data.multicastModeOptions" [value]="option.value">
{{ option.name }}
@ -26,17 +26,17 @@
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Velocidad</mat-label>
<mat-label>{{ 'labelSpeed' | translate }}</mat-label>
<input matInput [(ngModel)]="mcastSpeed" type="number" required>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Máx. Clientes</mat-label>
<mat-label>{{ 'maxClients' | translate }}</mat-label>
<input matInput [(ngModel)]="mcastMaxClients" type="number" required>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Tiempo máx. espera</mat-label>
<mat-label>{{ 'maxWaitTime' | translate }}</mat-label>
<input matInput [(ngModel)]="mcastMaxTime" type="number" required>
</mat-form-field>
</div>
@ -44,7 +44,7 @@
<!-- P2P options -->
<div *ngIf="isP2P()" class="config-grid">
<mat-form-field appearance="fill">
<mat-label>Modo P2P</mat-label>
<mat-label>{{ 'labelP2pMode' | translate }}</mat-label>
<mat-select [(ngModel)]="p2pMode" required>
<mat-option *ngFor="let option of data.p2pModeOptions" [value]="option.value">
{{ option.name }}
@ -53,15 +53,14 @@
</mat-form-field>
<mat-form-field appearance="fill" *ngIf="p2pMode === 'seeder'">
<mat-label>Semilla (minutos)</mat-label>
<mat-label>{{ 'seedMinutes' | translate }}</mat-label>
<input matInput [(ngModel)]="p2pTime" type="number" required>
</mat-form-field>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">Cancelar</button>
<button mat-button (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
<button mat-flat-button color="primary" [disabled]="!isValid()" (click)="onSave()">
Guardar configuración
</button>
{{ 'buttonSaveConfig' | translate }}</button>
</mat-dialog-actions>

View File

@ -9,30 +9,27 @@
<div class="destination-badge">
<mat-icon class="destination-icon">cloud_download</mat-icon>
<div class="destination-content">
<span class="destination-label">Destino</span>
<span class="destination-label">{{ 'labelDestination' | translate }}</span>
<span class="destination-value">{{ runScriptTitle }}</span>
</div>
</div>
</div>
</div>
<div class="button-row">
<button class="action-button" [disabled]="!selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0 || partitionValidationStatus === 'error'" (click)="save()">Ejecutar</button>
<button class="action-button" [disabled]="!selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0 || partitionValidationStatus === 'error'" (click)="save()">{{ 'buttonExecute' | translate }}</button>
<button class="action-button secondary-button" (click)="generateInstructions()">
Generar instrucciones
</button>
{{ 'generateInstructions' | translate }}</button>
<button class="action-button" color="accent" [disabled]="!selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="openScheduleModal()">
Opciones de programación
</button>
{{ 'schedulingOptions' | translate }}</button>
</div>
</div>
<div *ngIf="showInstructions" class="instructions-box instructions-top" (click)="$event.stopPropagation()">
<mat-card class="instructions-card">
<mat-card-title>
Script generado
<span style="float: right;">
<button mat-icon-button matTooltip="Copiar" (click)="copyInstructions()"><mat-icon>content_copy</mat-icon></button>
<button mat-icon-button matTooltip="Cerrar" (click)="showInstructions = false"><mat-icon>close</mat-icon></button>
{{ 'generatedScript' | translate }}<span style="float: right;">
<button mat-icon-button [matTooltip]="'buttonCopy' | translate" (click)="copyInstructions()"><mat-icon>content_copy</mat-icon></button>
<button mat-icon-button [matTooltip]="'buttonClose' | translate" (click)="showInstructions = false"><mat-icon>close</mat-icon></button>
</span>
</mat-card-title>
<mat-card-content>
@ -44,18 +41,16 @@
<div class="select-container">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title>
<mat-panel-title> {{ 'labelClients' | translate }}</mat-panel-title>
<mat-panel-description>
Listado de clientes donde se realizará el particionado
<mat-icon>desktop_windows</mat-icon>
{{ 'clientListPartition' | translate }}<mat-icon>desktop_windows</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header>
<div class="section-hint" (click)="$event.stopPropagation()">
<mat-icon>info</mat-icon>
<span>
Puedes marcar o desmarcar un cliente haciendo clic en el área del ordenador. Además, selecciona con "Modelo" el equipo que actuará como modelo.
</span>
{{ 'markClientModelInstruction2' | translate }}</span>
</div>
<div class="button-row">
@ -90,8 +85,7 @@
<mat-radio-button [value]="client"
color="primary"
(click)="$event.stopPropagation()">
Modelo
</mat-radio-button>
{{ 'labelModel' | translate }}</mat-radio-button>
</mat-radio-group>
</div>
@ -105,7 +99,7 @@
<div class="disk-and-partitions-container" id="disk-selector">
<div class="disk-toolbar">
<mat-form-field appearance="fill" class="disk-toolbar-select">
<mat-label>Disco</mat-label>
<mat-label>{{ 'labelDisk' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedDiskNumber" (selectionChange)="onDiskSelectionChange()">
<mat-option *ngFor="let disk of disks" [value]="disk.diskNumber">
<div class="disk-option">
@ -123,15 +117,15 @@
<div class="disk-toolbar-info" *ngIf="selectedDisk">
<div class="disk-stat">
<span class="disk-stat-label">Usado</span>
<span class="disk-stat-label">{{ 'labelDiskUsed' | translate }}</span>
<span class="disk-stat-value used">{{ selectedDisk.used | number:'1.0-0' }} MB</span>
</div>
<div class="disk-stat">
<span class="disk-stat-label">Libre</span>
<span class="disk-stat-label">{{ 'labelFree' | translate }}</span>
<span class="disk-stat-value free" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">{{ (selectedDisk.totalDiskSize - selectedDisk.used) | number:'1.0-0' }} MB</span>
</div>
<div class="disk-stat">
<span class="disk-stat-label">Total</span>
<span class="disk-stat-label">{{ 'labelDiskTotal' | translate }}</span>
<span class="disk-stat-value">{{ selectedDisk.totalDiskSize | number:'1.0-0' }} MB</span>
</div>
<div class="disk-usage-bar-inline">
@ -154,15 +148,14 @@
</div>
<button class="action-button add-partition-btn" *ngIf="selectedDisk" (click)="addPartition(selectedDisk.diskNumber)">
<mat-icon>add</mat-icon> Añadir partición
</button>
<mat-icon>add</mat-icon> {{ 'addPartition' | translate }}</button>
</div>
<div class="no-disks-message" *ngIf="!disks || disks.length === 0">
<mat-icon class="warning-icon">warning</mat-icon>
<div class="message-text">
<span class="message-title">No hay discos disponibles</span>
<span class="message-subtitle">Asegúrate de que el cliente modelo tenga discos configurados</span>
<span class="message-title">{{ 'noDisksAvailable' | translate }}</span>
<span class="message-subtitle">{{ 'ensureModelClientHasDisks' | translate }}</span>
</div>
</div>
</div>
@ -176,7 +169,7 @@
<div class="validation-status" [ngClass]="partitionValidationStatus">
<mat-icon *ngIf="partitionValidationStatus === 'loading'" class="validation-icon loading">hourglass_empty</mat-icon>
<mat-icon *ngIf="partitionValidationStatus === 'success'" class="validation-icon success">check_circle</mat-icon>
<mat-icon *ngIf="partitionValidationStatus === 'error'" class="validation-icon error">error</mat-icon>
<mat-icon *ngIf="partitionValidationStatus === 'error'" class="validation-icon error">{{ 'labelError' | translate }}</mat-icon>
<span class="validation-message" *ngIf="partitionValidationMessage">{{ partitionValidationMessage }}</span>
</div>
</div>
@ -184,12 +177,12 @@
<table class="partition-table" id="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>{{ 'labelPartition' | translate }}</th>
<th>{{ 'partitionType' | translate }}</th>
<th>{{ 'labelFsShort' | translate }}</th>
<th>{{ 'labelSizeMb' | translate }}</th>
<th>{{ 'labelSizePercentage' | translate }}</th>
<th>{{ 'buttonFormat' | translate }}</th>
</tr>
</thead>
<tbody>
@ -232,7 +225,7 @@
<div class="chart-container" *ngIf="selectedDisk" #chartContainer>
<div class="chart-header">
<h3>Distribución de Particiones</h3>
<h3>{{ 'partitionDistribution' | translate }}</h3>
</div>
<div class="chart-wrapper">
<ngx-charts-pie-chart

View File

@ -1,4 +1,6 @@
import {Component, EventEmitter, Inject, Input, OnInit, Output, AfterViewInit, OnDestroy, ViewChild, ElementRef, HostListener} from '@angular/core';
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {Component, EventEmitter, Inject, Input, OnInit, Output, AfterViewInit, OnDestroy, ViewChild, ElementRef, HostListener } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr';
import {ActivatedRoute, Router} from "@angular/router";
@ -32,6 +34,7 @@ interface Partition {
styleUrls: ['./partition-assistant.component.css']
})
export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDestroy{
public translate = inject(TranslateService);
baseUrl: string;
private apiUrl: string;
@Output() dataChange = new EventEmitter<any>();
@ -190,7 +193,7 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
this.initializeDisks();
},
(error) => {
console.error('Error al cargar los datos del cliente:', error);
console.error(this.translate.instant('errorLoadingData1'), error);
}
);
}
@ -412,7 +415,7 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
this.resizeChart();
}, 100);
} else {
this.toastService.error('No hay suficiente espacio libre en el disco para crear una nueva partición.');
this.toastService.error(this.translate.instant('notEnoughFreeSpace'));
}
}
}
@ -457,7 +460,7 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
}
if (!this.selectedDisk) {
this.toastService.error('No se ha seleccionado un disco.');
this.toastService.error(this.translate.instant('noOneHasBeenSelected'));
return;
}
@ -466,14 +469,14 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
.reduce((sum: any, partition: any) => sum + partition.size, 0);
if (totalPartitionSize > this.selectedDisk.totalDiskSize) {
this.toastService.error('El tamaño total de las particiones en el disco seleccionado excede el tamaño total del disco.');
this.toastService.error(this.translate.instant('theTotalSizeOfThe'));
return;
}
const modifiedPartitions = this.selectedDisk.partitions.filter((partition: { removed: any; format: any; }) => !partition.removed || partition.format);
if (modifiedPartitions.length === 0) {
this.toastService.info('No hay cambios para guardar en el disco seleccionado.');
this.toastService.info(this.translate.instant('noChangesToSave'));
return;
}
@ -507,13 +510,13 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
this.http.post(this.apiUrl, bulkPayload).subscribe(
(response) => {
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
this.toastService.success(this.translate.instant('partitionsSuccessfullyCreatedForThe'));
this.loading = false;
this.router.navigate(['/commands-logs']);
},
(error) => {
this.loading = false;
this.toastService.error('Error al crear las particiones.');
this.toastService.error(this.translate.instant('errorCreatingPartitions'));
}
);
}
@ -680,7 +683,7 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
if (modifiedPartitions.length === 0) {
this.loading = false;
this.toastService.info('No hay cambios para guardar en el disco seleccionado.');
this.toastService.info(this.translate.instant('noChangesToSave'));
return;
}
@ -711,7 +714,7 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
type: 'partition-assistant',
}).subscribe({
next: () => {
this.toastService.success('Script añadido con éxito a la tarea');
this.toastService.success(this.translate.instant('scriptSuccessfullyAddedTo'));
},
error: (error) => {
this.toastService.error(error.error['hydra:description']);
@ -723,7 +726,7 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
generateInstructions(): void {
if (!this.selectedModelClient || !this.selectedDisk) {
this.toastService.error('Selecciona un cliente modelo y un disco.');
this.toastService.error(this.translate.instant('selectAModelClientAnd'));
return;
}
@ -777,8 +780,8 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
copyInstructions(): void {
if (!this.generatedInstructions) { return; }
navigator.clipboard?.writeText(this.generatedInstructions)
.then(() => this.toastService.success('Script copiado al portapapeles'))
.catch(() => this.toastService.error('No se pudo copiar el script.'));
.then(() => this.toastService.success(this.translate.instant('scriptCopiedToClipboard')))
.catch(() => this.toastService.error(this.translate.instant('couldNotCopy')));
}
@ -837,7 +840,7 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
});
console.log('Scroll hacia botón ejecutar completado');
} else {
console.error('No se encontró el botón execute-button');
console.error(this.translate.instant('theButtonWasNotFound'));
}
}

View File

@ -9,38 +9,35 @@
<div class="destination-badge">
<mat-icon class="destination-icon">terminal</mat-icon>
<div class="destination-content">
<span class="destination-label">Destino</span>
<span class="destination-label">{{ 'labelDestination' | translate }}</span>
<span class="destination-value">{{ runScriptTitle }}</span>
</div>
</div>
</div>
</div>
<div class="button-row">
<button class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript) || (commandType === 'new' && !newScript.trim()) || loading" (click)="save()">Ejecutar</button>
<button class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript) || (commandType === 'new' && !newScript.trim()) || loading" (click)="save()">{{ 'buttonExecute' | translate }}</button>
</div>
<div class="button-row">
<button color="accent" class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript) || (commandType === 'new' && !newScript.trim()) || loading" (click)="openScheduleModal()">
Opciones de programación
</button>
{{ 'schedulingOptions' | translate }}</button>
</div>
</div>
<div class="select-container">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title>
<mat-panel-title> {{ 'labelClients' | translate }}</mat-panel-title>
<mat-panel-description>
Listado de clientes donde se ejecutara el script seleccionado
<mat-icon>desktop_windows</mat-icon>
{{ 'clientListExecuteScript' | translate }}<mat-icon>desktop_windows</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header>
<div class="section-hint" (click)="$event.stopPropagation()">
<mat-icon>info</mat-icon>
<span>
Puedes marcar o desmarcar un cliente haciendo clic en el área del ordenador.
</span>
{{ 'markClientInstruction' | translate }}</span>
</div>
<div class="button-row">
@ -79,24 +76,22 @@
<div class="form-section">
<div class="form-section-title">
<mat-icon>code</mat-icon>
Configuración de script
</div>
{{ 'scriptConfiguration' | translate }}</div>
<div class="run-as-admin-container">
<mat-checkbox [(ngModel)]="runAsAdmin" color="primary">
Ejecutar como administrador
</mat-checkbox>
{{ 'executeAsAdmin' | translate }}</mat-checkbox>
</div>
<div class="action-chips-container">
<mat-chip-listbox [(ngModel)]="commandType" required class="action-chip-listbox">
<mat-chip-option value="new" class="action-chip create-chip firmware-chip" (click)="commandType = 'new'">
<mat-icon>add_circle</mat-icon>
<span>Nuevo Script</span>
<span>{{ 'newScript' | translate }}</span>
</mat-chip-option>
<mat-chip-option value="existing" class="action-chip update-chip firmware-chip" (click)="commandType = 'existing'">
<mat-icon>folder</mat-icon>
<span>Script Guardado</span>
<span>{{ 'savedScript' | translate }}</span>
</mat-chip-option>
</mat-chip-listbox>
</div>
@ -106,8 +101,7 @@
<div class="script-input-header">
<h4>
<mat-icon>edit_note</mat-icon>
Editor de Script
</h4>
{{ 'scriptEditorTitle' | translate }}</h4>
<div class="script-stats" *ngIf="newScript.trim()">
<span class="stat-item">
<mat-icon>format_list_numbered</mat-icon>
@ -125,23 +119,22 @@
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Ingrese el script</mat-label>
<mat-label>{{ 'enterScript' | translate }}</mat-label>
<textarea
matInput
[(ngModel)]="newScript"
(ngModelChange)="onNewScriptChange()"
rows="8"
placeholder="#!/bin/bash&#10;# Escriba su script aquí&#10;echo 'Iniciando script...'&#10;&#10;# Sus comandos aquí"
[placeholder]="'bashScriptBoilerplate'|translate"
class="script-textarea"></textarea>
<mat-hint>Escriba comandos de shell/bash. Use &#64;parametro para variables dinámicas.</mat-hint>
<mat-hint>{{ 'writeShellCommandsHint' | translate }}</mat-hint>
</mat-form-field>
<!-- Vista previa de parámetros detectados en nuevo script -->
<div *ngIf="newScriptParameters.length > 0" class="detected-params">
<h4>
<mat-icon>tune</mat-icon>
Parámetros detectados
</h4>
{{ 'detectedParameters' | translate }}</h4>
<div class="params-grid">
<mat-chip-listbox>
<mat-chip-option *ngFor="let param of newScriptParameters" class="param-chip" selected>
@ -159,8 +152,7 @@
[disabled]="!isScriptValid(newScript)"
(click)="saveNewScript()">
<mat-icon>save</mat-icon>
Guardar Script
</button>
{{ 'buttonSaveScript' | translate }}</button>
</div>
</div>
@ -168,7 +160,7 @@
<div *ngIf="commandType === 'existing'">
<div class="script-selector-container">
<mat-form-field appearance="outline" class="full-width">
<mat-label>Seleccione script a ejecutar</mat-label>
<mat-label>{{ 'selectScriptToRun' | translate }}</mat-label>
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
<mat-option *ngFor="let script of scripts" [value]="script">
<div class="script-option">
@ -177,7 +169,7 @@
</div>
</mat-option>
</mat-select>
<mat-hint *ngIf="scripts.length === 0">No hay scripts guardados disponibles</mat-hint>
<mat-hint *ngIf="scripts.length === 0">{{ 'noSavedScripts' | translate }}</mat-hint>
<mat-hint *ngIf="scripts.length > 0">{{ scripts.length }} script(s) disponible(s)</mat-hint>
</mat-form-field>
</div>
@ -188,8 +180,7 @@
<mat-card-header>
<mat-card-title>
<mat-icon>preview</mat-icon>
Vista previa del script
</mat-card-title>
{{ 'scriptPreview' | translate }}</mat-card-title>
<mat-card-subtitle>{{ selectedScript.name }}</mat-card-subtitle>
</mat-card-header>
@ -206,10 +197,9 @@
<mat-card-header>
<mat-card-title>
<mat-icon>settings</mat-icon>
Parámetros del script
<mat-chip class="params-count">{{ parameterNames.length }}</mat-chip>
{{ 'scriptParameters' | translate }}<mat-chip class="params-count">{{ parameterNames.length }}</mat-chip>
</mat-card-title>
<mat-card-subtitle>Configure los valores para los parámetros requeridos</mat-card-subtitle>
<mat-card-subtitle>{{ 'configureRequiredParameters' | translate }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
@ -225,7 +215,7 @@
[required]="true">
<mat-icon matPrefix>label</mat-icon>
<mat-hint>Parámetro: &#64;{{ paramName }}</mat-hint>
<mat-error *ngIf="!parameters[paramName]">Este parámetro es requerido</mat-error>
<mat-error *ngIf="!parameters[paramName]">{{ 'parameterRequired' | translate }}</mat-error>
</mat-form-field>
</div>
</div>
@ -239,8 +229,8 @@
<div *ngIf="commandType === 'existing' && !selectedScript" class="empty-state">
<div class="empty-state-content">
<mat-icon>folder_open</mat-icon>
<h3>Selecciona un script</h3>
<p>Elige un script guardado de la lista para ver su contenido y configurar parámetros.</p>
<h3>{{ 'selectAScript' | translate }}</h3>
<p>{{ 'selectScriptToView' | translate }}</p>
</div>
</div>
@ -248,8 +238,8 @@
<div *ngIf="commandType === 'new' && !newScript.trim()" class="empty-state">
<div class="empty-state-content">
<mat-icon>edit_note</mat-icon>
<h3>Escribe tu script</h3>
<p>Crea un nuevo script escribiendo comandos de shell/bash en el editor de texto.</p>
<h3>{{ 'writeYourScript' | translate }}</h3>
<p>{{ 'createScriptHint' | translate }}</p>
</div>
</div>
</div>

View File

@ -1,4 +1,6 @@
import {Component, EventEmitter, OnInit, Output} from '@angular/core';
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {Component, EventEmitter, OnInit, Output } from '@angular/core';
import { SelectionModel } from "@angular/cdk/collections";
import { HttpClient } from "@angular/common/http";
import { ToastrService } from "ngx-toastr";
@ -15,6 +17,7 @@ import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confi
styleUrl: './run-script-assistant.component.css'
})
export class RunScriptAssistantComponent implements OnInit{
public translate = inject(TranslateService);
baseUrl: string;
@Output() dataChange = new EventEmitter<any>();
@ -96,7 +99,7 @@ export class RunScriptAssistantComponent implements OnInit{
saveNewScript() {
if (!this.newScript.trim()) {
this.toastService.error('Debe ingresar un script antes de guardar.');
this.toastService.error(this.translate.instant('youMustEnterAScriptBefore'));
return;
}
const dialogRef = this.dialog.open(SaveScriptComponent, {
@ -106,7 +109,7 @@ export class RunScriptAssistantComponent implements OnInit{
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastService.success('Script guardado correctamente');
this.toastService.success(this.translate.instant('scriptSavedSuccessfully'));
}
});
}
@ -205,13 +208,13 @@ export class RunScriptAssistantComponent implements OnInit{
runAsAdmin: this.runAsAdmin
}).subscribe(
response => {
this.toastService.success('Script ejecutado correctamente');
this.toastService.success(this.translate.instant('scriptExecutedSuccessfully'));
this.dataChange.emit();
this.router.navigate(['/commands-logs']);
this.loading = false;
},
error => {
this.toastService.error('Error al ejecutar el script');
this.toastService.error(this.translate.instant('errorWhenRunningScript'));
this.loading = false;
}
);
@ -258,7 +261,7 @@ export class RunScriptAssistantComponent implements OnInit{
type: 'run-script',
}).subscribe({
next: () => {
this.toastService.success('Script añadido con éxito a la tarea');
this.toastService.success(this.translate.instant('scriptSuccessfullyAddedTo'));
},
error: (error) => {
this.toastService.error(error.error['hydra:description']);

View File

@ -1,12 +1,12 @@
<h2 mat-dialog-title>Guardar Comando</h2>
<h2 mat-dialog-title>{{ 'buttonSaveCommand' | translate }}</h2>
<mat-dialog-content>
<p>Introduce un nombre para el comando:</p>
<p>{{ 'enterCommandName' | translate }}</p>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Nombre del Comando</mat-label>
<mat-label>{{ 'commandNameTitle' | translate }}</mat-label>
<input matInput [(ngModel)]="commandName">
</mat-form-field>
</mat-dialog-content>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">Cancelar</button>
<button class="submit-button" (click)="save()">Guardar</button>
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="save()">{{ 'labelSaveButton' | translate }}</button>
</div>

View File

@ -1,4 +1,6 @@
import {Component, Inject} from '@angular/core';
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {Component, Inject } from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {FormBuilder, Validators} from "@angular/forms";
import {HttpClient} from "@angular/common/http";
@ -12,6 +14,7 @@ import {DataService} from "../../../../../commands/main-commands/data.service";
styleUrl: './save-script.component.css'
})
export class SaveScriptComponent {
public translate = inject(TranslateService);
commandName: string = '';
baseUrl: string;
@ -36,7 +39,7 @@ export class SaveScriptComponent {
this.http.post(`${this.baseUrl}/commands`, payload).subscribe(
(response) => {
this.toastService.success('Comando añadido correctamente');
this.toastService.success(this.translate.instant('commandAddedSuccessfully'));
this.dialogRef.close();
},
(error) => {

View File

@ -33,13 +33,13 @@
</div>
<button class="ordinary-button" (click)="openBottomSheet()" joyrideStep="keyStep"
text="{{ 'keyStepText' | translate }}" matTooltipShowDelay="1000">
{{ 'legendButton' | translate }}
{{ 'Legend' | translate }}
</button>
</div>
<!-- Menú desplegable para pantallas pequeñas -->
<div *ngIf="isSmallScreen" class="groups-menu">
<button mat-icon-button [matMenuTriggerFor]="smallScreenMenu" matTooltip="Opciones" matTooltipShowDelay="1000">
<button mat-icon-button [matMenuTriggerFor]="smallScreenMenu" [matTooltip]="'labelOptions' | translate" matTooltipShowDelay="1000">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #smallScreenMenu="matMenu">
@ -53,7 +53,7 @@
{{ 'newMultipleClientButton' | translate }}
</button>
<button mat-menu-item (click)="openBottomSheet()">
{{ 'legendButton' | translate }}
{{ 'Legend' | translate }}
</button>
</mat-menu>
</div>
@ -76,8 +76,8 @@
<mat-form-field class="filter-form-field" appearance="outline">
<mat-label>{{ 'searchTree' | translate }}</mat-label>
<input matInput #treeSearchInput (input)="onTreeFilterInput($event)"
placeholder="Centro, aula, grupos ..." />
<button *ngIf="treeSearchInput.value" mat-icon-button matSuffix aria-label="Clear tree search"
[placeholder]="'placeholderOrgUnitTypes' | translate" />
<button *ngIf="treeSearchInput.value" mat-icon-button matSuffix [attr.aria-label]="'clearTreeSearch' | translate"
(click)="clearTreeSearch(treeSearchInput)">
<mat-icon>close</mat-icon>
</button>
@ -85,8 +85,8 @@
<mat-form-field class="filter-form-field" appearance="outline">
<mat-label>{{ 'searchClient' | translate }}</mat-label>
<input matInput #clientSearchInput (input)="onClientFilterInput($event)"
placeholder="Nombre, IP, estado o MAC" />
<button *ngIf="clientSearchInput.value" mat-icon-button matSuffix aria-label="Clear client search"
[placeholder]="'searchNameIpStatusMac' | translate" />
<button *ngIf="clientSearchInput.value" mat-icon-button matSuffix [attr.aria-label]="'clearClientSearch' | translate"
(click)="clearClientSearch(clientSearchInput)">
<mat-icon>close</mat-icon>
</button>
@ -98,7 +98,7 @@
{{ option.name }}
</mat-option>
</mat-select>
<button *ngIf="clientSearchStatusInput.value" mat-icon-button matSuffix aria-label="Clear tree search"
<button *ngIf="clientSearchStatusInput.value" mat-icon-button matSuffix [attr.aria-label]="'clearTreeSearch' | translate"
(click)="clearStatusFilter($event, clientSearchStatusInput)">
<mat-icon>close</mat-icon>
</button>
@ -116,7 +116,7 @@
<!-- Funcionalidad actualmente deshabilitada-->
<!-- <mat-form-field appearance="outline">
<mat-select (selectionChange)="loadSelectedFilter($event.value)" placeholder="Cargar filtro" disabled>
<mat-select (selectionChange)="loadSelectedFilter($event.value)" [placeholder]="'buttonLoadFilter' | translate" disabled>
<mat-option *ngFor="let savedFilter of savedFilterNames" [value]="savedFilter">
{{ savedFilter[0] }}
</mat-option>
@ -270,11 +270,11 @@
</button>
<button mat-menu-item (click)="onEditNode($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'">
<mat-icon>edit</mat-icon>
<span>{{ 'edit' | translate }}</span>
<span>{{ 'buttonEdit' | translate }}</span>
</button>
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'">
<mat-icon>delete</mat-icon>
<span>{{ 'delete' | translate }}</span>
<span>{{ 'buttonDelete' | translate }}</span>
</button>
<button mat-menu-item (click)="openPartitionTypeModal($event, selectedNode)">
<mat-icon>storage</mat-icon>
@ -300,7 +300,7 @@
<!-- CLIENTS HEADER -->
<div class="clients-view-header">
<span [ngStyle]="{ visibility: isLoadingClients ? 'hidden' : 'visible' }" class="clients-title-name">
{{ 'clients' | translate }}
{{ 'labelClients' | translate }}
<strong>{{ selectedNode?.name }}</strong>
</span>
<div class="view-type-container">
@ -308,7 +308,7 @@
(change)="toggleAllCards()"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()"
matTooltip="Seleccionar todos">
[matTooltip]="'buttonSelectAll' | translate">
</mat-checkbox>
<button class="action-button" [disabled]="selection.selected.length === 0" (click)="changeParent($event)"
matTooltip="{{ 'moveClientsTooltip' | translate }}" matTooltipShowDelay="1000">
@ -325,7 +325,7 @@
<mat-icon>delete</mat-icon>
</button>
<mat-button-toggle-group name="viewType" aria-label="View Type" [hideSingleSelectionIndicator]="true"
<mat-button-toggle-group name="viewType" [attr.aria-label]="'viewType' | translate" [hideSingleSelectionIndicator]="true"
(change)="toggleView($event.value)" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
<mat-button-toggle value="list" [disabled]="currentView === 'list'">
<mat-icon>list</mat-icon> <span class="type-view-text">{{ 'vistalista' | translate }}</span>
@ -419,16 +419,16 @@
<mat-menu #clientMenu="matMenu">
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)" *ngIf="auth.userCategory !== 'ou-minimal'">
<mat-icon>edit</mat-icon>
<span>{{ 'edit' | translate }}</span>
<span>{{ 'buttonEdit' | translate }}</span>
</button>
<button mat-menu-item (click)="onShowClientDetail($event, client)">
<mat-icon>visibility</mat-icon>
<span>{{ 'viewDetails' | translate }}</span>
<span>{{ 'buttonViewDetails' | translate }}</span>
</button>
<button mat-menu-item *ngIf="!syncingClientIds.has(client.uuid)"
(click)="getStatus(client, selectedNode)">
<mat-icon>sync</mat-icon>
<span>{{ 'sync' | translate }}</span>
<span>{{ 'buttonSync' | translate }}</span>
</button>
<button mat-menu-item (click)="openClientTaskLogs($event, client)">
<mat-icon>list_alt</mat-icon>
@ -440,15 +440,15 @@
</button>
<button mat-menu-item (click)="openClientLogsInNewTab($event, client)">
<mat-icon>article</mat-icon>
<span>Logs en tiempo real</span>
<span>{{ 'realTimeLogs' | translate }}</span>
</button>
<button mat-menu-item (click)="openClientLogsModal($event, client)">
<mat-icon>analytics</mat-icon>
<span>Logs de cliente</span>
<span>{{ 'clientLogs' | translate }}</span>
</button>
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
<mat-icon>delete</mat-icon>
<span>{{ 'delete' | translate }}</span>
<span>{{ 'buttonDelete' | translate }}</span>
</button>
</mat-menu>
</div>
@ -471,7 +471,7 @@
<span>{{ 'showingResults' | translate: { from: getPaginationFrom(), to: getPaginationTo(), total: getPaginationTotal() } }}</span>
</div>
<div class="table-actions">
<button mat-icon-button (click)="refreshClientData()" matTooltip="{{ 'refresh' | translate }}">
<button mat-icon-button (click)="refreshClientData()" matTooltip="{{ 'buttonUpdate' | translate }}">
<mat-icon>refresh</mat-icon>
</button>
<button mat-icon-button (click)="exportToCSV()" matTooltip="{{ 'exportCSV' | translate }}">
@ -498,7 +498,7 @@
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>
<div class="column-header">
<span>{{ 'status' | translate }}</span>
<span>{{ 'labelState' | translate }}</span>
<button mat-icon-button (click)="sortColumn('status')" class="sort-button">
<mat-icon>{{ getSortIcon('status') }}</mat-icon>
</button>
@ -535,7 +535,7 @@
<ng-container matColumnDef="ip">
<th mat-header-cell *matHeaderCellDef>
<div class="column-header">
<span>IP</span>
<span>{{ 'labelIpCaps' | translate }}</span>
<button mat-icon-button (click)="sortColumn('ip')" class="sort-button">
<mat-icon>{{ getSortIcon('ip') }}</mat-icon>
</button>
@ -567,7 +567,7 @@
</ng-container>
<ng-container matColumnDef="oglive">
<th mat-header-cell *matHeaderCellDef> OG Live </th>
<th mat-header-cell *matHeaderCellDef> {{ 'ogLiveTitle' | translate }}</th>
<td mat-cell *matCellDef="let client">
<div class="oglive-cell">
<span class="oglive-kernel">{{ client.ogLive?.kernel }}</span>
@ -590,7 +590,7 @@
</ng-container>
<ng-container matColumnDef="parentName">
<th mat-header-cell *matHeaderCellDef> {{ 'parent' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'labelEditOrgUnitParent' | translate }} </th>
<td mat-cell *matCellDef="let client" class="truncate-cell-medium" [matTooltip]="client.parentName"> {{ client.parentName }} </td>
</ng-container>
@ -608,22 +608,22 @@
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
[runScriptContext]="getRunScriptContext([client])" matTooltip="{{ 'executeCommand' | translate }}">
</app-execute-command>
<button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)" matTooltip="{{ 'viewDetails' | translate }}">
<button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)" matTooltip="{{ 'buttonViewDetails' | translate }}">
<mat-icon>visibility</mat-icon>
</button>
</div>
<mat-menu #clientMenu="matMenu">
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)" *ngIf="auth.userCategory !== 'ou-minimal'">
<mat-icon>edit</mat-icon>
<span>{{ 'edit' | translate }}</span>
<span>{{ 'buttonEdit' | translate }}</span>
</button>
<button mat-menu-item (click)="onShowClientDetail($event, client)">
<mat-icon>visibility</mat-icon>
<span>{{ 'viewDetails' | translate }}</span>
<span>{{ 'buttonViewDetails' | translate }}</span>
</button>
<button mat-menu-item (click)="getStatus(client, selectedNode)">
<mat-icon>sync</mat-icon>
<span>{{ 'sync' | translate }}</span>
<span>{{ 'buttonSync' | translate }}</span>
</button>
<button mat-menu-item (click)="openClientTaskLogs($event, client)">
<mat-icon>list_alt</mat-icon>
@ -635,15 +635,15 @@
</button>
<button mat-menu-item *ngIf="client.status === 'og-live'" (click)="openClientLogsInNewTab($event, client)">
<mat-icon>article</mat-icon>
<span>Logs en tiempo real</span>
<span>{{ 'realTimeLogs' | translate }}</span>
</button>
<button mat-menu-item (click)="openClientLogsModal($event, client)">
<mat-icon>analytics</mat-icon>
<span>Logs de cliente</span>
<span>{{ 'clientLogs' | translate }}</span>
</button>
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
<mat-icon>delete</mat-icon>
<span>{{ 'delete' | translate }}</span>
<span>{{ 'buttonDelete' | translate }}</span>
</button>
</mat-menu>
</td>

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Component, OnInit, OnDestroy, ViewChild, QueryList, ViewChildren, ChangeDetectorRef } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Router } from '@angular/router';
@ -48,6 +50,7 @@ enum NodeType {
styleUrls: ['./groups.component.css'],
})
export class GroupsComponent implements OnInit, OnDestroy {
public translate = inject(TranslateService);
@ViewChildren(MatMenuTrigger) menuTriggers!: QueryList<MatMenuTrigger>;
isSmallScreen: boolean = false;
baseUrl: string;
@ -283,7 +286,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
// this.savedFilterNames = data.map((filter: { name: string; uuid: string; }) => [filter.name, filter.uuid]);
// },
// (error) => {
// console.error('Error fetching filters:', error);
// console.error(this.translate.instant('errorFetchingFilters1'), error);
// }
// )
// );
@ -297,7 +300,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.savedFilterNames = data.map((filter: { name: string; uuid: string; }) => [filter.name, filter.uuid]);
},
(error) => {
console.error('Error fetching filters:', error);
console.error(this.translate.instant('errorFetchingFilters1'), error);
}
)
);
@ -350,8 +353,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
}
},
error: (error) => {
console.error('Error fetching organizational units', error);
this.toastr.error('Ocurrió un error al cargar las unidades organizativas');
console.error(this.translate.instant('errorFetchingOrganizationalUnits'), error);
this.toastr.error(this.translate.instant('anErrorOccurredWhileLoading'));
},
});
}
@ -561,7 +564,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
console.log('Nodo padre encontrado para actualización:', parentNode);
this.refreshData(parentNode.uuid);
} else {
console.error('No se encontró el nodo padre después de la creación masiva.');
console.error(this.translate.instant('nodeNotFound'));
}
}
this.loading = false;
@ -635,7 +638,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
}
},
error: (error) => {
console.error('Error deleting entity:', error);
console.error(this.translate.instant('errorDeletingEntity'), error);
const entityType = type === NodeType.Client ? 'cliente' : 'entidad';
this.toastr.error(`Error al eliminar el ${entityType}`);
},
@ -675,7 +678,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
});
},
(error) => {
console.error('Error fetching room data:', error);
console.error(this.translate.instant('errorFetchingRoomData'), error);
}
)
);
@ -685,7 +688,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
executeCommand(command: Command, selectedNode: TreeNode | null): void {
this.loading = true;
if (!selectedNode) {
this.toastr.error('No hay un nodo seleccionado.');
this.toastr.error(this.translate.instant('thereIsNoNodeSelected'));
return;
} else {
this.toastr.success(`Ejecutando comando: ${command.name} en ${selectedNode.name}`);
@ -834,7 +837,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.subscriptions.add(
this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe(
() => {
this.toastr.success('Cliente actualizado correctamente');
this.toastr.success(this.translate.instant('clientUpdatedSuccessfully'));
this.syncingClientIds.delete(client.uuid);
this.refreshData(parentNodeId)
},
@ -869,12 +872,12 @@ export class GroupsComponent implements OnInit, OnDestroy {
});
this.arrayClients = this.selectedClients.data;
this.calculateLocalStats();
this.toastr.success('Estado del aula actualizado');
this.toastr.success(this.translate.instant('classroomStatusUpdated'));
},
error: () => {
this.isCheckingNodeStatus = false;
this.selectedClients.data.forEach(c => c.uuid && this.syncingClientIds.delete(c.uuid));
this.toastr.error('Error al comprobar el estado del aula');
this.toastr.error(this.translate.instant('errorCheckingStatus'));
},
})
);
@ -1054,14 +1057,14 @@ export class GroupsComponent implements OnInit, OnDestroy {
`);
}
} else {
this.toastr.error('No se puede acceder a los logs: IP del cliente no disponible', 'Error');
this.toastr.error(this.translate.instant('cannotAccess'), 'Error');
}
}
openClientLogsModal(event: MouseEvent, client: Client): void {
event.stopPropagation();
if (!client.mac) {
this.toastr.error('No se puede acceder a los logs: MAC del cliente no disponible', 'Error');
this.toastr.error(this.translate.instant('cantAccess1'), 'Error');
return;
}
@ -1088,7 +1091,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
const allClients = response['hydra:member'] || [];
if (allClients.length === 0) {
this.toastr.warning('Esta unidad organizativa no tiene clientes');
this.toastr.warning(this.translate.instant('thisOrganizationalUnitDoesNotHave'));
return;
}
@ -1113,8 +1116,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
});
},
error: (error) => {
console.error('Error al obtener los clientes de la unidad organizativa:', error);
this.toastr.error('Error al cargar los clientes de la unidad organizativa');
console.error(this.translate.instant('errorGettingClients1'), error);
this.toastr.error(this.translate.instant('errorLoadingClients'));
this.loading = false;
}
});
@ -1135,7 +1138,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
refreshClientData(): void {
this.fetchClientsForNode(this.selectedNode);
this.toastr.success('Datos actualizados', 'Éxito');
this.toastr.success(this.translate.instant('updatedData'), 'Éxito');
}
exportToCSV(): void {
@ -1164,7 +1167,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
link.click();
document.body.removeChild(link);
this.toastr.success('Archivo CSV exportado correctamente', 'Éxito');
this.toastr.success(this.translate.instant('csvFileExportedSuccessfully'), 'Éxito');
}
private calculateLocalStats(): void {
@ -1318,7 +1321,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
event.stopPropagation();
if (this.selection.selected.length === 0) {
this.toastr.warning('No hay clientes seleccionados para eliminar');
this.toastr.warning(this.translate.instant('thereAreNoClientsSelectedFor'));
return;
}
@ -1355,8 +1358,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
},
error: (error) => {
this.loading = false;
console.error('Error deleting multiple clients:', error);
this.toastr.error('Error al eliminar los clientes seleccionados');
console.error(this.translate.instant('errorDeletingMultipleClients1'), error);
this.toastr.error(this.translate.instant('errorDeletingClients'));
}
});
}

View File

@ -1,3 +1,5 @@
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Injectable } from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
@ -9,6 +11,7 @@ import { ConfigService } from '@services/config.service';
providedIn: 'root'
})
export class DataService {
public translate = inject(TranslateService);
baseUrl: string;
private apiUrl: string;
@ -35,7 +38,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching unidades organizativas', error);
console.error(this.translate.instant('errorFetchingOrganizationalUnits'), error);
return throwError(error);
})
);
@ -51,7 +54,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching children', error);
console.error(this.translate.instant('errorFetchingChildren'), error);
return throwError(error);
})
);
@ -67,7 +70,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching clients', error);
console.error(this.translate.instant('errorFetchingClients'), error);
return throwError(error);
})
);
@ -84,7 +87,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching clients', error);
console.error(this.translate.instant('errorFetchingClients'), error);
return throwError(error);
})
);
@ -101,7 +104,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching clients', error);
console.error(this.translate.instant('errorFetchingClients'), error);
return throwError(error);
})
);
@ -118,7 +121,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching clients', error);
console.error(this.translate.instant('errorFetchingClients'), error);
return throwError(error);
})
);
@ -132,7 +135,7 @@ export class DataService {
console.log('DELETE URL:', url); // Depuración
return this.http.delete<void>(url).pipe(
catchError(error => {
console.error('Error deleting element', error);
console.error(this.translate.instant('errorDeletingElement'), error);
return throwError(error);
})
);
@ -144,7 +147,7 @@ export class DataService {
// @ts-ignore
return this.http.post<void>(url).pipe(
catchError(error => {
console.error('Error deleting element', error);
console.error(this.translate.instant('errorDeletingElement'), error);
return throwError(error);
})
);
@ -185,7 +188,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching data', error);
console.error(this.translate.instant('fetchingDataError'), error);
return throwError(error);
})
);
@ -201,7 +204,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching filters', error);
console.error(this.translate.instant('errorFetchingFilters'), error);
return throwError(error);
})
);
@ -219,7 +222,7 @@ export class DataService {
}
}),
catchError(error => {
console.error('Error fetching filters', error);
console.error(this.translate.instant('errorFetchingFilters'), error);
return throwError(error);
})
);
@ -229,7 +232,7 @@ export class DataService {
const url = `${this.baseUrl}/organizational-units/${id}`;
return this.http.get<any>(url).pipe(
catchError(error => {
console.error('Error fetching organizational unit', error);
console.error(this.translate.instant('errorFetchingOrganizationalUnit'), error);
return throwError(error);
})
);
@ -255,7 +258,7 @@ export class DataService {
return this.http.post<any>(url, body).pipe(
catchError(error => {
console.error('Error deleting multiple clients', error);
console.error(this.translate.instant('errorDeletingMultipleClients'), error);
return throwError(error);
})
);

View File

@ -1,8 +1,8 @@
<h2 mat-dialog-title>Mover clientes a:</h2>
<h2 mat-dialog-title>{{ 'moveClientsTo' | translate }}</h2>
<mat-dialog-content>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione aula</mat-label>
<mat-label>{{ 'selectClassroomPrompt' | translate }}</mat-label>
<mat-select [(ngModel)]="newOU">
<mat-option *ngFor="let unit of units" [value]="unit">{{ unit.name }}</mat-option>
</mat-select>
@ -10,6 +10,6 @@
</mat-dialog-content>
<div class="action-container">
<button class="ordinary-button" (click)="close()">Cancelar</button>
<button class="submit-button" (click)="save()" [disabled]="!newOU">Continuar</button>
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="save()" [disabled]="!newOU">{{ 'buttonContinue' | translate }}</button>
</div>

View File

@ -1,4 +1,6 @@
import {Component, Inject, OnInit} from '@angular/core';
import { inject } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {Component, Inject, OnInit } from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ToastrService} from "ngx-toastr";
@ -11,6 +13,7 @@ import {ConfigService} from "@services/config.service";
styleUrl: './change-parent.component.css'
})
export class ChangeParentComponent implements OnInit {
public translate = inject(TranslateService);
baseUrl: string;
loading: boolean = true;
units: any[] = [];
@ -38,7 +41,7 @@ export class ChangeParentComponent implements OnInit {
this.units = response['hydra:member'];
this.loading = false;
},
error => console.error('Error fetching organizational units:', error)
error => console.error(this.translate.instant('errorFetchingOrganizationalUnits1'), error)
);
}
@ -48,7 +51,7 @@ export class ChangeParentComponent implements OnInit {
organizationalUnit: this.newOU['@id']
}).subscribe({
next: (response) => {
this.toastService.success('Parent changed successfully');
this.toastService.success(this.translate.instant('parentChangedSuccessfully'));
this.dialogRef.close(true);
},
error: error => {

Some files were not shown because too many files have changed in this diff Show More