New minor version. Several changes. New release. #97
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -1,5 +1,29 @@
|
|||
# Changelog
|
||||
|
||||
## [1.8.2] - 2026-07-27
|
||||
|
||||
### Added
|
||||
- Task Logs (`task-logs.component`): Consume `output` and `finishedAt` properties from Mercure events to update trace outputs in real-time without reloading the page.
|
||||
|
||||
### Fixed
|
||||
- Creación de imágenes (`create-image.component`): Al seleccionar la acción "Actualizar Imagen", ahora se filtran correctamente las imágenes para mostrar solo aquellas que pertenecen al repositorio objetivo asignado al cliente.
|
||||
- Menú de opciones de cliente (`groups.component.html`): Unificado el menú de acciones (tres puntos verticales) usando `matMenuContent`. Se elimina la duplicidad de código que causaba divergencias de comportamiento entre la vista tarjeta y la vista lista. Ahora la opción de "Logs en tiempo real" está siempre disponible en ambas vistas, al igual que los bloqueos al sincronizar.
|
||||
- Detalles de tareas (`OutputDialogComponent`): Ampliado el ancho de la ventana modal de detalles a `70vw` para facilitar la lectura de trazas largas y comandos de OGAgent/OGLive.
|
||||
- Repositorios Git (`show-git-images.component`): Corregido el cierre abrupto de la ventana modal al borrar un repositorio. Ahora, la interfaz recarga la lista de repositorios, permitiendo seguir operando sin tener que volver a abrir el gestor.
|
||||
|
||||
## [1.8.1] - 2026-07-17
|
||||
|
||||
### Added
|
||||
- Formulario de despliegue (`Deploy Image`): Añadido selector de repositorio origen (OG Repository) al menú de despliegue para permitir al usuario elegir desde dónde descargar la imagen.
|
||||
|
||||
### Changed
|
||||
- Modal de partición de arranque (`boot-so-partition`): Refactorizado para usar el componente compartido `app-disk-partition-selector`, eliminando código duplicado y unificando el diseño y comportamiento de la selección de discos y particiones con el resto de modales.
|
||||
|
||||
### Fixed
|
||||
- Detalles de tareas (`OutputDialogComponent`): Corregido el formateo de los detalles mostrados desde los agentes (OGAgent) para renderizar el texto en crudo manteniendo los saltos de línea (usando pre-wrap por CSS y <pre> tag), y dándole formato monoespaciado en lugar de intentar forzar un parseo estricto JSON que rompía la salida de comandos.
|
||||
- Formulario de despliegue (`Deploy Image`): Solucionado el problema que ocultaba las comandos (instructions) cuando el despliegue era en modo caché. El código ha sido refactorizado y limpiado usando directivas estructurales estándar de Angular (`*ngIf`, `*ngFor`).
|
||||
- Roles: Permisos actualizados en vistas para reflejar recientes cambios de backend en la gestión de perfiles de software.
|
||||
|
||||
## [1.8.0] - 2026-06-16
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ test.describe('OU Operator — solo grupos y operaciones de equipos', () => {
|
|||
const finalUrl = await navigateTo(page, '/clients/run-script');
|
||||
expect(finalUrl).toContain('/clients/run-script');
|
||||
});
|
||||
|
||||
test('accede a /commands-logs (trazas de sus acciones)', async ({ page }) => {
|
||||
const finalUrl = await navigateTo(page, '/commands-logs');
|
||||
expect(finalUrl).toContain('/commands-logs');
|
||||
});
|
||||
});
|
||||
|
||||
// ── GUI en /groups: botones de crear OU/cliente ocultos ─────────────────────
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const routes: Routes = [
|
|||
{ 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'] } },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
||||
{ path: 'calendars', component: CalendarComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
||||
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
|
||||
|
|
|
|||
|
|
@ -172,12 +172,14 @@ import { ShowTaskScheduleComponent } from './components/commands/commands-task/s
|
|||
import { ShowTaskScriptComponent } from './components/commands/commands-task/show-task-script/show-task-script.component';
|
||||
import { CreateTaskScriptComponent } from './components/commands/commands-task/create-task-script/create-task-script.component';
|
||||
import { ViewParametersModalComponent } from './components/commands/commands-task/show-task-script/view-parameters-modal/view-parameters-modal.component';
|
||||
import { ViewScriptModalComponent } from './components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component';
|
||||
import { OutputDialogComponent } from './components/task-logs/output-dialog/output-dialog.component';
|
||||
import { ClientTaskLogsComponent } from './components/task-logs/client-task-logs/client-task-logs.component';
|
||||
import { BootSoPartitionComponent } from './components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component';
|
||||
import { RemoveCacheImageComponent } from './components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component';
|
||||
import { ChangeParentComponent } from './components/groups/shared/change-parent/change-parent.component';
|
||||
import { SoftwareProfilePartitionComponent } from './components/commands/main-commands/execute-command/software-profile-partition/software-profile-partition.component';
|
||||
import { ExecuteTaskNowComponent } from './components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component';
|
||||
import { ClientPendingTasksComponent } from './components/task-logs/client-pending-tasks/client-pending-tasks.component';
|
||||
import { QueueConfirmationModalComponent } from './shared/queue-confirmation-modal/queue-confirmation-modal.component';
|
||||
import { ModalOverlayComponent } from './shared/modal-overlay/modal-overlay.component';
|
||||
|
|
@ -316,12 +318,14 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
ShowTaskScriptComponent,
|
||||
CreateTaskScriptComponent,
|
||||
ViewParametersModalComponent,
|
||||
ViewScriptModalComponent,
|
||||
OutputDialogComponent,
|
||||
ClientTaskLogsComponent,
|
||||
BootSoPartitionComponent,
|
||||
RemoveCacheImageComponent,
|
||||
ChangeParentComponent,
|
||||
SoftwareProfilePartitionComponent,
|
||||
ExecuteTaskNowComponent,
|
||||
ClientPendingTasksComponent,
|
||||
ModalOverlayComponent,
|
||||
ScrollToTopComponent,
|
||||
|
|
|
|||
|
|
@ -15,30 +15,48 @@
|
|||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelRole' | translate }}</mat-label>
|
||||
<mat-select formControlName="role" required>
|
||||
<mat-option *ngFor="let group of userGroups" [value]="group['@id']">
|
||||
{{ group.name }}
|
||||
</mat-option>
|
||||
@for (group of userGroups; track group['@id']) {
|
||||
<mat-option [value]="group['@id']">
|
||||
{{ group.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelOrganizationalUnit' | translate }}</mat-label>
|
||||
<mat-select multiple formControlName="organizationalUnits">
|
||||
<mat-option *ngFor="let unit of organizationalUnits" [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
@for (unit of organizationalUnits; track unit['@id']) {
|
||||
<mat-option [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'cardView' | translate }}</mat-label>
|
||||
<mat-select formControlName="groupsView" required>
|
||||
<mat-option *ngFor="let option of views" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
@for (option of views; track option.value) {
|
||||
<mat-option [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'userLanguage' | translate }}</mat-label>
|
||||
<mat-select formControlName="language">
|
||||
@for (lang of availableLanguages; track lang.code) {
|
||||
<mat-option [value]="lang.code">
|
||||
{{ lang.label }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
<mat-hint>{{ 'userLanguageHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'clientsPerPage' | translate }}</mat-label>
|
||||
<input matInput formControlName="clientsPerPage" type="number" min="1" [placeholder]="'default20' | translate">
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ export class AddUserModalComponent implements OnInit {
|
|||
{ value: 'list', name: 'Listado' },
|
||||
];
|
||||
|
||||
readonly availableLanguages: { code: string; label: string }[];
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<AddUserModalComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
|
|
@ -46,12 +48,15 @@ export class AddUserModalComponent implements OnInit {
|
|||
private configService: ConfigService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
const langs = this.configService.languages;
|
||||
this.availableLanguages = Object.keys(langs).map(code => ({ code, label: langs[code] }));
|
||||
this.userForm = this.fb.group({
|
||||
username: ['', Validators.required],
|
||||
password: ['', Validators.required],
|
||||
role: ['', Validators.required],
|
||||
groupsView: ['card', Validators.required],
|
||||
clientsPerPage: [null, [Validators.min(1)]],
|
||||
language: ['es'],
|
||||
organizationalUnits: [[]]
|
||||
});
|
||||
|
||||
|
|
@ -87,7 +92,8 @@ export class AddUserModalComponent implements OnInit {
|
|||
role: response.userGroups.length > 0 ? response.userGroups[0]['@id'] : null,
|
||||
organizationalUnits: organizationalUnitIds,
|
||||
groupsView: response.groupsView,
|
||||
clientsPerPage: response.clientsPerPage ?? null
|
||||
clientsPerPage: response.clientsPerPage ?? null,
|
||||
language: response.language ?? 'es'
|
||||
});
|
||||
|
||||
this.userId = response['@id'];
|
||||
|
|
@ -114,7 +120,8 @@ export class AddUserModalComponent implements OnInit {
|
|||
enabled: true,
|
||||
userGroups: [this.userForm.value.role],
|
||||
groupsView: this.userForm.value.groupsView,
|
||||
clientsPerPage: this.userForm.value.clientsPerPage || null
|
||||
clientsPerPage: this.userForm.value.clientsPerPage || null,
|
||||
language: this.userForm.value.language || null
|
||||
};
|
||||
|
||||
if (!this.userId && this.userForm.value.password) {
|
||||
|
|
|
|||
|
|
@ -321,18 +321,21 @@ mat-spinner {
|
|||
color: #666;
|
||||
}
|
||||
|
||||
/* Contenido del script en celda */
|
||||
.script-content-cell {
|
||||
background-color: #f5f5f5;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
white-space: pre-wrap;
|
||||
font-size: 12px;
|
||||
max-width: 300px;
|
||||
/* Vista previa del script en celda (una sola línea, altura de fila fija) */
|
||||
.script-preview-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.script-preview-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.no-content-hint {
|
||||
|
|
|
|||
|
|
@ -50,8 +50,11 @@
|
|||
<ng-container matColumnDef="content">
|
||||
<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>
|
||||
<div *ngIf="item.content" class="script-preview-cell">
|
||||
<span class="script-preview-text">{{ item.content }}</span>
|
||||
<button mat-icon-button (click)="openScriptModal(item.content)" [matTooltip]="'viewScript' | translate">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<span *ngIf="!item.content" class="no-content-hint">
|
||||
({{ 'viewParameters' | translate }})
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { CreateTaskScheduleComponent } from "../create-task-schedule/create-task
|
|||
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 { ViewScriptModalComponent } from "./view-script-modal/view-script-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-task-script',
|
||||
|
|
@ -119,6 +120,13 @@ export class ShowTaskScriptComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
openScriptModal(content: string): void {
|
||||
this.dialog.open(ViewScriptModalComponent, {
|
||||
width: '700px',
|
||||
data: content
|
||||
});
|
||||
}
|
||||
|
||||
createNewScript(): void {
|
||||
const dialogRef = this.dialog.open(CreateTaskScriptComponent, {
|
||||
width: '800px',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
.script-full-content {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<h2 mat-dialog-title>{{ 'labelScript' | translate }}</h2>
|
||||
<mat-dialog-content>
|
||||
<pre class="script-full-content">{{ data }}</pre>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="close()">{{ 'buttonClose' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-view-script-modal',
|
||||
templateUrl: './view-script-modal.component.html',
|
||||
styleUrl: './view-script-modal.component.css'
|
||||
})
|
||||
export class ViewScriptModalComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ViewScriptModalComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -53,42 +53,13 @@
|
|||
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<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">{{ 'buttonSelectPartition' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group
|
||||
[(ngModel)]="selectedPartition"
|
||||
[disabled]="!row.operativeSystem"
|
||||
>
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header | translate }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef !== 'size'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.size }} MB</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
<app-disk-partition-selector
|
||||
[(selectedPartition)]="selectedPartition"
|
||||
[partitions]="dataSource.data"
|
||||
[columns]="columns"
|
||||
[displayedColumns]="displayedColumns"
|
||||
[disableCondition]="isPartitionDisabled">
|
||||
</app-disk-partition-selector>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ export class BootSoPartitionComponent implements OnInit {
|
|||
|
||||
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
|
||||
|
||||
isPartitionDisabled = (partition: any): boolean => {
|
||||
return !partition.operativeSystem;
|
||||
};
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: { clients: any },
|
||||
private dialogRef: MatDialogRef<BootSoPartitionComponent>,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { RemoveCacheImageComponent } from "./remove-cache-image/remove-cache-ima
|
|||
import { AuthService } from '@services/auth.service';
|
||||
import {SoftwareProfilePartitionComponent} from "./software-profile-partition/software-profile-partition.component";
|
||||
import { QueueConfirmationModalComponent } from '../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component';
|
||||
import { ExecuteTaskNowComponent } from './execute-task-now/execute-task-now.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-execute-command',
|
||||
|
|
@ -43,6 +44,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
{ translationKey: 'executeCommands.runScript', slug: 'run-script', disabled: false },
|
||||
{ translationKey: 'executeCommands.refresh', slug: 'refresh', disabled: false },
|
||||
{ translationKey: 'executeCommands.deleteLease', slug: 'delete-lease', disabled: false },
|
||||
{ translationKey: 'executeCommands.executeTask', slug: 'execute-task', disabled: false },
|
||||
];
|
||||
|
||||
client: any = {};
|
||||
|
|
@ -87,7 +89,8 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
'remove-cache-image',
|
||||
'partition',
|
||||
'refresh',
|
||||
'delete-lease'
|
||||
'delete-lease',
|
||||
'execute-task'
|
||||
],
|
||||
'ou-minimal': [
|
||||
'power-on',
|
||||
|
|
@ -138,6 +141,10 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
command.disabled = !states.every(state => state === 'og-live');
|
||||
}
|
||||
|
||||
if (command.slug === 'execute-task') {
|
||||
command.disabled = this.clientData.length === 0;
|
||||
}
|
||||
|
||||
return command;
|
||||
});
|
||||
}
|
||||
|
|
@ -194,6 +201,18 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
if (action === 'delete-lease') {
|
||||
this.deleteClientLease();
|
||||
}
|
||||
|
||||
if (action === 'execute-task') {
|
||||
this.openExecuteTaskModal();
|
||||
}
|
||||
}
|
||||
|
||||
openExecuteTaskModal(): void {
|
||||
this.dialog.open(ExecuteTaskNowComponent, {
|
||||
width: '500px',
|
||||
height: 'auto',
|
||||
data: { clients: this.clientData }
|
||||
});
|
||||
}
|
||||
|
||||
rebootClient(): void {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.mat-dialog-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mat-dialog-content {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mat-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<h2 mat-dialog-title>{{ 'executeTaskNowModalTitle' | translate }}</h2>
|
||||
|
||||
<mat-dialog-content class="form-container">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectTask' | translate }}</mat-label>
|
||||
<input type="text" matInput [formControl]="taskControl" [matAutocomplete]="taskAuto"
|
||||
[placeholder]="'labelSearchTask' | translate" #taskInput
|
||||
#taskAutoTrigger="matAutocompleteTrigger">
|
||||
|
||||
<button type="button" mat-icon-button matSuffix tabindex="-1"
|
||||
[disabled]="tasksLoading || tasks.length === 0"
|
||||
(click)="taskInput.focus(); taskAutoTrigger.openPanel()">
|
||||
<mat-icon>arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
|
||||
<mat-autocomplete #taskAuto="matAutocomplete"
|
||||
[displayWith]="displayFn" autoActiveFirstOption
|
||||
(optionSelected)="onTaskSelected($event.option.value)">
|
||||
<mat-option *ngFor="let task of filteredTasks$ | async" [value]="task">
|
||||
{{ task.name }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<p *ngIf="tasksLoading" class="hint-text">{{ 'loadingDots' | translate }}</p>
|
||||
<p *ngIf="!tasksLoading && tasks.length === 0">{{ 'noTasksAvailableMessage' | translate }}</p>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="closeModal()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="executeNow()" [disabled]="!selectedTask">
|
||||
{{ 'executeButton' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
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 { HttpClient } from '@angular/common/http';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-execute-task-now',
|
||||
templateUrl: './execute-task-now.component.html',
|
||||
styleUrls: ['./execute-task-now.component.css']
|
||||
})
|
||||
export class ExecuteTaskNowComponent implements OnInit {
|
||||
public translate = inject(TranslateService);
|
||||
baseUrl: string;
|
||||
tasks: any[] = [];
|
||||
tasksLoading = true;
|
||||
taskControl = new FormControl();
|
||||
filteredTasks$!: Observable<any[]>;
|
||||
selectedTask: any = null;
|
||||
private tasks$ = new BehaviorSubject<any[]>([]);
|
||||
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<ExecuteTaskNowComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTasks();
|
||||
|
||||
this.filteredTasks$ = combineLatest([
|
||||
this.taskControl.valueChanges.pipe(startWith('')),
|
||||
this.tasks$
|
||||
]).pipe(
|
||||
map(([value]) => this._filterTasks(typeof value === 'string' ? value : value?.name))
|
||||
);
|
||||
}
|
||||
|
||||
loadTasks(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/command-tasks?page=1&itemsPerPage=100`).subscribe(
|
||||
response => {
|
||||
this.tasks = response['hydra:member'] || [];
|
||||
this.tasksLoading = false;
|
||||
this.tasks$.next(this.tasks);
|
||||
},
|
||||
error => {
|
||||
this.tasksLoading = false;
|
||||
this.toastService.error(this.translate.instant('errorLoadingTasks'), error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _filterTasks(value: string): any[] {
|
||||
const filterValue = (value || '').toLowerCase();
|
||||
return this.tasks.filter(task => task.name.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
displayFn(task: any): string {
|
||||
return task && task.name ? task.name : '';
|
||||
}
|
||||
|
||||
onTaskSelected(task: any): void {
|
||||
this.selectedTask = task;
|
||||
}
|
||||
|
||||
executeNow(): void {
|
||||
if (!this.selectedTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clients = this.data?.clients || [];
|
||||
|
||||
if (clients.length === 0) {
|
||||
this.toastService.error(this.translate.instant('noClientsSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
clients: clients.map((client: any) => client['@id'] || `/clients/${client.uuid}`)
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/command-tasks/${this.selectedTask.uuid}/execute`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('taskExecutedSuccessfully'));
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: () => {
|
||||
this.toastService.error(this.translate.instant('errorExecutingTaskNow'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeModal(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { HttpClient } from '@angular/common/http';
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { AuthService } from '@services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-global-status',
|
||||
|
|
@ -67,7 +68,8 @@ export class GlobalStatusComponent implements OnInit {
|
|||
constructor(
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient
|
||||
private http: HttpClient,
|
||||
private auth: AuthService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.dhcpsApiUrl = `${this.baseUrl}/dhcp-servers`;
|
||||
|
|
@ -115,7 +117,18 @@ export class GlobalStatusComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Las sincronizaciones con la infraestructura (subredes DHCP, plantillas PXE y
|
||||
* ogLives) están restringidas a ROLE_SUPER_ADMIN en ogCore; para el resto de
|
||||
* perfiles devuelven "Access Denied", así que ni siquiera se lanzan.
|
||||
*/
|
||||
private get canSyncInfrastructure(): boolean {
|
||||
return this.auth.isSuperAdmin;
|
||||
}
|
||||
|
||||
syncSubnets() {
|
||||
if (!this.canSyncInfrastructure) return;
|
||||
|
||||
this.http.get<any>(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe(data => {
|
||||
const dhcps = data['hydra:member'] || [];
|
||||
if (dhcps.length === 0) return;
|
||||
|
|
@ -155,6 +168,7 @@ export class GlobalStatusComponent implements OnInit {
|
|||
}
|
||||
|
||||
syncTemplates() {
|
||||
if (!this.canSyncInfrastructure) return;
|
||||
if (this.bootServers.length === 0) return;
|
||||
|
||||
let pending = this.bootServers.length;
|
||||
|
|
@ -189,6 +203,7 @@ export class GlobalStatusComponent implements OnInit {
|
|||
}
|
||||
|
||||
syncOgLives(): void {
|
||||
if (!this.canSyncInfrastructure) return;
|
||||
if (this.bootServers.length === 0) return;
|
||||
|
||||
let pending = this.bootServers.length;
|
||||
|
|
|
|||
|
|
@ -152,7 +152,8 @@ export class CreateClientImageComponent implements OnInit{
|
|||
}
|
||||
|
||||
loadImages() {
|
||||
const url = `${this.baseUrl}/images?created=false&type=${this.imageType}&page=1&itemsPerPage=100`;
|
||||
const repoFilter = this.selectedRepository ? `&repositoryId=${this.selectedRepository.id}` : '';
|
||||
const url = `${this.baseUrl}/images?created=false&type=${this.imageType}${repoFilter}&page=1&itemsPerPage=100`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
this.images = response['hydra:member'];
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@
|
|||
</div>
|
||||
|
||||
<div class="clients-grid">
|
||||
<div *ngFor="let client of clientData" class="client-item">
|
||||
@for (client of clientData; track client) {
|
||||
<div class="client-item">
|
||||
<div class="client-card" (click)="toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected}" [matTooltip]="getPartitionsTooltip(client)"
|
||||
matTooltipPosition="above" matTooltipClass="custom-tooltip">
|
||||
|
|
@ -73,6 +74,7 @@
|
|||
</mat-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
|
|
@ -99,49 +101,89 @@
|
|||
</div>
|
||||
|
||||
<!-- Campos monolítica -->
|
||||
<div class="monolithic-row" *ngIf="imageType === 'monolithic'">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
@if (imageType === 'monolithic') {
|
||||
<div class="monolithic-row" style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<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>
|
||||
@for (method of allMethods; track method.value) {
|
||||
<mat-option [value]="method.value">{{ method.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectRepository' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="selectedRepository" (selectionChange)="onRepositorySelected($event.value)" [compareWith]="compareRepositories" [disabled]="loadingAllRepositories">
|
||||
@for (repo of allImageRepositories; track repo.uuid) {
|
||||
<mat-option [value]="repo">{{ repo.name }} ({{ repo.ip }})</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (loadingAllRepositories) {
|
||||
<mat-icon matSuffix>hourglass_empty</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectImagePrompt' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage" [disabled]="!imageType">
|
||||
<mat-option *ngFor="let image of images" [value]="image">
|
||||
@for (image of images; track image.uuid || $index) {
|
||||
<mat-option [value]="image">
|
||||
<div class="unit-name"> {{ image.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ image.description }}</div>
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Campos Git -->
|
||||
<div *ngIf="imageType === 'git'" class="filters-row git-gap">
|
||||
<mat-form-field appearance="fill" style="width: 300px;">
|
||||
@if (imageType === 'git') {
|
||||
<div class="git-gap" style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>{{ 'selectRepository' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="selectedRepository" (selectionChange)="onRepositorySelected($event.value)" [compareWith]="compareRepositories" [disabled]="loadingAllRepositories">
|
||||
@for (repo of allImageRepositories; track repo.uuid) {
|
||||
<mat-option [value]="repo">{{ repo.name }} ({{ repo.ip }})</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (loadingAllRepositories) {
|
||||
<mat-icon matSuffix>hourglass_empty</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<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">
|
||||
@for (repo of repositories; track repo) {
|
||||
<mat-option [value]="repo">
|
||||
{{ repo }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
<mat-icon matSuffix *ngIf="loadingRepositories">hourglass_empty</mat-icon>
|
||||
@if (loadingRepositories) {
|
||||
<mat-icon matSuffix>hourglass_empty</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" style="width: 300px;">
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>{{ 'buttonSelectBranch' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="selectedBranch" (selectionChange)="onGitBranchChange()"
|
||||
[disabled]="loadingBranches || !selectedRepository">
|
||||
<mat-option *ngFor="let branch of branches" [value]="branch">
|
||||
[disabled]="loadingBranches || !selectedGitRepository">
|
||||
@for (branch of branches; track branch) {
|
||||
<mat-option [value]="branch">
|
||||
{{ branch }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
<mat-icon matSuffix *ngIf="loadingBranches">hourglass_empty</mat-icon>
|
||||
@if (loadingBranches) {
|
||||
<mat-icon matSuffix>hourglass_empty</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" style="width: 200px;">
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>{{ 'labelTag' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="showOnlyTagged" (selectionChange)="onTagFilterChange()" [disabled]="!commits.length">
|
||||
<mat-option [value]="false">{{ 'buttonNo' | translate }}</mat-option>
|
||||
|
|
@ -149,6 +191,7 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Configuración avanzada del método (Multicast / P2P) -->
|
||||
@if (imageType === 'monolithic' && selectedMethod && needsAdvancedConfig(selectedMethod)) {
|
||||
|
|
@ -182,9 +225,11 @@
|
|||
}
|
||||
|
||||
|
||||
<div *ngIf="errorMessage" class="error-message">
|
||||
@if (errorMessage) {
|
||||
<div class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -221,6 +266,22 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
<!-- Contenedor para mostrar las instrucciones en cualquier modo -->
|
||||
@if (showInstructions) {
|
||||
<div class="instructions-box" style="margin-bottom: 20px;">
|
||||
<mat-card class="instructions-card">
|
||||
<mat-card-title>
|
||||
{{ 'generatedInstructions' | translate }}<button mat-icon-button (click)="showInstructions = false" style="float: right;">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<pre>{{ ogInstructions }}</pre>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Sección: Selección de partición (oculta en modo solo-caché) -->
|
||||
@if (deployMode !== 'update-cache') {
|
||||
<div class="form-section compact-section partition-select-section" id="partition-selection">
|
||||
|
|
@ -229,18 +290,6 @@
|
|||
{{ 'partitionSelection' | translate }}</div>
|
||||
|
||||
<div class="partition-table-container">
|
||||
<div *ngIf="showInstructions" class="instructions-box">
|
||||
<mat-card class="instructions-card">
|
||||
<mat-card-title>
|
||||
{{ 'generatedInstructions' | translate }}<button mat-icon-button (click)="showInstructions = false" style="float: right;">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<pre>{{ ogInstructions }}</pre>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<app-disk-partition-selector
|
||||
[(selectedPartition)]="selectedPartition"
|
||||
|
|
@ -274,7 +323,8 @@
|
|||
</div>
|
||||
|
||||
<!-- Tabla de commits Git (solo cuando hay commits cargados) -->
|
||||
<div *ngIf="imageType === 'git' && filteredCommits.length > 0" class="form-section compact-section">
|
||||
@if (imageType === 'git' && filteredCommits.length > 0) {
|
||||
<div class="form-section compact-section">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>history</mat-icon>
|
||||
{{ 'commitSelection' | translate }}</div>
|
||||
|
|
@ -309,11 +359,15 @@
|
|||
<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"
|
||||
@for (tag of commit.tags; track tag) {
|
||||
<mat-chip 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">{{ 'noTags' | translate }}</span>
|
||||
}
|
||||
@if (!commit.tags || commit.tags.length === 0) {
|
||||
<span class="no-tags-text">{{ 'noTags' | translate }}</span>
|
||||
}
|
||||
</mat-chip-list>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
|
@ -322,6 +376,7 @@
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ export class DeployImageComponent implements OnInit {
|
|||
selectedModelClient: any = null;
|
||||
filteredPartitions: any[] = [];
|
||||
|
||||
allImageRepositories: any[] = [];
|
||||
loadingAllRepositories: boolean = false;
|
||||
selectedRepository: any = null;
|
||||
imageType: string = 'monolithic';
|
||||
|
||||
|
|
@ -145,6 +147,42 @@ export class DeployImageComponent implements OnInit {
|
|||
this.route.queryParams.subscribe(params => {
|
||||
this.runScriptContext = params['runScriptContext'] ? JSON.parse(params['runScriptContext']) : null;
|
||||
});
|
||||
this.loadAllRepositories();
|
||||
}
|
||||
|
||||
loadAllRepositories() {
|
||||
this.loadingAllRepositories = true;
|
||||
this.http.get<any>(`${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`).subscribe(
|
||||
(response: any) => {
|
||||
this.allImageRepositories = response?.['hydra:member'] || [];
|
||||
this.loadingAllRepositories = false;
|
||||
// Si ya había un selectedRepository cargado por el cliente, asegurarse de enlazar la misma referencia del array
|
||||
if (this.selectedRepository) {
|
||||
const match = this.allImageRepositories.find(r => r.uuid === this.selectedRepository.uuid);
|
||||
if (match) {
|
||||
this.selectedRepository = match;
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('errorLoadingRepositories1'), error);
|
||||
this.loadingAllRepositories = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
compareRepositories(r1: any, r2: any): boolean {
|
||||
return r1 && r2 ? r1.uuid === r2.uuid : r1 === r2;
|
||||
}
|
||||
|
||||
onRepositorySelected(repo: any) {
|
||||
this.selectedRepository = repo;
|
||||
if (this.imageType === 'git') {
|
||||
this.selectedGitRepository = '';
|
||||
this.selectedBranch = '';
|
||||
this.loadGitRepositories();
|
||||
}
|
||||
this.loadImages();
|
||||
}
|
||||
|
||||
onImageTypeSelected(event: any) {
|
||||
|
|
@ -217,6 +255,23 @@ export class DeployImageComponent implements OnInit {
|
|||
this.filteredPartitions = fullClientData.partitions;
|
||||
this.selectedRepository = fullClientData.repository ?? null;
|
||||
|
||||
if (this.selectedRepository?.uuid && this.allImageRepositories.length > 0) {
|
||||
const match = this.allImageRepositories.find(r => r.uuid === this.selectedRepository.uuid);
|
||||
if (match) {
|
||||
this.selectedRepository = match;
|
||||
}
|
||||
} else if (this.selectedRepository?.uuid) {
|
||||
// Fallback por si la llamada de loadAllRepositories no ha terminado
|
||||
this.http.get(`${this.baseUrl}/image-repositories/${this.selectedRepository.uuid}`).subscribe(
|
||||
(repository: any) => {
|
||||
this.selectedRepository = repository;
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('errorLoadingRepositories1'), error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (fullClientData.partitions) {
|
||||
this.filteredPartitions = fullClientData.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0;
|
||||
|
|
@ -256,7 +311,9 @@ export class DeployImageComponent implements OnInit {
|
|||
if (this.imageType === 'monolithic') {
|
||||
url = `${this.baseUrl}/image-image-repositories?status=success&repository.id=${repositoryId}&page=1&itemsPerPage=1000`;
|
||||
} else {
|
||||
url = `${this.baseUrl}/git-image-repositories?status=success&repository.id=${repositoryId}&page=1&itemsPerPage=1000`;
|
||||
// Para 'git', la carga de repositorios y ramas se maneja con loadGitRepositories() y loadGitBranches()
|
||||
this.images = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.http.get(url).subscribe(
|
||||
|
|
@ -586,9 +643,79 @@ export class DeployImageComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
generateOgInstructions() {
|
||||
this.showInstructions = true;
|
||||
this.ogInstructions = `og-deploy-image --image ${this.selectedImage.name} --partition ${this.selectedPartition?.partitionNumber || 1} --method ${this.selectedMethod}`;
|
||||
this.ogInstructions = this.imageType === 'git'
|
||||
? this.buildGitOgInstructions()
|
||||
: this.buildMonolithicOgInstructions();
|
||||
}
|
||||
|
||||
private getProto(method: string | null): string {
|
||||
switch (method) {
|
||||
case 'uftp-direct':
|
||||
case 'udpcast-direct':
|
||||
return 'MULTICAST-DIRECT';
|
||||
case 'multicast':
|
||||
case 'uftp':
|
||||
case 'udpcast':
|
||||
return 'MULTICAST';
|
||||
case 'unicast-direct':
|
||||
return 'UNICAST-DIRECT';
|
||||
case 'unicast':
|
||||
return 'UNICAST';
|
||||
case 'p2p':
|
||||
return 'TORRENT';
|
||||
default:
|
||||
return (method || '').toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
private getProtoOpt(method: string | null): string {
|
||||
if (method === 'udpcast' || method === 'uftp' || method === 'udpcast-direct') {
|
||||
const mcastMode = `${this.mcastMode}-duplex`;
|
||||
const mcastSpeed = `${this.mcastSpeed}M`;
|
||||
return `${this.mcastPort}:${mcastMode}:${this.mcastIp}:${mcastSpeed}:${this.mcastMaxClients}:${this.mcastMaxTime}`;
|
||||
}
|
||||
if (method === 'p2p') {
|
||||
return `${this.p2pMode}:${this.p2pTime}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private buildMonolithicOgInstructions(): string {
|
||||
const method = this.selectedMethod;
|
||||
const proto = this.getProto(method);
|
||||
const protoOpt = this.getProtoOpt(method);
|
||||
|
||||
const disk = String(this.selectedPartition?.diskNumber || 1);
|
||||
const par = String(this.selectedPartition?.partitionNumber || 1);
|
||||
const imgname = this.selectedImage?.name || '<image>';
|
||||
const repo = this.selectedRepository?.ip || '<repository-ip>';
|
||||
|
||||
const effectiveDeployMode = this.isDirectMethod() ? 'deploy' : this.deployMode;
|
||||
const isCache = effectiveDeployMode === 'update-cache';
|
||||
|
||||
const restaurarArgs = [disk, par, imgname, repo, proto, protoOpt].filter(a => a !== '').join(' ');
|
||||
const scriptName = isCache ? 'ActualizarCache' : 'RestaurarImagen';
|
||||
|
||||
const innerCommand = isCache
|
||||
? ['updateCache', repo, `/${imgname}.img`, proto, protoOpt].filter(a => a !== '').join(' ')
|
||||
: ['deployImage', repo, imgname, disk, par, proto, protoOpt].filter(a => a !== '').join(' ');
|
||||
|
||||
return `${scriptName} ${restaurarArgs}\n ↳ ${innerCommand}`;
|
||||
}
|
||||
|
||||
private buildGitOgInstructions(): string {
|
||||
const disk = String(this.selectedPartition?.diskNumber || 1);
|
||||
const par = String(this.selectedPartition?.partitionNumber || 1);
|
||||
const repo = this.selectedRepository?.ip || '<repository-ip>';
|
||||
const imageName = this.selectedGitRepository || '<repository-name>';
|
||||
const branch = this.selectedBranch || '<branch>';
|
||||
const commit = this.selectedCommit?.hexsha || '<commit>';
|
||||
|
||||
return `RestaurarImagenGit --disk ${disk} --partition ${par} --repository ${repo} ` +
|
||||
`--image-name ${imageName} --branch ${branch} --commit ${commit}`;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -168,7 +168,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
ngOnInit(): void {
|
||||
this.updateGridCols();
|
||||
this.refreshData();
|
||||
this.refreshData(localStorage.getItem('lastSelectedOU') ?? undefined);
|
||||
window.addEventListener('resize', this.updateGridCols);
|
||||
|
||||
this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => {
|
||||
|
|
@ -340,12 +340,12 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
this.treeDataSource.data = [...this.originalTreeData];
|
||||
if (selectedNodeIdOrUuid) {
|
||||
this.selectedNode = this.findNodeByIdOrUuid(this.treeDataSource.data, selectedNodeIdOrUuid);
|
||||
}
|
||||
|
||||
if (this.selectedNode) {
|
||||
this.treeControl.collapseAll();
|
||||
this.expandPathToNode(this.selectedNode);
|
||||
this.fetchClientsForNode(this.selectedNode, selectedClientsBeforeEdit);
|
||||
}
|
||||
if (selectedNodeIdOrUuid && this.selectedNode) {
|
||||
this.treeControl.collapseAll();
|
||||
this.expandPathToNode(this.selectedNode);
|
||||
this.fetchClientsForNode(this.selectedNode, selectedClientsBeforeEdit);
|
||||
} else {
|
||||
this.treeControl.collapseAll();
|
||||
if (this.treeDataSource.data.length > 0) {
|
||||
|
|
@ -419,8 +419,9 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
onNodeClick(event: MouseEvent, node: TreeNode): void {
|
||||
event.stopPropagation();
|
||||
this.selectedNode = node;
|
||||
this.persistSelectedNode(node);
|
||||
const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid);
|
||||
|
||||
|
||||
const checkStatusAfterLoad = node.type === NodeType.Classroom || node.type === NodeType.ClientsGroup;
|
||||
this.fetchClientsForNode(node, selectedClientsBeforeEdit, checkStatusAfterLoad);
|
||||
}
|
||||
|
|
@ -428,10 +429,17 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
onMenuClick(event: Event, node: any): void {
|
||||
event.stopPropagation();
|
||||
this.selectedNode = node;
|
||||
this.persistSelectedNode(node);
|
||||
const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid);
|
||||
this.fetchClientsForNode(node, selectedClientsBeforeEdit);
|
||||
}
|
||||
|
||||
private persistSelectedNode(node: TreeNode): void {
|
||||
if (node.uuid) {
|
||||
localStorage.setItem('lastSelectedOU', node.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -83,4 +83,22 @@ mat-option .unit-path {
|
|||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
||||
.hardware-profile-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 0 12px 0;
|
||||
}
|
||||
|
||||
.hardware-profile-box .form-field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.hardware-profile-link {
|
||||
margin-left: auto;
|
||||
color: #3f51b5;
|
||||
}
|
||||
|
|
@ -139,6 +139,26 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
|
||||
@if (isEditMode) {
|
||||
<fieldset class="fieldset-group">
|
||||
<legend class="fieldset-legend">{{ 'labelHardwareProfile' | translate }}</legend>
|
||||
<div class="hardware-profile-box">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option [value]="null">{{ 'noHardwareProfileAssigned' | translate }}</mat-option>
|
||||
@for (profile of hardwareProfiles; track profile['@id']) {
|
||||
<mat-option [value]="profile['@id']">{{ profile.description }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button type="button" mat-icon-button class="hardware-profile-link" (click)="goToHardwareProfiles()"
|
||||
[matTooltip]="'manageHardwareProfiles' | translate">
|
||||
<mat-icon>settings</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
}
|
||||
|
||||
<mat-form-field class="form-field full-width">
|
||||
<mat-label>{{ 'labelNotes' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="notes" rows="4"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Component, Inject, OnInit } from '@angular/core';
|
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Router } from '@angular/router';
|
||||
import { DataService } from '../../../services/data.service';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ export class ManageClientComponent implements OnInit {
|
|||
templates: any[] = [];
|
||||
repositories: any[] = [];
|
||||
bootServers: any[] = [];
|
||||
hardwareProfiles: any[] = [];
|
||||
loading: boolean = false;
|
||||
displayedColumns: string[] = ['name', 'ip'];
|
||||
dialogTitle: string;
|
||||
|
|
@ -41,6 +43,7 @@ export class ManageClientComponent implements OnInit {
|
|||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
private dataService: DataService,
|
||||
private router: Router,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
|
|
@ -55,7 +58,8 @@ export class ManageClientComponent implements OnInit {
|
|||
this.loadParentUnits(),
|
||||
this.loadBootServers(),
|
||||
this.loadRepositories(),
|
||||
this.loadMenus()
|
||||
this.loadMenus(),
|
||||
this.loadHardwareProfiles()
|
||||
];
|
||||
|
||||
Promise.all(observables).then(() => {
|
||||
|
|
@ -91,6 +95,7 @@ export class ManageClientComponent implements OnInit {
|
|||
ogLive: [null],
|
||||
repository: [null],
|
||||
menu: [null],
|
||||
hardwareProfile: [null],
|
||||
maintenance: [false],
|
||||
resolution: [null],
|
||||
notes: ['']
|
||||
|
|
@ -222,6 +227,24 @@ export class ManageClientComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
loadHardwareProfiles(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/hardware-profiles?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.hardwareProfiles = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error(this.translate.instant('errorFetchingHardwareProfiles'), error);
|
||||
this.hardwareProfiles = [];
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadRepositories(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
|
||||
|
|
@ -294,6 +317,7 @@ export class ManageClientComponent implements OnInit {
|
|||
bootServer: bootServerIri,
|
||||
repository: data.repository ? data.repository['@id'] : null,
|
||||
menu: data.menu ? data.menu['@id'] : null,
|
||||
hardwareProfile: data.hardwareProfile ? data.hardwareProfile['@id'] : null,
|
||||
maintenance: data.maintenance,
|
||||
resolution: data.resolution,
|
||||
notes: data.notes || ''
|
||||
|
|
@ -364,4 +388,9 @@ export class ManageClientComponent implements OnInit {
|
|||
onNoClick(): void {
|
||||
this.dialogRef.close({ success: false });
|
||||
}
|
||||
|
||||
goToHardwareProfiles(): void {
|
||||
this.dialogRef.close({ success: false });
|
||||
this.router.navigate(['/hardware-profiles']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,11 +62,22 @@
|
|||
.client-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.client-more {
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
|
|
@ -74,8 +85,4 @@
|
|||
flex-direction: column;
|
||||
font-size: 0.75em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.client-ip, .client-mac {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
|
@ -47,11 +47,12 @@
|
|||
</mat-icon>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'client'">
|
||||
<ng-container *ngIf="column.columnDef === 'clients'">
|
||||
<div *ngIf="column.cell(image) !== 'N/A'; else noClientData" class="client-info">
|
||||
<div class="client-name">{{ column.cell(image).name }}</div>
|
||||
<div class="client-details">
|
||||
<span class="client-ip">{{ column.cell(image).ip }}</span>
|
||||
<div class="client-name" *ngFor="let assignedClient of getVisibleClients(column.cell(image))">{{ assignedClient.name }}</div>
|
||||
<div class="client-name client-more" *ngIf="column.cell(image).length > maxVisibleClients"
|
||||
[matTooltip]="getHiddenClientNames(column.cell(image))">
|
||||
{{ 'andMoreClients' | translate: { count: column.cell(image).length - maxVisibleClients } }}
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #noClientData>
|
||||
|
|
@ -60,7 +61,7 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container
|
||||
*ngIf="column.columnDef !== 'isDefault' && column.columnDef !== 'installed' && column.columnDef !== 'downloadUrl' && column.columnDef !== 'client'">
|
||||
*ngIf="column.columnDef !== 'isDefault' && column.columnDef !== 'installed' && column.columnDef !== 'downloadUrl' && column.columnDef !== 'clients'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export class HardwareProfileComponent {
|
|||
filteredClients!: Observable<any[]>;
|
||||
clientControl = new FormControl();
|
||||
clients: any[] = [];
|
||||
maxVisibleClients: number = 3;
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
|
|
@ -49,13 +50,9 @@ export class HardwareProfileComponent {
|
|||
cell: (hardware: any) => hardware.description
|
||||
},
|
||||
{
|
||||
columnDef: 'client',
|
||||
header: 'client',
|
||||
cell: (hardware: any) => hardware.client ? {
|
||||
name: hardware.client.name || 'N/A',
|
||||
ip: hardware.client.ip || 'N/A',
|
||||
mac: hardware.client.mac || 'N/A'
|
||||
} : 'N/A'
|
||||
columnDef: 'clients',
|
||||
header: 'clients',
|
||||
cell: (hardware: any) => hardware.clients?.length ? hardware.clients : 'N/A'
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
|
|
@ -104,16 +101,24 @@ export class HardwareProfileComponent {
|
|||
}
|
||||
|
||||
onOptionClientSelected(selectedClient: any): void {
|
||||
this.filters['client.id'] = selectedClient.id;
|
||||
this.filters['clients.id'] = selectedClient.id;
|
||||
this.search();
|
||||
}
|
||||
|
||||
clearClientFilter(event: Event, clientSearchClientInput: any): void {
|
||||
clientSearchClientInput.value = '';
|
||||
this.filters['client.id'] = '';
|
||||
this.filters['clients.id'] = '';
|
||||
this.search();
|
||||
}
|
||||
|
||||
getVisibleClients(clients: any): any[] {
|
||||
return (clients || []).slice(0, this.maxVisibleClients);
|
||||
}
|
||||
|
||||
getHiddenClientNames(clients: any): string {
|
||||
return (clients || []).slice(this.maxVisibleClients).map((client: any) => client.name).join(', ');
|
||||
}
|
||||
|
||||
private _filterClients(value: string): any[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
|
||||
|
|
|
|||
|
|
@ -32,15 +32,6 @@
|
|||
<input matInput formControlName="comments" name="comments">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="form-field">
|
||||
<mat-label>{{ 'softwareProfile' | translate }}</mat-label>
|
||||
<mat-select formControlName="softwareProfile">
|
||||
<mat-option *ngFor="let profile of softwareProfiles" [value]="profile['@id']">
|
||||
{{ profile.description }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox formControlName="remotePc">
|
||||
{{ 'labelRemotePc' | translate }}
|
||||
</mat-checkbox>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ export class CreateImageComponent implements OnInit {
|
|||
baseUrl: string;
|
||||
imageForm: FormGroup<any>;
|
||||
imageId: string | null = null;
|
||||
softwareProfiles: any[] = [];
|
||||
repositories: any[] = [];
|
||||
loading: boolean = false;
|
||||
isEditMode: boolean = false;
|
||||
|
|
@ -41,7 +40,6 @@ export class CreateImageComponent implements OnInit {
|
|||
comments: [''],
|
||||
remotePc: [false],
|
||||
isGlobal: [false],
|
||||
softwareProfile: [''],
|
||||
imageRepositories: [[], Validators.required],
|
||||
});
|
||||
}
|
||||
|
|
@ -54,7 +52,6 @@ export class CreateImageComponent implements OnInit {
|
|||
} else {
|
||||
this.loading = false;
|
||||
}
|
||||
this.fetchSoftwareProfiles();
|
||||
this.fetchRepositories();
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +64,6 @@ export class CreateImageComponent implements OnInit {
|
|||
comments: response.comments,
|
||||
remotePc: response.remotePc,
|
||||
isGlobal: response.isGlobal,
|
||||
softwareProfile: response.softwareProfile ? response.softwareProfile['@id'] : null,
|
||||
imageRepositories: response.imageRepositories ? response.imageRepositories.map((r: any) => r.imageRepository['@id']) : [],
|
||||
});
|
||||
this.imageId = response['@id'];
|
||||
|
|
@ -85,18 +81,6 @@ export class CreateImageComponent implements OnInit {
|
|||
this.showWarning = this.imageForm.get('isGlobal')?.value;
|
||||
}
|
||||
|
||||
fetchSoftwareProfiles() {
|
||||
const url = `${this.baseUrl}/software-profiles`;
|
||||
this.http.get(url).subscribe({
|
||||
next: (response: any) => {
|
||||
this.softwareProfiles = response['hydra:member'];
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(this.translate.instant('errorGettingProfiles'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchRepositories() {
|
||||
const url = `${this.baseUrl}/image-repositories`;
|
||||
this.http.get(url).subscribe({
|
||||
|
|
@ -117,7 +101,6 @@ export class CreateImageComponent implements OnInit {
|
|||
remotePc: this.imageForm.value.remotePc,
|
||||
isGlobal: this.imageForm.value.isGlobal,
|
||||
imageRepositories: this.imageForm.value.imageRepositories,
|
||||
...(this.imageForm.value.softwareProfile ? { softwareProfile: this.imageForm.value.softwareProfile } : {}),
|
||||
};
|
||||
|
||||
if (this.imageId) {
|
||||
|
|
|
|||
|
|
@ -64,10 +64,14 @@ export class LoginComponent {
|
|||
this.auth.refresh();
|
||||
this.openSnackBar(false, 'Bienvenido ' + this.auth.username);
|
||||
this.router.navigateByUrl('/groups');
|
||||
this.dialog.open(GlobalStatusComponent, {
|
||||
width: '65vw',
|
||||
height: '80vh',
|
||||
});
|
||||
// El estado global consulta y sincroniza infraestructura (ogBoot, DHCP,
|
||||
// repositorios), a la que solo tienen acceso los perfiles de administración.
|
||||
if (this.auth.isSuperAdmin || this.auth.isOUAdmin) {
|
||||
this.dialog.open(GlobalStatusComponent, {
|
||||
width: '65vw',
|
||||
height: '80vh',
|
||||
});
|
||||
}
|
||||
}
|
||||
this.isLoading = false;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -110,6 +110,11 @@ export class ShowGitCommitsComponent implements OnInit{
|
|||
if (this.repositories.length > 0) {
|
||||
this.selectedRepository = this.repositories[0];
|
||||
this.loadBranches();
|
||||
} else {
|
||||
this.selectedRepository = '';
|
||||
this.branches = [];
|
||||
this.selectedBranch = '';
|
||||
this.dataSource.data = [];
|
||||
}
|
||||
},
|
||||
error => {
|
||||
|
|
@ -344,7 +349,7 @@ export class ShowGitCommitsComponent implements OnInit{
|
|||
this.http.post(`${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/delete`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('repositoryDeletedSuccessfully'));
|
||||
this.dialogRef.close(true);
|
||||
this.loadRepositories();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(this.translate.instant('errorDeletingRepository1'), error);
|
||||
|
|
|
|||
|
|
@ -108,3 +108,36 @@ table {
|
|||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.software-profile-select {
|
||||
width: 150px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.software-profile-select ::ng-deep .mat-mdc-text-field-wrapper {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.software-profile-select ::ng-deep .mat-mdc-form-field-infix {
|
||||
min-height: unset;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.software-profile-caret {
|
||||
cursor: pointer;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
::ng-deep .software-profile-panel {
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
::ng-deep .software-profile-panel .mat-mdc-option {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,8 +83,25 @@
|
|||
{{ getStatusLabel(image[column.columnDef]) }}
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'softwareProfile'">
|
||||
<mat-form-field appearance="outline" class="software-profile-select" subscriptSizing="dynamic">
|
||||
<input matInput #profileInput="matAutocompleteTrigger" [ngModel]="image.profileValue"
|
||||
(ngModelChange)="image.profileValue = $event" [matAutocomplete]="profileAuto"
|
||||
(blur)="resetProfileSearchIfInvalid(image)" [placeholder]="'noSoftwareProfileAssigned' | translate">
|
||||
<mat-icon matSuffix class="software-profile-caret" (click)="profileInput.openPanel()">arrow_drop_down</mat-icon>
|
||||
<mat-autocomplete #profileAuto="matAutocomplete" [displayWith]="displayProfile" panelWidth="auto"
|
||||
class="software-profile-panel" (optionSelected)="onProfileSelected(image, $event.option.value)">
|
||||
<mat-option [value]="null">{{ 'noSoftwareProfileAssigned' | translate }}</mat-option>
|
||||
<mat-option *ngFor="let profile of filterProfiles(image.profileValue)" [value]="profile">
|
||||
{{ profile.description }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
</ng-container>
|
||||
|
||||
<ng-container
|
||||
*ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status' && column.columnDef !== 'isGlobal' && column.columnDef !== 'name' && column.columnDef !== 'datasize'">
|
||||
*ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status' && column.columnDef !== 'isGlobal' && column.columnDef !== 'name' && column.columnDef !== 'datasize' && column.columnDef !== 'softwareProfile'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export class ShowMonoliticImagesComponent implements OnInit {
|
|||
filters: { [key: string]: string } = {};
|
||||
alertMessage: string | null = null;
|
||||
repository: any = {};
|
||||
softwareProfiles: any[] = [];
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
columns = [
|
||||
{
|
||||
|
|
@ -70,9 +71,9 @@ export class ShowMonoliticImagesComponent implements OnInit {
|
|||
cell: (image: any) => this.formatSize(image.datasize)
|
||||
},
|
||||
{
|
||||
columnDef: 'description',
|
||||
header: 'labelDescription',
|
||||
cell: (image: any) => image.description
|
||||
columnDef: 'softwareProfile',
|
||||
header: 'softwareProfile',
|
||||
cell: (image: any) => image.softwareProfile ? image.softwareProfile['@id'] : null
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
|
|
@ -100,13 +101,63 @@ export class ShowMonoliticImagesComponent implements OnInit {
|
|||
if (this.data) {
|
||||
this.loadData();
|
||||
}
|
||||
this.loadSoftwareProfiles();
|
||||
}
|
||||
|
||||
loadSoftwareProfiles(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/software-profiles?page=1&itemsPerPage=10000`).subscribe({
|
||||
next: (response) => {
|
||||
this.softwareProfiles = response['hydra:member'];
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(this.translate.instant('errorGettingProfiles'), error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
filterProfiles(value: any): any[] {
|
||||
const query = typeof value === 'string' ? value : (value?.description || '');
|
||||
const normalized = query.toLowerCase();
|
||||
return this.softwareProfiles.filter(profile => (profile.description || '').toLowerCase().includes(normalized));
|
||||
}
|
||||
|
||||
displayProfile(value: any): string {
|
||||
return value?.description || '';
|
||||
}
|
||||
|
||||
onProfileSelected(image: any, selectedProfile: any | null): void {
|
||||
const previousProfile = image.softwareProfile;
|
||||
|
||||
// Optimistic update: reflect the choice immediately so the blur/reset
|
||||
// handler (which reads image.softwareProfile) doesn't race the PATCH
|
||||
// response and briefly flash back to the old value.
|
||||
image.softwareProfile = selectedProfile;
|
||||
image.profileValue = selectedProfile;
|
||||
|
||||
this.http.patch(`${this.baseUrl}${image['@id']}`, { softwareProfile: selectedProfile ? selectedProfile['@id'] : null }).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('successfullyEditedSoftware'));
|
||||
},
|
||||
error: (error) => {
|
||||
image.softwareProfile = previousProfile;
|
||||
image.profileValue = previousProfile;
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resetProfileSearchIfInvalid(image: any): void {
|
||||
image.profileValue = image.softwareProfile || null;
|
||||
}
|
||||
|
||||
loadData(): void {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&repository.id=${this.data.repositoryId}`, { params: this.filters }).subscribe(
|
||||
data => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.dataSource.data = (data['hydra:member'] || []).map((item: any) => ({
|
||||
...item,
|
||||
profileValue: item.softwareProfile || null
|
||||
}));
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -63,4 +63,28 @@
|
|||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 0em 1.5em 1.5em 1.5em;
|
||||
}
|
||||
|
||||
.software-tab-hint {
|
||||
color: #666;
|
||||
font-size: 0.85em;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.software-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
font-size: 0.85em;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.clear-selection-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #3f51b5;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
padding: 0;
|
||||
}
|
||||
|
|
@ -12,14 +12,6 @@
|
|||
<mat-label>{{ 'labelComments' | translate }}</mat-label>
|
||||
<input matInput formControlName="comments" [placeholder]="'commandComments' | translate">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectOrganizationalUnit' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" required>
|
||||
<mat-option *ngFor="let ou of organizationalUnits" [value]="ou['@id']">
|
||||
{{ ou.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectOperativeSystem' | translate }}</mat-label>
|
||||
<mat-select formControlName="operativeSystem">
|
||||
|
|
@ -33,11 +25,13 @@
|
|||
|
||||
<mat-tab label="Seleccionar Software">
|
||||
<div class="form-group">
|
||||
<p class="software-tab-hint">{{ 'selectSoftwareHint' | translate }}</p>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<input type="text" matInput [formControl]="softwareControl" [matAutocomplete]="clientAuto"
|
||||
[placeholder]="'selectSoftwareToAdd' | translate">
|
||||
|
||||
<mat-autocomplete #clientAuto="matAutocomplete" [displayWith]="displayFnClient"
|
||||
<mat-autocomplete #clientAuto="matAutocomplete" [displayWith]="displayFnClient" autoActiveFirstOption
|
||||
(optionSelected)="onOptionClientSelected($event.option.value)">
|
||||
|
||||
<mat-option *ngFor="let software of filteredSoftware | async" [value]="software">
|
||||
|
|
@ -46,9 +40,17 @@
|
|||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="software-list-header">
|
||||
<span>{{ 'selectedSoftwareCount' | translate: { count: selectedSoftwares.length } }}</span>
|
||||
<button type="button" class="clear-selection-link" *ngIf="selectedSoftwares.length"
|
||||
(click)="clearSelectedSoftwares()">
|
||||
{{ 'clearAll' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="software-list">
|
||||
<mat-list>
|
||||
<mat-list-item *ngFor="let software of softwareCollection">
|
||||
<mat-list-item *ngFor="let software of selectedSoftwares">
|
||||
{{ software.name }}
|
||||
<button mat-icon-button (click)="removeSoftware(software)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
|
@ -21,11 +21,8 @@ export class CreateSoftwareProfileComponent implements OnInit {
|
|||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
formGroup: FormGroup;
|
||||
softwareCollection: any[] = [];
|
||||
organizationalUnits: any[] = [];
|
||||
operativeSystems: any[] = [];
|
||||
softwareProfileId: string | null = null;
|
||||
selectedSoftware: any;
|
||||
selectedSoftwares: any[] = [];
|
||||
softwareControl = new FormControl();
|
||||
filteredSoftware!: Observable<any[]>;
|
||||
|
|
@ -45,7 +42,6 @@ export class CreateSoftwareProfileComponent implements OnInit {
|
|||
this.formGroup = this.fb.group({
|
||||
description: [''],
|
||||
comments: [''],
|
||||
organizationalUnit: [null, Validators.required],
|
||||
operativeSystem: [null]
|
||||
});
|
||||
}
|
||||
|
|
@ -54,10 +50,9 @@ export class CreateSoftwareProfileComponent implements OnInit {
|
|||
if (this.data) {
|
||||
this.softwareProfileId = this.data['@id'];
|
||||
this.patchFormData();
|
||||
this.loadSoftware();
|
||||
}
|
||||
|
||||
this.loadSoftware();
|
||||
this.loadOrganizationalUnits();
|
||||
this.loadOperativeSystems();
|
||||
|
||||
this.filteredSoftware = this.softwareControl.valueChanges.pipe(
|
||||
|
|
@ -71,15 +66,14 @@ export class CreateSoftwareProfileComponent implements OnInit {
|
|||
this.formGroup.patchValue({
|
||||
description: this.data.description || '',
|
||||
comments: this.data.comments || '',
|
||||
organizationalUnit: this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null,
|
||||
operativeSystem: this.data.operativeSystem ? this.data.operativeSystem['@id'] : null,
|
||||
});
|
||||
}
|
||||
|
||||
loadSoftware() {
|
||||
this.http.get<any>(`${this.baseUrl}/software?softwareProfileId=${this.data.id}&page=1&itemsPerPage=10`).subscribe(
|
||||
this.http.get<any>(`${this.baseUrl}/software?softwareProfileId=${this.data.id}&page=1&itemsPerPage=1000`).subscribe(
|
||||
response => {
|
||||
this.softwareCollection = response['hydra:member'];
|
||||
this.selectedSoftwares = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error(this.translate.instant('softwareFetchingError'), error);
|
||||
|
|
@ -87,17 +81,6 @@ export class CreateSoftwareProfileComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
loadOrganizationalUnits() {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`).subscribe(
|
||||
response => {
|
||||
this.organizationalUnits = response['hydra:member'];
|
||||
},
|
||||
error => {
|
||||
console.error(this.translate.instant('errorFetchingOrganizationalUnits1'), error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadOperativeSystems() {
|
||||
this.http.get<any>(`${this.baseUrl}/operative-systems?page=1&itemsPerPage=10000`).subscribe(
|
||||
response => {
|
||||
|
|
@ -110,33 +93,36 @@ export class CreateSoftwareProfileComponent implements OnInit {
|
|||
}
|
||||
|
||||
private _filterSoftware(value: string): Observable<any[]> {
|
||||
return this.softwareDataService.getSoftwareCollection({ name: value }).pipe(
|
||||
map(response => response || [])
|
||||
const filters = { name: value || '', itemsPerPage: '15' };
|
||||
|
||||
return this.softwareDataService.getSoftwareCollection(filters).pipe(
|
||||
map(response => (response || []).filter(software => !this.isSelected(software)))
|
||||
);
|
||||
}
|
||||
|
||||
isSelected(software: any): boolean {
|
||||
return this.selectedSoftwares.some(s => s.id === software.id);
|
||||
}
|
||||
|
||||
displayFnClient(client: any): string {
|
||||
return client && client.name ? client.name : '';
|
||||
}
|
||||
|
||||
onOptionClientSelected(software: any) {
|
||||
if (!this.selectedSoftwares.find(s => s.id === software.id)) {
|
||||
if (!this.isSelected(software)) {
|
||||
this.selectedSoftwares.push(software);
|
||||
}
|
||||
this.softwareControl.setValue('');
|
||||
}
|
||||
|
||||
addSoftware() {
|
||||
const software = this.softwareCollection.find(s => s.id === this.selectedSoftware);
|
||||
if (software && !this.selectedSoftwares.includes(software)) {
|
||||
this.selectedSoftwares.push(software);
|
||||
}
|
||||
}
|
||||
|
||||
removeSoftware(software: any) {
|
||||
this.selectedSoftwares = this.selectedSoftwares.filter(s => s.id !== software.id);
|
||||
}
|
||||
|
||||
clearSelectedSoftwares(): void {
|
||||
this.selectedSoftwares = [];
|
||||
}
|
||||
|
||||
onCancel(event: Event): void {
|
||||
event.preventDefault();
|
||||
this.dialogRef.close();
|
||||
|
|
@ -148,7 +134,6 @@ export class CreateSoftwareProfileComponent implements OnInit {
|
|||
description: this.formGroup.value.description,
|
||||
comments: this.formGroup.value.comments,
|
||||
softwareCollection: this.selectedSoftwares.map(s => s['@id']),
|
||||
organizationalUnit: this.formGroup.value.organizationalUnit,
|
||||
operativeSystem: this.formGroup.value.operativeSystem
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -105,13 +105,13 @@
|
|||
|
||||
<ng-container *ngSwitchCase="'executedAt'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span style="font-size: 0.8rem;"> {{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
|
||||
<span style="font-size: 0.8rem;"> {{ trace.executedAt |date: 'dd/MM/yyyy HH:mm:ss'}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'finishedAt'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span style="font-size: 0.8rem;"> {{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
|
||||
<span style="font-size: 0.8rem;"> {{ trace.finishedAt |date: 'dd/MM/yyyy HH:mm:ss'}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
|
|
|
|||
|
|
@ -53,12 +53,12 @@ export class ClientPendingTasksComponent implements OnInit {
|
|||
{
|
||||
columnDef: 'executedAt',
|
||||
header: 'execution',
|
||||
cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
columnDef: 'finishedAt',
|
||||
header: 'ending',
|
||||
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy HH:mm:ss'),
|
||||
},
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'information'];
|
||||
|
|
@ -105,19 +105,19 @@ export class ClientPendingTasksComponent implements OnInit {
|
|||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data && data['@id']) {
|
||||
this.updateTracesStatus(data['@id'], data.status);
|
||||
this.updateTraceFromEvent(data['@id'], data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateTracesStatus(clientUuid: string, newStatus: string): void {
|
||||
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
|
||||
private updateTraceFromEvent(traceId: string, data: any): void {
|
||||
const traceIndex = this.traces.findIndex(trace => trace['@id'] === traceId);
|
||||
if (traceIndex !== -1) {
|
||||
const updatedTraces = [...this.traces];
|
||||
|
||||
updatedTraces[traceIndex] = {
|
||||
...updatedTraces[traceIndex],
|
||||
status: newStatus
|
||||
...data
|
||||
};
|
||||
|
||||
this.traces = updatedTraces;
|
||||
|
|
@ -240,7 +240,7 @@ export class ClientPendingTasksComponent implements OnInit {
|
|||
|
||||
openOutputModal(outputData: any): void {
|
||||
this.dialog.open(OutputDialogComponent, {
|
||||
width: '500px',
|
||||
width: '70vw',
|
||||
data: { input: outputData }
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,13 +137,13 @@
|
|||
|
||||
<ng-container *ngSwitchCase="'executedAt'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span style="font-size: 0.8rem;"> {{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
|
||||
<span style="font-size: 0.8rem;"> {{ trace.executedAt |date: 'dd/MM/yyyy HH:mm:ss'}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'finishedAt'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span style="font-size: 0.8rem;"> {{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
|
||||
<span style="font-size: 0.8rem;"> {{ trace.finishedAt |date: 'dd/MM/yyyy HH:mm:ss'}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
|
|
|
|||
|
|
@ -65,12 +65,12 @@ export class ClientTaskLogsComponent implements OnInit {
|
|||
{
|
||||
columnDef: 'executedAt',
|
||||
header: 'execution',
|
||||
cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
columnDef: 'finishedAt',
|
||||
header: 'ending',
|
||||
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy HH:mm:ss'),
|
||||
},
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'information'];
|
||||
|
|
@ -107,28 +107,23 @@ export class ClientTaskLogsComponent implements OnInit {
|
|||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data && data['@id']) {
|
||||
this.updateTracesStatus(data['@id'], data.status, data.progress);
|
||||
this.updateTraceFromEvent(data['@id'], data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void {
|
||||
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
|
||||
private updateTraceFromEvent(traceId: string, data: any): void {
|
||||
const traceIndex = this.traces.findIndex(trace => trace['@id'] === traceId);
|
||||
if (traceIndex !== -1) {
|
||||
const updatedTraces = [...this.traces];
|
||||
|
||||
updatedTraces[traceIndex] = {
|
||||
...updatedTraces[traceIndex],
|
||||
status: newStatus,
|
||||
progress: progress
|
||||
...data
|
||||
};
|
||||
|
||||
this.traces = updatedTraces;
|
||||
this.cdr.detectChanges();
|
||||
|
||||
console.log(`Estado actualizado para la traza ${clientUuid}: ${newStatus}`);
|
||||
} else {
|
||||
console.warn(`Traza con UUID ${clientUuid} no encontrado en la lista.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +167,7 @@ export class ClientTaskLogsComponent implements OnInit {
|
|||
|
||||
openOutputModal(outputData: any): void {
|
||||
this.dialog.open(OutputDialogComponent, {
|
||||
width: '500px',
|
||||
width: '70vw',
|
||||
data: { input: outputData }
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<h1 mat-dialog-title>{{ 'inputDetails' | translate }}</h1>
|
||||
<div mat-dialog-content>
|
||||
<pre>{{ data.input | json }}</pre>
|
||||
@if (isString) {
|
||||
<pre>{{ data.input }}</pre>
|
||||
} @else {
|
||||
<pre>{{ data.input | json }}</pre>
|
||||
}
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'buttonClose' | translate }}</button>
|
||||
|
|
|
|||
|
|
@ -15,4 +15,8 @@ export class OutputDialogComponent {
|
|||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
get isString(): boolean {
|
||||
return typeof this.data.input === 'string';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,6 +224,11 @@
|
|||
<span>{{ 'showingResults' | translate: { from: getPaginationFrom(), to: getPaginationTo(), total: getPaginationTotal() } }}</span>
|
||||
</div>
|
||||
<div class="table-actions">
|
||||
<button class="action-button" [disabled]="selection.selected.length === 0" (click)="cancelSelectedTraces()"
|
||||
matTooltip="{{ 'cancelSelectedTraces' | translate }}">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
{{ 'cancelSelectedTraces' | translate }}{{ selection.selected.length > 0 ? ' (' + selection.selected.length + ')' : '' }}
|
||||
</button>
|
||||
<button mat-icon-button (click)="refreshData()" matTooltip="{{ 'buttonUpdate' | translate }}">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
|
|
@ -231,6 +236,22 @@
|
|||
</div>
|
||||
|
||||
<table mat-table [dataSource]="traces" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<mat-checkbox (change)="$event ? toggleAllRows() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()"
|
||||
[disabled]="cancellableTraces.length === 0">
|
||||
</mat-checkbox>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let trace">
|
||||
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(trace)"
|
||||
[checked]="selection.isSelected(trace)"
|
||||
[disabled]="!isCancellable(trace)">
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<div class="column-header">
|
||||
|
|
@ -316,14 +337,14 @@
|
|||
|
||||
<ng-container *ngSwitchCase="'executedAt'">
|
||||
<div class="date-cell">
|
||||
<span class="date-time">{{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
|
||||
<span class="date-time">{{ trace.executedAt |date: 'dd/MM/yyyy HH:mm:ss'}}</span>
|
||||
<span class="date-relative" *ngIf="getRelativeTime(trace.executedAt)">{{ getRelativeTime(trace.executedAt) }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'finishedAt'">
|
||||
<div class="date-cell">
|
||||
<span class="date-time">{{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
|
||||
<span class="date-time">{{ trace.finishedAt |date: 'dd/MM/yyyy HH:mm:ss'}}</span>
|
||||
<span class="date-relative" *ngIf="getRelativeTime(trace.finishedAt)">{{ getRelativeTime(trace.finishedAt) }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { map, startWith } from 'rxjs/operators';
|
|||
import { DatePipe } from '@angular/common';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { InputDialogComponent } from "./input-dialog/input-dialog.component";
|
||||
import { ProgressBarMode } from '@angular/material/progress-bar';
|
||||
import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
|
|
@ -101,17 +102,18 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
{
|
||||
columnDef: 'executedAt',
|
||||
header: 'execution',
|
||||
cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy HH:mm:ss'),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
columnDef: 'finishedAt',
|
||||
header: 'ending',
|
||||
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy HH:mm:ss'),
|
||||
sortable: true
|
||||
},
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'information'];
|
||||
displayedColumns = ['select', ...this.columns.map(column => column.columnDef), 'information'];
|
||||
selection = new SelectionModel<any>(true, []);
|
||||
|
||||
filters: { [key: string]: string } = {};
|
||||
filteredClients!: Observable<any[]>;
|
||||
|
|
@ -155,7 +157,7 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
this.eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data && data['@id']) {
|
||||
this.updateTracesStatus(data['@id'], data.status, data.progress);
|
||||
this.updateTraceFromEvent(data['@id'], data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -179,6 +181,31 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
this.selectedTrace = trace;
|
||||
}
|
||||
|
||||
isCancellable(trace: any): boolean {
|
||||
return trace.status === 'pending' || trace.status === 'in-progress';
|
||||
}
|
||||
|
||||
get cancellableTraces(): any[] {
|
||||
return this.traces.filter(trace => this.isCancellable(trace));
|
||||
}
|
||||
|
||||
isAllSelected(): boolean {
|
||||
return this.selection.selected.length === this.cancellableTraces.length && this.cancellableTraces.length > 0;
|
||||
}
|
||||
|
||||
toggleAllRows(): void {
|
||||
if (this.isAllSelected()) {
|
||||
this.selection.clear();
|
||||
} else {
|
||||
this.selection.clear();
|
||||
this.selection.select(...this.cancellableTraces);
|
||||
}
|
||||
}
|
||||
|
||||
toggleRow(row: any): void {
|
||||
this.selection.toggle(row);
|
||||
}
|
||||
|
||||
sortColumn(columnDef: string): void {
|
||||
if (this.currentSortColumn === columnDef) {
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
|
|
@ -258,8 +285,8 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
this.translateCommand(trace.command),
|
||||
trace.client?.name || '',
|
||||
trace.status,
|
||||
this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy HH:mm:ss'),
|
||||
this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy HH:mm:ss'),
|
||||
trace.jobId || ''
|
||||
]);
|
||||
|
||||
|
|
@ -328,6 +355,55 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
cancelSelectedTraces(): void {
|
||||
const selectedTraces = this.selection.selected.filter(trace => this.isCancellable(trace));
|
||||
|
||||
if (selectedTraces.length === 0) {
|
||||
this.toastService.warning(this.translate.instant('noTracesSelectedToCancel'), 'Advertencia');
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '400px',
|
||||
data: {
|
||||
title: this.translate.instant('cancelSelectedTraces'),
|
||||
message: this.translate.instant('confirmCancelSelectedTraces', { count: selectedTraces.length }),
|
||||
confirmText: 'Cancelar',
|
||||
cancelText: 'No cancelar'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.http.post<any>(`${this.baseUrl}/traces/cancel-multiple`, {
|
||||
traces: selectedTraces.map(trace => trace['@id'])
|
||||
}).subscribe(
|
||||
(response) => {
|
||||
const cancelledCount = response?.cancelled?.length ?? selectedTraces.length;
|
||||
const failedCount = response?.failed?.length ?? 0;
|
||||
|
||||
if (failedCount > 0) {
|
||||
this.toastService.warning(
|
||||
this.translate.instant('selectedTracesCancelledSummary', { cancelled: cancelledCount, failed: failedCount }),
|
||||
'Advertencia'
|
||||
);
|
||||
} else {
|
||||
this.toastService.success(this.translate.instant('traceCanceledSuccessfully'), 'Éxito');
|
||||
}
|
||||
|
||||
this.selection.clear();
|
||||
this.loadTraces();
|
||||
this.loadTotalStats();
|
||||
},
|
||||
(error) => {
|
||||
console.error(this.translate.instant('errorCancelingSelectedTraces'), error);
|
||||
this.toastService.error(this.translate.instant('errorCancelingSelectedTraces'), 'Error');
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
markTraceAsSuccess(trace: any): void {
|
||||
if (trace.status !== 'in-progress') {
|
||||
this.toastService.warning(this.translate.instant('canOnlyBeMarkedAs'), 'Advertencia');
|
||||
|
|
@ -363,15 +439,14 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void {
|
||||
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
|
||||
private updateTraceFromEvent(traceId: string, data: any): void {
|
||||
const traceIndex = this.traces.findIndex(trace => trace['@id'] === traceId);
|
||||
if (traceIndex !== -1) {
|
||||
const updatedTraces = [...this.traces];
|
||||
|
||||
updatedTraces[traceIndex] = {
|
||||
...updatedTraces[traceIndex],
|
||||
status: newStatus,
|
||||
progress: progress
|
||||
...data
|
||||
};
|
||||
|
||||
this.traces = updatedTraces;
|
||||
|
|
@ -423,7 +498,7 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
|
||||
openOutputModal(outputData: any): void {
|
||||
this.dialog.open(OutputDialogComponent, {
|
||||
width: '500px',
|
||||
width: '70vw',
|
||||
data: { input: outputData }
|
||||
});
|
||||
}
|
||||
|
|
@ -471,6 +546,7 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
this.traces = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.groupedTraces = this.groupByCommandId(this.traces);
|
||||
this.selection.clear();
|
||||
this.loading = false;
|
||||
if (Object.keys(this.filters).length === 0) {
|
||||
this.loadTotalStats();
|
||||
|
|
|
|||
|
|
@ -139,7 +139,6 @@ describe('roleGuard — control de acceso por rol', () => {
|
|||
// super-admin + ou-admin
|
||||
{ path: 'commands', allowedRoles: ['super-admin', 'ou-admin'] },
|
||||
{ path: 'commands-task', allowedRoles: ['super-admin', 'ou-admin'] },
|
||||
{ path: 'commands-logs', allowedRoles: ['super-admin', 'ou-admin'] },
|
||||
{ path: 'pxe-images', allowedRoles: ['super-admin', 'ou-admin'] },
|
||||
{ path: 'pxe', allowedRoles: ['super-admin', 'ou-admin'] },
|
||||
{ path: 'pxe-boot-file', allowedRoles: ['super-admin', 'ou-admin'] },
|
||||
|
|
@ -156,6 +155,7 @@ describe('roleGuard — control de acceso por rol', () => {
|
|||
{ path: 'hardware-profiles', allowedRoles: ['super-admin', 'ou-admin'] },
|
||||
{ path: 'hardware-types', allowedRoles: ['super-admin', 'ou-admin'] },
|
||||
// super-admin + ou-admin + ou-operator
|
||||
{ path: 'commands-logs', allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] },
|
||||
{ path: 'clients/deploy-image', allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] },
|
||||
{ path: 'clients/partition-assistant',allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] },
|
||||
{ path: 'clients/run-script', allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] },
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@
|
|||
{{ 'downloadAgent' | translate }}
|
||||
</a>
|
||||
|
||||
<button class="ordinary-button" (click)="showGlobalStatus()">
|
||||
{{ 'GlobalStatus' | translate }}
|
||||
</button>
|
||||
@if (auth.userCategory === 'super-admin' || auth.userCategory === 'ou-admin') {
|
||||
<button class="ordinary-button" (click)="showGlobalStatus()">
|
||||
{{ 'GlobalStatus' | translate }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (auth.userCategory === 'super-admin') {
|
||||
<button class="ordinary-button" [matMenuTriggerFor]="menu"
|
||||
|
|
@ -75,9 +77,11 @@
|
|||
<mat-icon>download</mat-icon>
|
||||
{{ 'downloadAgent' | translate }}
|
||||
</a>
|
||||
<button mat-menu-item (click)="showGlobalStatus()">
|
||||
{{ 'GlobalStatus' | translate }}
|
||||
</button>
|
||||
@if (auth.userCategory === 'super-admin' || auth.userCategory === 'ou-admin') {
|
||||
<button mat-menu-item (click)="showGlobalStatus()">
|
||||
{{ 'GlobalStatus' | translate }}
|
||||
</button>
|
||||
}
|
||||
@if (auth.userCategory === 'super-admin') {
|
||||
<button mat-menu-item [matMenuTriggerFor]="menu">
|
||||
{{ 'Administration' | translate }}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,14 @@ export class HeaderComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
this.breakpointObserver.observe(['(max-width: 576px)']).subscribe((result) => {
|
||||
this.isSmallScreen = result.matches;
|
||||
})
|
||||
});
|
||||
|
||||
// Aplicar idioma guardado: el del JWT (servidor) tiene prioridad sobre localStorage
|
||||
const jwtLang = this.auth.language;
|
||||
const localLang = localStorage.getItem('language');
|
||||
const langToApply = jwtLang || localLang || 'es';
|
||||
this.translate.use(langToApply);
|
||||
localStorage.setItem('language', langToApply);
|
||||
}
|
||||
|
||||
get availableLanguages() {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
</mat-list-item>
|
||||
|
||||
<mat-list-item (click)="toggleCommandSub()" matTooltip="{{ 'TOOLTIP_ACTIONS' | translate }}"
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">playlist_play</mat-icon>
|
||||
<span>{{ 'actions' | translate }}</span>
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item routerLink="/commands-logs" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_TRACE_LOGS' | translate }}"
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">chevron_right</mat-icon>
|
||||
<span>{{ 'traceLogs' | translate }}</span>
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ describe('SidebarComponent — visibilidad de elementos por rol', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ── Ou-operator: solo /groups ──────────────────────────────────────────────
|
||||
describe('ou-operator — solo ve /groups', () => {
|
||||
// ── Ou-operator: /groups y las trazas de sus acciones ──────────────────────
|
||||
describe('ou-operator — ve /groups y /commands-logs', () => {
|
||||
beforeEach(async () => createComponent('ou-operator'));
|
||||
|
||||
it('ve /groups', () => {
|
||||
|
|
@ -130,9 +130,22 @@ describe('SidebarComponent — visibilidad de elementos por rol', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('NO ve el menú de acciones (commands)', () => {
|
||||
// Solo debe haber 1 mat-list-item visible: /groups
|
||||
expect(countVisibleListItems(fixture)).toBe(1);
|
||||
it('ve el menú de acciones (commands) para acceder a las trazas', () => {
|
||||
// /groups + el desplegable de acciones
|
||||
expect(countVisibleListItems(fixture)).toBe(2);
|
||||
});
|
||||
|
||||
it('ve el enlace a /commands-logs al desplegar el menú de acciones', () => {
|
||||
component.toggleCommandSub();
|
||||
fixture.detectChanges();
|
||||
expect(hasRouteLink(fixture, '/commands-logs')).toBeTrue();
|
||||
});
|
||||
|
||||
it('NO ve los enlaces de gestión de comandos ni tareas', () => {
|
||||
component.toggleCommandSub();
|
||||
fixture.detectChanges();
|
||||
expect(hasRouteLink(fixture, '/commands')).toBeFalse();
|
||||
expect(hasRouteLink(fixture, '/commands-task')).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ interface JwtPayload {
|
|||
uuid: string;
|
||||
groupsView: string;
|
||||
clientsPerPage: number | null;
|
||||
language: string | null;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
|
|
@ -103,6 +104,11 @@ export class AuthService {
|
|||
return this.tokenPayload?.clientsPerPage ?? null;
|
||||
}
|
||||
|
||||
/** Idioma preferido del usuario */
|
||||
get language(): string | null {
|
||||
return this.tokenPayload?.language ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve el rol principal del usuario según prioridad.
|
||||
*/
|
||||
|
|
@ -122,6 +128,7 @@ export class AuthService {
|
|||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('groupsView');
|
||||
localStorage.removeItem('language');
|
||||
localStorage.removeItem('lastSelectedOU');
|
||||
this.tokenPayload = null;
|
||||
if (options.redirect && this.router.url !== '/auth/login') {
|
||||
this.router.navigate(['/auth/login']);
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
"advancedParameters": "Paràmetres avançats",
|
||||
"all": "Tots",
|
||||
"allowedOrganizationalUnits": "Unitats Organitzacionals Permeses",
|
||||
"andMoreClients": "i {{count}} més",
|
||||
"anErrorOccurredWhileLoading": "S'ha produït un error en carregar les unitats organitzatives",
|
||||
"apiPort": "Port API",
|
||||
"application": "Aplicació",
|
||||
|
|
@ -152,7 +153,7 @@
|
|||
"buttonSelectIso": "Seleccionar ISO",
|
||||
"buttonSelectOption": "Seleccionar opció",
|
||||
"buttonSelectPartition": "Seleccionar partició",
|
||||
"buttonSelectRepository": "Seleccionar Repositori",
|
||||
"buttonSelectRepository": "Seleccionar Repositori Git",
|
||||
"buttonSync": "Sincronitzar",
|
||||
"buttonUpdate": "Actualitzar",
|
||||
"buttonUpdateData": "Actualitzar dades",
|
||||
|
|
@ -176,6 +177,7 @@
|
|||
"cancelAllDisplayedActions": "Cancel·lar totes les accions mostrades",
|
||||
"cancelImageTransmission": "Cancel·lar transmissió d'imatge",
|
||||
"cancelled": "Cancel·lat",
|
||||
"cancelSelectedTraces": "Cancel·lar seleccionades",
|
||||
"cancelTrace": "Cancel·lant traça",
|
||||
"cannotAccess": "No es pot accedir als registres: IP del client no disponible",
|
||||
"cannotConnectBootServer": "No s'ha pogut connectar amb el servidor OgBoot seleccionat",
|
||||
|
|
@ -211,6 +213,7 @@
|
|||
"classroomStatusUpdated": "Estat de l'aula actualitzat",
|
||||
"classroomTitle": "Aula",
|
||||
"classroomWillBe": "L'aula estarà",
|
||||
"clearAll": "Treure tots",
|
||||
"clearClientSearch": "Netejar cerca de client",
|
||||
"clearInputSearch": "Netejar cerca",
|
||||
"clearTreeSearch": "Netejar cerca a l'arbre",
|
||||
|
|
@ -235,9 +238,12 @@
|
|||
"clientOff": "Client apagat",
|
||||
"clientOgLive": "Client OGLive",
|
||||
"clientPropertiesTitle": "Propietats del client",
|
||||
"clients": "Clients",
|
||||
"clientSelectStepText": "Selecciona un client per veure les traces associades.",
|
||||
"clientsPerPage": "Clients per pàgina",
|
||||
"clientsPerPageHint": "Nombre de clients mostrats per pàgina al llistat",
|
||||
"userLanguage": "Idioma preferit",
|
||||
"userLanguageHint": "Idioma de la interfície per a aquest usuari",
|
||||
"clientSuccessfullyRemovedFromThe": "Client eliminat de la xarxa correctament",
|
||||
"clientsViewStepText": "Visualització dels clients de la unitat organitzativa seleccionada",
|
||||
"clientUpdatedSuccessfully": "Client actualitzat correctament",
|
||||
|
|
@ -283,6 +289,7 @@
|
|||
"configuredParameters": "(paràmetres configurats)",
|
||||
"configureRequiredParameters": "Configuri els valors per als paràmetres requerits",
|
||||
"confirmationTitle": "Confirmació",
|
||||
"confirmCancelSelectedTraces": "Segur que vols cancel·lar {{count}} traça/traces seleccionada(es)?",
|
||||
"confirmServerMigration": "Confirmar migració de servidors",
|
||||
"connectionError": "Error de connexió",
|
||||
"convertImageButton": "Convertir imatge virtual",
|
||||
|
|
@ -422,6 +429,7 @@
|
|||
"errorCancelingActions1": "Error en cancel·lar les accions:",
|
||||
"errorCancelingInprogressTrace": "Error en cancel·lar la traça en progrés:",
|
||||
"errorCancelingPendingTrace": "Error en cancel·lar la traça pendent:",
|
||||
"errorCancelingSelectedTraces": "Error en cancel·lar les traces seleccionades",
|
||||
"errorCancelingTrace": "Error en cancel·lar la traça",
|
||||
"errorChangingPassword": "Error en canviar la contrasenya:",
|
||||
"errorCheckingStatus": "Error en comprovar l'estat de l'aula",
|
||||
|
|
@ -454,6 +462,7 @@
|
|||
"errorEditingRole1": "Error en editar el rol:",
|
||||
"errorEditingSystem": "Error en editar el sistema operatiu",
|
||||
"errorExecutingGroupOfCommands": "Error executant el grup de comandaments:",
|
||||
"errorExecutingTaskNow": "Error en executar la tasca",
|
||||
"errorFetchingAvailableCommands": "Error en obtenir els comandaments disponibles",
|
||||
"errorFetchingBranchesForGit": "Error en obtenir les branques per a les dades de Git",
|
||||
"errorFetchingCalendars": "Error en obtenir els calendaris",
|
||||
|
|
@ -584,11 +593,13 @@
|
|||
"hardwareInventory": "Inventari de maquinari",
|
||||
"runScript": "Executar script",
|
||||
"refresh": "Refrescar",
|
||||
"deleteLease": "Eliminar concessió DHCP"
|
||||
"deleteLease": "Eliminar concessió DHCP",
|
||||
"executeTask": "Executar tasca"
|
||||
},
|
||||
"executeCommandStepText": "Conjunt d'accions a executar sobre una selecció de clients o unitat organitzativa",
|
||||
"executeCommandTitle": "Executar Comandament",
|
||||
"executeCommandTooltip": "Executar comandament en aquest element",
|
||||
"executeTaskNowModalTitle": "Executar tasca ara",
|
||||
"execution": "Execució",
|
||||
"executionDate": "Data d'execució",
|
||||
"executionDateTimeSectionTitle": "Data i hora d'execució",
|
||||
|
|
@ -1015,6 +1026,7 @@
|
|||
"noFilesInDirectory": "No hi ha fitxers en aquest directori",
|
||||
"noGitInfoForPartition": "No s'ha trobat informació de Git per a aquesta partició",
|
||||
"noHardwareAssociatedToProfile": "No hi ha maquinari associat a aquest perfil",
|
||||
"noHardwareProfileAssigned": "Aquest client no té cap perfil de maquinari assignat",
|
||||
"noInternalElementsMessage": "No hi ha elements interns",
|
||||
"noLogsAvailable": "noLogsAvailable",
|
||||
"noOgLivesInServer": "No hi ha OgLives en aquest servidor",
|
||||
|
|
@ -1027,14 +1039,17 @@
|
|||
"noReposAvailableCreateNew": "No hi ha repositoris disponibles. Crea un de nou per continuar.",
|
||||
"noResultsMessage": "No hi ha resultats per mostrar.",
|
||||
"noSavedScripts": "No hi ha scripts desats disponibles",
|
||||
"noSoftwareProfileAssigned": "Sense perfil de programari assignat",
|
||||
"notAccessible": "No accessible",
|
||||
"noTags": "Sense etiquetes",
|
||||
"noTasksAvailableMessage": "No hi ha tasques disponibles",
|
||||
"notAvailableLowercase": "no disponible",
|
||||
"notCalculated": "No calculada",
|
||||
"notDefined": "No definit",
|
||||
"noTemplateOption": "Cap",
|
||||
"noTemplatesInServer": "No hi ha plantilles en aquest servidor",
|
||||
"notEnoughFreeSpace": "No hi ha suficient espai lliure al disc per crear una nova partició.",
|
||||
"noTracesSelectedToCancel": "No hi ha traces seleccionades per cancel·lar",
|
||||
"notesPlaceholder": "Ingressa les teves notes aquí",
|
||||
"notValidDataFound": "No s'han trobat dades vàlides",
|
||||
"ntfsOption": "NTFS",
|
||||
|
|
@ -1264,6 +1279,8 @@
|
|||
"selectedClients": "Clients seleccionats",
|
||||
"selectedClientsTitle": "Clients seleccionats:",
|
||||
"selectedCommandsTitle": "Comandaments Seleccionats",
|
||||
"selectedSoftwareCount": "{{count}} programari afegit",
|
||||
"selectedTracesCancelledSummary": "{{cancelled}} cancel·lada(es), {{failed}} no s'han pogut cancel·lar",
|
||||
"selectGitRepoHint": "Selecciona el repositori git per obtenir les imatges disponibles.",
|
||||
"selectGitRepoOs": "Seleccionar repositori git / SO",
|
||||
"selectGitRepository": "Seleccionar repositori Git",
|
||||
|
|
@ -1280,10 +1297,11 @@
|
|||
"selectParentOuDhcpHint": "Seleccioni una Unitat Organitzativa Pare (OU) on agrupar les subxarxes i clients que es detectin automàticament en el servei DHCP.",
|
||||
"selectPartitionToBootOs": "Seleccionar partició per arrencar SO",
|
||||
"selectPartitionToInventory": "Seleccionar partició per inventariar",
|
||||
"selectRepository": "Seleccionar repositori",
|
||||
"selectRepository": "Seleccionar Repositori OG",
|
||||
"selectRepoToView": "Tria un repositori de la llista per veure el seu estat detallat.",
|
||||
"selectScriptToRun": "Seleccioni script a executar",
|
||||
"selectScriptToView": "Tria un script desat de la llista per veure el seu contingut i configurar paràmetres.",
|
||||
"selectSoftwareHint": "Escriu per cercar i prem Retorn o fes clic per afegir-lo. El camp s'esborra automàticament perquè puguis continuar afegint programari sense interrupcions.",
|
||||
"selectSoftwareToAdd": "Seleccioni programari per afegir",
|
||||
"selectTask": "Seleccioni una tasca",
|
||||
"selectTemplateDescription": "Seleccioni plantilla",
|
||||
|
|
@ -1395,6 +1413,7 @@
|
|||
"taskCreatedSuccessfully": "Tasca creada correctament",
|
||||
"taskDeletedSuccessfully": "Tasca eliminada correctament",
|
||||
"taskDetailsTitle": "Detalls de la Tasca",
|
||||
"taskExecutedSuccessfully": "Tasca executada correctament",
|
||||
"taskId": "ID de la Tasca",
|
||||
"taskName": "Nom de la tasca",
|
||||
"tasks": "Tasques",
|
||||
|
|
@ -1503,6 +1522,7 @@
|
|||
"viewInput": "Veure entrada",
|
||||
"viewOutput": "Veure sortida",
|
||||
"viewParameters": "Veure paràmetres",
|
||||
"viewScript": "Veure script",
|
||||
"viewPendingTraces": "Veure traces pendents",
|
||||
"viewSentTraces": "Veure traces enviades",
|
||||
"viewSubnets": "Veure Subxarxes",
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
"advancedParameters": "Advanced parameters",
|
||||
"all": "All",
|
||||
"allowedOrganizationalUnits": "Allowed Organizational Units",
|
||||
"andMoreClients": "and {{count}} more",
|
||||
"anErrorOccurredWhileLoading": "An error occurred while loading the organizational units",
|
||||
"apiPort": "API Port",
|
||||
"application": "Application",
|
||||
|
|
@ -152,7 +153,7 @@
|
|||
"buttonSelectIso": "Select ISO",
|
||||
"buttonSelectOption": "Select option",
|
||||
"buttonSelectPartition": "Select partition",
|
||||
"buttonSelectRepository": "Select Repository",
|
||||
"buttonSelectRepository": "Select Git Repository",
|
||||
"buttonSync": "Sync",
|
||||
"buttonUpdate": "Refresh",
|
||||
"buttonUpdateData": "Update data",
|
||||
|
|
@ -176,6 +177,7 @@
|
|||
"cancelAllDisplayedActions": "Cancel all displayed actions",
|
||||
"cancelImageTransmission": "Cancel image transmission",
|
||||
"cancelled": "Cancelled",
|
||||
"cancelSelectedTraces": "Cancel selected",
|
||||
"cancelTrace": "Cancelling Trace",
|
||||
"cannotAccess": "Cannot access logs: Client IP not available",
|
||||
"cannotConnectBootServer": "Could not connect to the selected OgBoot server",
|
||||
|
|
@ -211,6 +213,7 @@
|
|||
"classroomStatusUpdated": "Classroom status updated",
|
||||
"classroomTitle": "Classroom",
|
||||
"classroomWillBe": "The classroom will be",
|
||||
"clearAll": "Clear all",
|
||||
"clearClientSearch": "Clear client search",
|
||||
"clearInputSearch": "Clear input search",
|
||||
"clearTreeSearch": "Clear tree search",
|
||||
|
|
@ -235,9 +238,12 @@
|
|||
"clientOff": "Client off",
|
||||
"clientOgLive": "OGLive client",
|
||||
"clientPropertiesTitle": "Client properties",
|
||||
"clients": "Clients",
|
||||
"clientSelectStepText": "Select a client to see the associated traces.",
|
||||
"clientsPerPage": "Clients per page",
|
||||
"clientsPerPageHint": "Number of clients shown per page in the list",
|
||||
"userLanguage": "Preferred language",
|
||||
"userLanguageHint": "Interface language for this user",
|
||||
"clientSuccessfullyRemovedFromThe": "Client successfully removed from the network",
|
||||
"clientsViewStepText": "Display of the selected organizational unit's clients",
|
||||
"clientUpdatedSuccessfully": "Client updated successfully",
|
||||
|
|
@ -283,6 +289,7 @@
|
|||
"configuredParameters": "(configured parameters)",
|
||||
"configureRequiredParameters": "Configure values for required parameters",
|
||||
"confirmationTitle": "Confirmation",
|
||||
"confirmCancelSelectedTraces": "Are you sure you want to cancel {{count}} selected trace(s)?",
|
||||
"confirmServerMigration": "Confirm server migration",
|
||||
"connectionError": "Connection error",
|
||||
"convertImageButton": "Convert virtual image",
|
||||
|
|
@ -422,6 +429,7 @@
|
|||
"errorCancelingActions1": "Error canceling actions:",
|
||||
"errorCancelingInprogressTrace": "Error cancelling in-progress trace:",
|
||||
"errorCancelingPendingTrace": "Error cancelling pending trace:",
|
||||
"errorCancelingSelectedTraces": "Error cancelling selected traces",
|
||||
"errorCancelingTrace": "Error canceling trace",
|
||||
"errorChangingPassword": "Error changing password:",
|
||||
"errorCheckingStatus": "Error checking classroom status",
|
||||
|
|
@ -454,6 +462,7 @@
|
|||
"errorEditingRole1": "Error editing role:",
|
||||
"errorEditingSystem": "Error editing operating system",
|
||||
"errorExecutingGroupOfCommands": "Error executing command group:",
|
||||
"errorExecutingTaskNow": "Error executing the task",
|
||||
"errorFetchingAvailableCommands": "Error fetching available commands",
|
||||
"errorFetchingBranchesForGit": "Error fetching branches for Git data",
|
||||
"errorFetchingCalendars": "Error fetching calendars",
|
||||
|
|
@ -584,11 +593,13 @@
|
|||
"hardwareInventory": "Hardware Inventory",
|
||||
"runScript": "Run Script",
|
||||
"refresh": "Refresh",
|
||||
"deleteLease": "Delete DHCP Lease"
|
||||
"deleteLease": "Delete DHCP Lease",
|
||||
"executeTask": "Run Task"
|
||||
},
|
||||
"executeCommandStepText": "Set of actions to be performed on a selection of clients or an organizational unit.",
|
||||
"executeCommandTitle": "Execute Command",
|
||||
"executeCommandTooltip": "Execute command on this element",
|
||||
"executeTaskNowModalTitle": "Run Task Now",
|
||||
"execution": "Execution",
|
||||
"executionDate": "Execution date",
|
||||
"executionDateTimeSectionTitle": "Execution date and time",
|
||||
|
|
@ -1015,6 +1026,7 @@
|
|||
"noFilesInDirectory": "There are no files in this directory",
|
||||
"noGitInfoForPartition": "No Git information found for this partition",
|
||||
"noHardwareAssociatedToProfile": "There is no hardware associated with this profile",
|
||||
"noHardwareProfileAssigned": "This client does not have any hardware profile assigned",
|
||||
"noInternalElementsMessage": "No internal elements",
|
||||
"noLogsAvailable": "noLogsAvailable",
|
||||
"noOgLivesInServer": "There are no OgLives on this server",
|
||||
|
|
@ -1027,14 +1039,17 @@
|
|||
"noReposAvailableCreateNew": "No repositories available. Create a new one to continue.",
|
||||
"noResultsMessage": "No results to display.",
|
||||
"noSavedScripts": "No saved scripts available",
|
||||
"noSoftwareProfileAssigned": "No software profile assigned",
|
||||
"notAccessible": "Not accessible",
|
||||
"noTags": "No tags",
|
||||
"noTasksAvailableMessage": "No tasks available",
|
||||
"notAvailableLowercase": "not available",
|
||||
"notCalculated": "Not calculated",
|
||||
"notDefined": "Not defined",
|
||||
"noTemplateOption": "None",
|
||||
"noTemplatesInServer": "There are no templates on this server",
|
||||
"notEnoughFreeSpace": "Not enough free space on the disk to create a new partition.",
|
||||
"noTracesSelectedToCancel": "No traces selected to cancel",
|
||||
"notesPlaceholder": "Enter your notes here",
|
||||
"notValidDataFound": "No valid data found",
|
||||
"ntfsOption": "NTFS",
|
||||
|
|
@ -1264,6 +1279,8 @@
|
|||
"selectedClients": "Selected clients",
|
||||
"selectedClientsTitle": "Selected clients:",
|
||||
"selectedCommandsTitle": "Selected Commands",
|
||||
"selectedSoftwareCount": "{{count}} software added",
|
||||
"selectedTracesCancelledSummary": "{{cancelled}} cancelled, {{failed}} could not be cancelled",
|
||||
"selectGitRepoHint": "Select the git repository to get available images.",
|
||||
"selectGitRepoOs": "Select git repository / OS",
|
||||
"selectGitRepository": "Select Git repository",
|
||||
|
|
@ -1280,10 +1297,11 @@
|
|||
"selectParentOuDhcpHint": "Select a Parent Organizational Unit (OU) to group the subnets and clients automatically detected in the DHCP service.",
|
||||
"selectPartitionToBootOs": "Select partition to boot OS",
|
||||
"selectPartitionToInventory": "Select partition to inventory",
|
||||
"selectRepository": "Select repository",
|
||||
"selectRepository": "Select OG Repository",
|
||||
"selectRepoToView": "Select a repository from the list to view its detailed status.",
|
||||
"selectScriptToRun": "Select script to run",
|
||||
"selectScriptToView": "Select a saved script from the list to view its content and configure parameters.",
|
||||
"selectSoftwareHint": "Type to search and press Enter or click to add it. The field clears automatically so you can keep adding software without interruptions.",
|
||||
"selectSoftwareToAdd": "Select software to add",
|
||||
"selectTask": "Select a task",
|
||||
"selectTemplateDescription": "Select template description",
|
||||
|
|
@ -1395,6 +1413,7 @@
|
|||
"taskCreatedSuccessfully": "Task created successfully",
|
||||
"taskDeletedSuccessfully": "Task deleted successfully",
|
||||
"taskDetailsTitle": "Task Details",
|
||||
"taskExecutedSuccessfully": "Task executed successfully",
|
||||
"taskId": "Task ID",
|
||||
"taskName": "Task name",
|
||||
"tasks": "Tasks",
|
||||
|
|
@ -1503,6 +1522,7 @@
|
|||
"viewInput": "View input",
|
||||
"viewOutput": "View output",
|
||||
"viewParameters": "View parameters",
|
||||
"viewScript": "View script",
|
||||
"viewPendingTraces": "View pending traces",
|
||||
"viewSentTraces": "View sent traces",
|
||||
"viewSubnets": "View Subnets",
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
"advancedParameters": "Parámetros avanzados",
|
||||
"all": "Todos",
|
||||
"allowedOrganizationalUnits": "Unidades Organizacionales Permitidas",
|
||||
"andMoreClients": "y {{count}} más",
|
||||
"anErrorOccurredWhileLoading": "Ocurrió un error al cargar las unidades organizativas",
|
||||
"apiPort": "Puerto API",
|
||||
"application": "Aplicacion",
|
||||
|
|
@ -152,7 +153,7 @@
|
|||
"buttonSelectIso": "Seleccionar ISO",
|
||||
"buttonSelectOption": "Seleccionar opción",
|
||||
"buttonSelectPartition": "Seleccionar partición",
|
||||
"buttonSelectRepository": "Seleccionar Repositorio",
|
||||
"buttonSelectRepository": "Seleccionar Repositorio Git",
|
||||
"buttonSync": "Sincronizar",
|
||||
"buttonUpdate": "Actualizar",
|
||||
"buttonUpdateData": "Actualizar datos",
|
||||
|
|
@ -176,6 +177,7 @@
|
|||
"cancelAllDisplayedActions": "Cancelar todas las acciones mostradas",
|
||||
"cancelImageTransmission": "Cancelar transmisión de imagen",
|
||||
"cancelled": "Cancelado",
|
||||
"cancelSelectedTraces": "Cancelar seleccionadas",
|
||||
"cancelTrace": "Cancelando traza",
|
||||
"cannotAccess": "No se puede acceder a los logs: IP del cliente no disponible",
|
||||
"cannotConnectBootServer": "No se pudo conectar con el servidor OgBoot seleccionado",
|
||||
|
|
@ -211,6 +213,7 @@
|
|||
"classroomStatusUpdated": "Estado del aula actualizado",
|
||||
"classroomTitle": "Aula",
|
||||
"classroomWillBe": "El aula estará",
|
||||
"clearAll": "Quitar todos",
|
||||
"clearClientSearch": "Limpiar búsqueda de cliente",
|
||||
"clearInputSearch": "Limpiar búsqueda",
|
||||
"clearTreeSearch": "Limpiar búsqueda en el árbol",
|
||||
|
|
@ -235,9 +238,12 @@
|
|||
"clientOff": "Cliente apagado",
|
||||
"clientOgLive": "Cliente OGLive",
|
||||
"clientPropertiesTitle": "Propiedades del cliente",
|
||||
"clients": "Clientes",
|
||||
"clientSelectStepText": "Selecciona un cliente para ver las trazas asociadas.",
|
||||
"clientsPerPage": "Clientes por página",
|
||||
"clientsPerPageHint": "Número de clientes mostrados por página en el listado",
|
||||
"userLanguage": "Idioma preferido",
|
||||
"userLanguageHint": "Idioma de la interfaz para este usuario",
|
||||
"clientSuccessfullyRemovedFromThe": "Cliente eliminado de la red con éxito",
|
||||
"clientsViewStepText": "Visualización de los clientes de la unidad organizativa seleccionada",
|
||||
"clientUpdatedSuccessfully": "Cliente actualizado con éxito",
|
||||
|
|
@ -283,6 +289,7 @@
|
|||
"configuredParameters": "(parámetros configurados)",
|
||||
"configureRequiredParameters": "Configure los valores para los parámetros requeridos",
|
||||
"confirmationTitle": "Confirmación",
|
||||
"confirmCancelSelectedTraces": "¿Estás seguro de que quieres cancelar {{count}} traza(s) seleccionada(s)?",
|
||||
"confirmServerMigration": "Confirmar migración de servidores",
|
||||
"connectionError": "Error de conexión",
|
||||
"convertImageButton": "Convertir imagen virtual",
|
||||
|
|
@ -422,6 +429,7 @@
|
|||
"errorCancelingActions1": "Error al cancelar las acciones:",
|
||||
"errorCancelingInprogressTrace": "Error al cancelar la traza en progreso:",
|
||||
"errorCancelingPendingTrace": "Error al cancelar la traza pendiente:",
|
||||
"errorCancelingSelectedTraces": "Error al cancelar las trazas seleccionadas",
|
||||
"errorCancelingTrace": "Error al cancelar la traza",
|
||||
"errorChangingPassword": "Error al cambiar la contraseña:",
|
||||
"errorCheckingStatus": "Error al comprobar el estado del aula",
|
||||
|
|
@ -454,6 +462,7 @@
|
|||
"errorEditingRole1": "Error al editar el rol:",
|
||||
"errorEditingSystem": "Error al editar el sistema operativo",
|
||||
"errorExecutingGroupOfCommands": "Error ejecutando el grupo de comandos:",
|
||||
"errorExecutingTaskNow": "Error al ejecutar la tarea",
|
||||
"errorFetchingAvailableCommands": "Error al obtener los comandos disponibles",
|
||||
"errorFetchingBranchesForGit": "Error al obtener las ramas para los datos de Git",
|
||||
"errorFetchingCalendars": "Error al obtener los calendarios",
|
||||
|
|
@ -584,11 +593,13 @@
|
|||
"hardwareInventory": "Inventario de hardware",
|
||||
"runScript": "Ejecutar script",
|
||||
"refresh": "Refrescar",
|
||||
"deleteLease": "Eliminar concesión DHCP"
|
||||
"deleteLease": "Eliminar concesión DHCP",
|
||||
"executeTask": "Ejecutar tarea"
|
||||
},
|
||||
"executeCommandStepText": "Conjunto de acciones a ejecutar sobre una selección de clientes o unidad organizativa",
|
||||
"executeCommandTitle": "Ejecutar Comando",
|
||||
"executeCommandTooltip": "Ejecutar comando en este elemento",
|
||||
"executeTaskNowModalTitle": "Ejecutar tarea ahora",
|
||||
"execution": "Ejecución",
|
||||
"executionDate": "Fecha de ejecución",
|
||||
"executionDateTimeSectionTitle": "Fecha y hora de ejecución",
|
||||
|
|
@ -1015,6 +1026,7 @@
|
|||
"noFilesInDirectory": "No hay archivos en este directorio",
|
||||
"noGitInfoForPartition": "No se encontró información de Git para esta partición",
|
||||
"noHardwareAssociatedToProfile": "No hay hardware asociado a este perfil",
|
||||
"noHardwareProfileAssigned": "Este cliente no tiene ningún perfil de hardware asignado",
|
||||
"noInternalElementsMessage": "No hay elementos internos",
|
||||
"noLogsAvailable": "noLogsAvailable",
|
||||
"noOgLivesInServer": "No hay OgLives en este servidor",
|
||||
|
|
@ -1027,14 +1039,17 @@
|
|||
"noReposAvailableCreateNew": "No hay repositorios disponibles. Crea uno nuevo para continuar.",
|
||||
"noResultsMessage": "No hay resultados para mostrar.",
|
||||
"noSavedScripts": "No hay scripts guardados disponibles",
|
||||
"noSoftwareProfileAssigned": "Sin perfil de software asignado",
|
||||
"notAccessible": "No accesible",
|
||||
"noTags": "Sin tags",
|
||||
"noTasksAvailableMessage": "No hay tareas disponibles",
|
||||
"notAvailableLowercase": "no disponible",
|
||||
"notCalculated": "No calculada",
|
||||
"notDefined": "No definido",
|
||||
"noTemplateOption": "Ninguna",
|
||||
"noTemplatesInServer": "No hay plantillas en este servidor",
|
||||
"notEnoughFreeSpace": "No hay suficiente espacio libre en el disco para crear una nueva partición.",
|
||||
"noTracesSelectedToCancel": "No hay trazas seleccionadas para cancelar",
|
||||
"notesPlaceholder": "Ingresa tus notas aquí",
|
||||
"notValidDataFound": "No se encontraron datos válidos",
|
||||
"ntfsOption": "NTFS",
|
||||
|
|
@ -1264,6 +1279,8 @@
|
|||
"selectedClients": "Clientes seleccionados",
|
||||
"selectedClientsTitle": "Clientes seleccionados:",
|
||||
"selectedCommandsTitle": "Comandos Seleccionados",
|
||||
"selectedSoftwareCount": "{{count}} programas añadidos",
|
||||
"selectedTracesCancelledSummary": "{{cancelled}} cancelada(s), {{failed}} no se pudieron cancelar",
|
||||
"selectGitRepoHint": "Selecciona el repositorio git para obtener las imágenes disponibles.",
|
||||
"selectGitRepoOs": "Seleccionar repositorio git / SO",
|
||||
"selectGitRepository": "Seleccionar repositorio Git",
|
||||
|
|
@ -1280,10 +1297,11 @@
|
|||
"selectParentOuDhcpHint": "Seleccione una Unidad Organizativa Padre (OU) donde agrupar las subredes y clientes que se detecten automáticamente en el servicio DHCP.",
|
||||
"selectPartitionToBootOs": "Seleccionar particion para arrancar SO",
|
||||
"selectPartitionToInventory": "Seleccionar partición para inventariar",
|
||||
"selectRepository": "Seleccionar repositorio",
|
||||
"selectRepository": "Seleccionar Repositorio OG",
|
||||
"selectRepoToView": "Elige un repositorio de la lista para ver su estado detallado.",
|
||||
"selectScriptToRun": "Seleccione script a ejecutar",
|
||||
"selectScriptToView": "Elige un script guardado de la lista para ver su contenido y configurar parámetros.",
|
||||
"selectSoftwareHint": "Escribe para buscar y pulsa Enter o haz clic para añadir. El campo se vacía automáticamente para que puedas seguir añadiendo software sin interrupciones.",
|
||||
"selectSoftwareToAdd": "Seleccione software para añadir",
|
||||
"selectTask": "Seleccione una tarea",
|
||||
"selectTemplateDescription": "Seleccione plantilla",
|
||||
|
|
@ -1395,6 +1413,7 @@
|
|||
"taskCreatedSuccessfully": "Tarea creada con éxito",
|
||||
"taskDeletedSuccessfully": "Tarea eliminada con éxito",
|
||||
"taskDetailsTitle": "Detalles de la Tarea",
|
||||
"taskExecutedSuccessfully": "Tarea ejecutada correctamente",
|
||||
"taskId": "ID de la Tarea",
|
||||
"taskName": "Nombre de la tarea",
|
||||
"tasks": "Tareas",
|
||||
|
|
@ -1503,6 +1522,7 @@
|
|||
"viewInput": "Ver entrada",
|
||||
"viewOutput": "Ver salida",
|
||||
"viewParameters": "Ver parámetros",
|
||||
"viewScript": "Ver script",
|
||||
"viewPendingTraces": "Ver trazas pendientes",
|
||||
"viewSentTraces": "Ver trazas enviadas",
|
||||
"viewSubnets": "Ver Subredes",
|
||||
|
|
|
|||
Loading…
Reference in New Issue