refs #2596. Create task options
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details

pull/33/head
Manuel Aranda Rosales 2025-08-05 10:19:46 +02:00
parent 0a14bbd486
commit 3270f75f15
7 changed files with 262 additions and 21 deletions

View File

@ -33,9 +33,9 @@ export class CommandsTaskComponent implements OnInit {
columns = [
{ columnDef: 'id', header: 'ID', cell: (task: any) => task.id },
{ columnDef: 'name', header: 'Nombre de tarea', cell: (task: any) => task.name },
{ columnDef: 'organizationalUnit', header: 'Ámbito', cell: (task: any) => task.organizationalUnit.name },
{ columnDef: 'organizationalUnit', header: 'Ámbito', cell: (task: any) => task.scope },
{ columnDef: 'management', header: 'Gestiones', cell: (task: any) => task.schedules },
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (task: any) => this.datePipe.transform(task.nextExecution, 'dd/MM/yyyy HH:mm:ss', 'UTC') },
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (task: any) => this.datePipe.transform(task.nextExecution, 'dd/MM/yyyy HH:mm:ss') },
{ columnDef: 'createdBy', header: 'Creado por', cell: (task: any) => task.createdBy },
];

View File

@ -22,7 +22,6 @@
<input matInput formControlName="executionTime" placeholder="08:00" type="time">
</mat-form-field>
<!-- Mostrar solo si no es 'none' -->
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
<label>Días de la semana:</label>
<div class="weekday-toggle-group">
@ -37,7 +36,6 @@
</div>
</div>
<!-- Selección de meses -->
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
<label>Meses:</label>
<div class="month-toggle-row" *ngFor="let row of monthRows">
@ -52,7 +50,6 @@
</div>
</div>
<!-- Rango de fechas -->
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails">
<mat-form-field appearance="fill" class="w-half">
<mat-label>Desde</mat-label>

View File

@ -19,6 +19,14 @@ mat-form-field {
margin-bottom: 16px;
}
mat-form-field.mat-form-field-disabled {
opacity: 0.7;
}
mat-form-field.mat-form-field-disabled .mat-form-field-label {
color: #666;
}
.loading-spinner {
display: block;
margin: 0 auto;
@ -63,3 +71,148 @@ mat-form-field {
gap: 1em;
padding: 1.5em;
}
/* Estilos para la selección de clientes */
.clients-selection {
margin-bottom: 16px;
}
.clients-selection h4 {
margin-bottom: 16px;
color: #333;
font-weight: 500;
}
.pre-selected-info {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background-color: #e3f2fd;
border: 1px solid #2196f3;
border-radius: 4px;
margin-bottom: 16px;
color: #1976d2;
font-size: 14px;
}
.pre-selected-info mat-icon {
color: #2196f3;
font-size: 20px;
}
.loading-clients {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
color: #666;
}
.clients-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.selected-count {
font-weight: 500;
color: #1976d2;
}
.clients-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 12px;
max-height: 300px;
overflow-y: auto;
padding: 8px 0;
}
.client-card {
transition: all 0.2s ease;
border: 2px solid transparent;
position: relative;
}
.client-card.pre-selected {
border-color: #4caf50;
background-color: #e8f5e8;
}
.client-card.pre-selected:hover {
border-color: #45a049;
background-color: #d4edda;
}
.client-card mat-card-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
}
.client-info {
flex: 1;
}
.client-name {
font-weight: 500;
margin-bottom: 4px;
color: #333;
}
.client-details {
display: flex;
gap: 8px;
font-size: 12px;
color: #666;
}
.client-ip {
font-family: monospace;
}
.client-status {
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
}
.status-og-live {
background-color: #4caf50;
color: white;
}
.status-offline {
background-color: #f44336;
color: white;
}
.status-unknown {
background-color: #ff9800;
color: white;
}
.selected-icon {
color: #1976d2;
font-size: 20px;
}
/* Responsive design */
@media (max-width: 768px) {
.clients-grid {
grid-template-columns: 1fr;
}
.clients-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}

View File

