ref 3009. Added excecute task right now
oggui-debian-package/pipeline/head This commit looks good Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details

pull/97/head
Manuel Aranda Rosales 2026-07-15 11:17:34 +02:00
parent c72d49f576
commit 0adae7dbae
14 changed files with 288 additions and 16 deletions

View File

@ -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,

View File

@ -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 {

View File

@ -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 }})

View File

@ -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',

View File

@ -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;
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -459,6 +459,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",
@ -589,11 +590,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ó",
@ -1036,6 +1039,7 @@
"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",
@ -1404,6 +1408,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",
@ -1512,6 +1517,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",

View File

@ -459,6 +459,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",
@ -589,11 +590,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",
@ -1036,6 +1039,7 @@
"noSoftwareProfileAssigned": "No software profile assigned",
"notAccessible": "Not accessible",
"noTags": "No tags",
"noTasksAvailableMessage": "No tasks available",
"notAvailableLowercase": "not available",
"notCalculated": "Not calculated",
"notDefined": "Not defined",
@ -1404,6 +1408,7 @@
"taskCreatedSuccessfully": "Task created successfully",
"taskDeletedSuccessfully": "Task deleted successfully",
"taskDetailsTitle": "Task Details",
"taskExecutedSuccessfully": "Task executed successfully",
"taskId": "Task ID",
"taskName": "Task name",
"tasks": "Tasks",
@ -1512,6 +1517,7 @@
"viewInput": "View input",
"viewOutput": "View output",
"viewParameters": "View parameters",
"viewScript": "View script",
"viewPendingTraces": "View pending traces",
"viewSentTraces": "View sent traces",
"viewSubnets": "View Subnets",

View File

@ -459,6 +459,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",
@ -589,11 +590,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",
@ -1036,6 +1039,7 @@
"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",
@ -1404,6 +1408,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",
@ -1512,6 +1517,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",