@ -5,13 +5,11 @@
<mat-dialog-content class="dialog-content">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<!-- Toggle entre crear o añadir -->
<mat-radio-group *ngIf="data?.source === 'assistant'" [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode">
<mat-radio-button value="create">Crear tarea</mat-radio-button>
<mat-radio-button value="add">Introducir en tarea existente</mat-radio-button>
</mat-radio-group>
<!-- Selección de tarea existente -->
<div *ngIf="taskMode === 'add'" class="select-task">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione una tarea</mat-label>
@ -33,7 +31,6 @@
</mat-form-field>
</div>
<!-- Formulario de nueva tarea -->
<form *ngIf="taskMode === 'create' && taskForm && !loading" [formGroup]="taskForm" class="task-form">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
@ -48,15 +45,17 @@
<mat-form-field appearance="fill" class="full-width">
<mat-label>Ámbito</mat-label>
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)">
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)"
[disabled]="data?.clients && data.clients.length >= 1">
<mat-option value="organizational-unit">Unidad Organizativa</mat-option>
<mat-option value="classrooms-group">Grupo de aulas</mat-option>
<mat-option value="classroom">Aulas</mat-option>
<mat-option value="clients-group">Grupos de clientes</mat-option>
<mat-option value="clients">Clientes</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-form-field *ngIf="taskForm.get('scope')?.value !== 'clients'" appearance="fill" class="full-width">
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
<mat-select formControlName="organizationalUnit">
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
@ -66,6 +65,40 @@
</mat-select>
</mat-form-field>
<div *ngIf="taskForm.get('scope')?.value === 'clients'" class="clients-selection">
<h4>Clientes seleccionados</h4>
<div *ngIf="data?.selectedClients && data.selectedClients.length > 0" class="pre-selected-info">
<mat-icon>info</mat-icon>
<span>Los clientes han sido pre-seleccionados desde el componente de despliegue de imágenes.</span>
</div>
<div class="clients-list">
<div class="clients-grid">
<mat-card
*ngFor="let client of clients"
class="client-card"
[class.pre-selected]="isClientPreSelected(client)"
>
<mat-card-content>
<div class="client-info">
<div class="client-name">{{ client.name || client.hostname }}</div>
<div class="client-details">
<span class="client-ip">{{ client.ip }}</span>
<span class="client-status" [class]="'status-' + client.status">
{{ client.status }}
</span>
</div>
</div>
<mat-icon class="selected-icon">
check_circle
</mat-icon>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
<mat-checkbox *ngIf="!editing" formControlName="scheduleAfterCreate">
¿Quieres programar la tarea al finalizar su creación?
</mat-checkbox>

View File

@ -29,6 +29,7 @@ export class CreateTaskComponent implements OnInit {
existingTasks: any[] = [];
selectedExistingTask: string | null = null;
executionOrder: number | null = null;
selectedClients: any[] = [];
constructor(
private fb: FormBuilder,
@ -41,13 +42,32 @@ export class CreateTaskComponent implements OnInit {
) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/command-tasks`;
let initialScope = '';
if (this.data?.selectedClients && this.data.selectedClients.length > 0) {
initialScope = 'clients';
} else if (this.data?.scope) {
initialScope = this.data.scope;
} else if (this.data?.runScriptContext) {
initialScope = this.data.runScriptContext.type || '';
}
this.taskForm = this.fb.group({
scope: [ this.data?.scope ? this.data.scope : '', Validators.required],
scope: [initialScope, Validators.required],
name: ['', Validators.required],
organizationalUnit: [ this.data?.organizationalUnit ? this.data.organizationalUnit : null, Validators.required],
organizationalUnit: [ this.data?.organizationalUnit ? this.data.organizationalUnit : null],
notes: [''],
scheduleAfterCreate: [false]
});
if (this.data?.selectedClients && Array.isArray(this.data.selectedClients)) {
this.selectedClients = [...this.data.selectedClients];
this.clients = [...this.data.selectedClients];
}
setTimeout(() => {
this.onScopeChange(initialScope);
}, 0);
}
ngOnInit(): void {
@ -111,10 +131,31 @@ export class CreateTaskComponent implements OnInit {
}
onScopeChange(scope: string): void {
this.filterUnits(scope).subscribe(filteredUnits => {
this.availableOrganizationalUnits = filteredUnits;
if (scope === 'clients') {
if (this.data?.selectedClients && this.data.selectedClients.length > 0) {
this.selectedClients = [...this.data.selectedClients];
this.clients = [...this.data.selectedClients];
} else {
this.toastr.error('No hay clientes pre-seleccionados para este ámbito');
this.taskForm.get('scope')?.setValue('');
return;
}
this.taskForm.get('organizationalUnit')?.setValue('');
});
this.taskForm.get('organizationalUnit')?.clearValidators();
} else {
this.filterUnits(scope).subscribe(filteredUnits => {
this.availableOrganizationalUnits = filteredUnits;
if (!this.data?.organizationalUnit) {
this.taskForm.get('organizationalUnit')?.setValue('');
}
this.taskForm.get('organizationalUnit')?.setValidators(Validators.required);
});
}
this.taskForm.get('organizationalUnit')?.updateValueAndValidity();
}
isClientPreSelected(client: any): boolean {
return this.data?.selectedClients && this.data.selectedClients.some((c: any) => c.uuid === client.uuid);
}
startUnitsFilter(): Promise<void> {
@ -209,14 +250,31 @@ export class CreateTaskComponent implements OnInit {
}
const formData = this.taskForm.value;
const scope = formData.scope;
if (scope === 'clients' && this.selectedClients.length === 0) {
this.toastr.error('Debe seleccionar al menos un cliente');
return;
}
if (scope !== 'clients' && !formData.organizationalUnit) {
this.toastr.error('Debe seleccionar una unidad organizativa');
return;
}
const payload: any = {
name: formData.name,
scope: formData.scope,
organizationalUnit: formData.organizationalUnit,
notes: formData.notes || '',
};
if (scope === 'clients') {
payload.clients = this.selectedClients.map(client => client.uuid);
payload.organizationalUnit = null;
} else {
payload.organizationalUnit = formData.organizationalUnit;
}
if (this.editing) {
const taskId = this.data.task.uuid;
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({

View File

@ -28,7 +28,7 @@ export class ShowTaskScheduleComponent implements OnInit{
columns = [
{ columnDef: 'id', header: 'ID', cell: (schedule: any) => schedule.id },
{ columnDef: 'recurrenceType', header: 'Recurrencia', cell: (schedule: any) => schedule.recurrenceType },
{ columnDef: 'time', header: 'Hora de ejecución', cell: (schedule: any) => this.datePipe.transform(schedule.executionTime, 'HH:mm', 'UTC') },
{ columnDef: 'time', header: 'Hora de ejecución', cell: (schedule: any) => this.datePipe.transform(schedule.executionTime, 'HH:mm') },
{ columnDef: 'daysOfWeek', header: 'Dias de la semana', cell: (schedule: any) => schedule.recurrenceDetails.daysOfWeek },
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => schedule.recurrenceDetails.months },
{ columnDef: 'enabled', header: 'Activo', cell: (schedule: any) => schedule.enabled }

View File

@ -107,18 +107,18 @@ export class ExecuteCommandComponent implements OnInit {
this.arrayCommands = this.arrayCommands.map(command => {
if (allOffOrDisconnected) {
command.disabled = command.slug !== 'power-on';
command.disabled = command.slug !== 'power-on' && command.slug !== 'create-image';
} else if (allSameState) {
if (states[0] === 'off' || states[0] === 'disconnected') {
command.disabled = command.slug !== 'power-on';
command.disabled = !['power-on', 'create-image'].includes(command.slug);
} else {
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'remove-cache-image', 'partition', 'run-script', 'software-inventory'].includes(command.slug);
}
} else {
if (command.slug === 'create-image'|| command.slug === 'software-inventory') {
if (command.slug === 'software-inventory') {
command.disabled = multipleClients;
} else if (
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'remove-cache-image', 'run-script'].includes(command.slug)
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'remove-cache-image', 'run-script', 'create-image'].includes(command.slug)
) {
command.disabled = false;
} else {