Merge pull request 'develop' (#36) from develop into main
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/tag This commit looks good Details

Reviewed-on: #36
pull/37/head^2 0.20.0
Manuel Aranda Rosales 2025-08-25 13:13:31 +02:00
commit 8f4e7a7319
28 changed files with 1382 additions and 226 deletions

View File

@ -1,4 +1,13 @@
# Changelog
## [0.20.0] - 2025-08-25
### Added
- Se ha añadido un nuevo boton en "Trazas" para marcar la misma como completada cuando se requiera.
- Nuevo estado "ocupado" en las trazas para indicar que el cliente envia un "409" y que ya esta ejecutando una accion
### Improved
- Mejorada la interfaz para gestionar las tareas.
---
## [0.19.0] - 2025-08-06
### Added
- Se ha añadido un nuevo estado "enviado" para cuando se ejecuten acciones a equipos en estado Windows o Linux

View File

@ -77,3 +77,64 @@ table {
color: white !important;
}
/* Estilos para los botones de gestión */
.schedule-btn, .script-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
transition: all 0.3s ease;
border: none;
cursor: pointer;
margin: 4px;
line-height: 1;
vertical-align: middle;
}
.schedule-btn {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
color: white;
}
.schedule-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
}
.script-btn {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
}
.script-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
}
.schedule-btn mat-icon, .script-btn mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.schedule-btn span, .script-btn span {
display: inline-block;
vertical-align: middle;
line-height: 1.2;
}
/* Responsive para los botones */
@media (max-width: 768px) {
.schedule-btn, .script-btn {
width: 100%;
justify-content: center;
margin: 4px 0;
}
}

View File

@ -33,8 +33,14 @@
</ng-container>
<ng-container *ngIf="column.columnDef === 'management'">
<button class="action-button" (click)="openShowScheduleDialog(task)"> Programaciones</button>
<button class="action-button" style="margin-left: 0.5vw;" (click)="openShowScriptDialog(task)">Acciones</button>
<button class="action-button schedule-btn" (click)="openShowScheduleDialog(task)">
<mat-icon>schedule</mat-icon>
<span>Programaciones</span>
</button>
<button class="action-button script-btn" (click)="openShowScriptDialog(task)">
<mat-icon>code</mat-icon>
<span>Acciones</span>
</button>
</ng-container>
</td>
</ng-container>
@ -42,12 +48,6 @@
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<td mat-cell *matCellDef="let task" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
<button mat-icon-button color="primary" (click)="manageScheduleAction(task)">
<mat-icon>watch</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="manageScriptAction(task)">
<mat-icon>code-blocks</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="editTask(task)">
<mat-icon>edit</mat-icon>
</button>

View File

@ -3,16 +3,11 @@ import { HttpClient } from '@angular/common/http';
import { MatDialog } from '@angular/material/dialog';
import { ToastrService } from 'ngx-toastr';
import { CreateTaskComponent } from './create-task/create-task.component';
import { DetailTaskComponent } from './detail-task/detail-task.component';
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
import { JoyrideService } from 'ngx-joyride';
import { ConfigService } from '@services/config.service';
import {CreateTaskScheduleComponent} from "./create-task-schedule/create-task-schedule.component";
import {ShowClientsComponent} from "../../ogdhcp/show-clients/show-clients.component";
import {Subnet} from "../../ogdhcp/og-dhcp-subnets.component";
import {ShowTaskScheduleComponent} from "./show-task-schedule/show-task-schedule.component";
import {ShowTaskScriptComponent} from "./show-task-script/show-task-script.component";
import {CreateTaskScriptComponent} from "./create-task-script/create-task-script.component";
import {DatePipe} from "@angular/common";
@Component({
@ -114,28 +109,6 @@ export class CommandsTaskComponent implements OnInit {
});
}
manageScheduleAction(task: any): void {
this.dialog.open(CreateTaskScheduleComponent, {
width: '800px',
data: { task },
}).afterClosed().subscribe( result => {
if (result) {
this.loadTasks();
}
})
}
manageScriptAction(task: any): void {
this.dialog.open(CreateTaskScriptComponent, {
width: '900px',
data: { task },
}).afterClosed().subscribe( result => {
if (result) {
this.loadTasks();
}
})
}
onPageChange(event: any): void {
this.page = event.pageIndex + 1;
this.itemsPerPage = event.pageSize;

View File

@ -1,128 +1,243 @@
.dialog-title {
font-weight: bold;
color: #333;
font-weight: 600;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 16px;
margin: 0;
}
.dialog-content {
padding: 40px;
margin-top: 20px;
margin-bottom: 20px;
}
.task-form {
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
padding: 0;
}
.full-width {
.w-full {
width: 100%;
}
.w-half {
width: 48%;
}
.custom-time {
display: flex;
gap: 15px;
}
.w-half {
width: 50%;
}
mat-form-field {
margin-bottom: 16px;
}
form {
display: flex;
flex-direction: column;
gap: 16px;
margin: auto;
flex-wrap: wrap;
}
mat-form-field {
width: 100%;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
.section-label {
display: block;
font-weight: 600;
color: #333;
margin-bottom: 12px;
font-size: 0.9rem;
}
.weekday-toggle-group {
display: flex;
justify-content: space-between;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.weekday-toggle {
flex: 1;
padding: 8px 0;
border: 1px solid #ccc;
border-radius: 4px;
background: #f5f5f5;
padding: 8px 12px;
border: 2px solid #e0e0e0;
background: white;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.85rem;
font-weight: 500;
transition: background 0.2s ease;
min-width: 60px;
}
.weekday-toggle:hover {
border-color: #2196f3;
background: #f3f8ff;
}
.weekday-toggle.selected {
background: #1976d2;
background: #2196f3;
color: white;
border-color: #1976d2;
}
.month-toggle-group {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 16px;
border-color: #2196f3;
}
.month-toggle-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
justify-content: space-between;
}
.month-toggle {
flex: 1;
padding: 8px;
min-width: 48px;
border: 1px solid #ccc;
border-radius: 6px;
background-color: #f0f0f0;
text-align: center;
padding: 6px 10px;
border: 2px solid #e0e0e0;
background: white;
border-radius: 16px;
cursor: pointer;
transition: background-color 0.2s ease;
transition: all 0.3s ease;
font-size: 0.8rem;
font-weight: 500;
min-width: 50px;
}
.month-toggle:hover {
border-color: #4caf50;
background: #f1f8e9;
}
.month-toggle.selected {
background-color: #4caf50;
background: #4caf50;
color: white;
border-color: #4caf50;
}
.summary-card {
background: #f9fafb;
border-left: 5px solid #3f51b5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-radius: 12px;
padding: 16px;
transition: box-shadow 0.3s ease;
}
.summary-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.summary-card {
background-color: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 12px 16px;
margin-top: 16px;
border-radius: 6px;
.enabled-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.checkbox-icon {
color: #4caf50;
}
/* Resumen mejorado */
.summary-card {
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
border: 1px solid #bbdefb;
border-radius: 12px;
margin-top: 20px;
}
.summary-title {
display: flex;
align-items: center;
gap: 8px;
color: #1976d2;
font-size: 1.1rem;
margin: 0;
}
.summary-title mat-icon {
font-size: 24px;
}
.schedule-summary {
display: flex;
flex-direction: column;
gap: 12px;
}
.summary-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid rgba(25, 118, 210, 0.1);
}
.summary-item:last-child {
border-bottom: none;
}
.summary-icon {
color: #1976d2;
font-size: 20px;
}
.summary-text {
color: #0d47a1;
color: #333;
font-size: 0.95rem;
line-height: 1.4;
}
/* Acciones */
.action-container {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px;
border-top: 1px solid #e0e0e0;
}
.ordinary-button {
background: #f5f5f5;
color: #333;
border: 1px solid #ddd;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.ordinary-button:hover {
background: #e0e0e0;
}
.submit-button {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
border: none;
padding: 10px 24px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.submit-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
}
.submit-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.submit-button mat-icon {
font-size: 18px;
}
/* Responsive */
@media (max-width: 768px) {
.custom-time {
flex-direction: column;
}
.w-half {
width: 100%;
}
.weekday-toggle-group {
justify-content: center;
}
.month-toggle-row {
justify-content: center;
}
.action-container {
flex-direction: column;
}
.submit-button, .ordinary-button {
width: 100%;
justify-content: center;
}
}

View File

@ -1,4 +1,6 @@
<h2 mat-dialog-title class="dialog-title">Programar accion</h2>
<h2 mat-dialog-title class="dialog-title">
{{ data.schedule ? 'Editar' : 'Crear' }} Programación de Tarea
</h2>
<mat-dialog-content class="dialog-content">
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="task-form">
@ -18,12 +20,13 @@
</mat-form-field>
<mat-form-field appearance="fill" class="w-full">
<mat-label>Hora</mat-label>
<mat-label>Hora de ejecución</mat-label>
<input matInput formControlName="executionTime" placeholder="08:00" type="time">
<mat-hint>Hora en formato 24h (ej: 14:30)</mat-hint>
</mat-form-field>
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
<label>Días de la semana:</label>
<label class="section-label">Días de la semana:</label>
<div class="weekday-toggle-group">
<button
*ngFor="let day of weekDays"
@ -37,7 +40,7 @@
</div>
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
<label>Meses:</label>
<label class="section-label">Meses:</label>
<div class="month-toggle-row" *ngFor="let row of monthRows">
<button
*ngFor="let month of row"
@ -52,27 +55,49 @@
<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>
<mat-label>Fecha de inicio</mat-label>
<input matInput [matDatepicker]="fromPicker" formControlName="initDate">
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="fill" class="w-half">
<mat-label>Hasta</mat-label>
<mat-label>Fecha de fin</mat-label>
<input matInput [matDatepicker]="toPicker" formControlName="endDate">
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker>
</mat-form-field>
</div>
<mat-checkbox formControlName="enabled">Activar tarea</mat-checkbox>
<mat-checkbox formControlName="enabled" class="enabled-checkbox">
<mat-icon class="checkbox-icon">power_settings_new</mat-icon>
Activar programación
</mat-checkbox>
<!-- Resumen mejorado de la programación -->
<mat-card *ngIf="summaryText" class="summary-card">
<mat-icon color="primary" style="width: 50px;">info</mat-icon>
<span class="summary-text">
{{ summaryText }}
</span>
<mat-card-header>
<mat-card-title class="summary-title">
<mat-icon color="primary">info</mat-icon>
Resumen de la Programación
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="schedule-summary">
<div class="summary-item">
<mat-icon class="summary-icon">schedule</mat-icon>
<span class="summary-text">{{ summaryText }}</span>
</div>
<div class="summary-item" *ngIf="nextExecutionDate">
<mat-icon class="summary-icon">next_plan</mat-icon>
<span class="summary-text">Próxima ejecución: {{ nextExecutionDate | date:'full' }}</span>
</div>
<div class="summary-item" *ngIf="executionCount">
<mat-icon class="summary-icon">repeat</mat-icon>
<span class="summary-text">Se ejecutará {{ executionCount }} veces</span>
</div>
</div>
</mat-card-content>
</mat-card>
</form>
@ -80,5 +105,8 @@
<mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="onSubmit()" >{{ 'buttonSave' | translate }}</button>
<button class="submit-button" [disabled]="!form.valid" (click)="onSubmit()">
<mat-icon>save</mat-icon>
{{ 'buttonSave' | translate }}
</button>
</mat-dialog-actions>

View File

@ -26,6 +26,8 @@ export class CreateTaskScheduleComponent implements OnInit{
editing: boolean = false;
selectedMonths: { [key: string]: boolean } = {};
selectedDays: { [key: string]: boolean } = {};
nextExecutionDate: Date | null = null;
executionCount: number = 0;
constructor(
private fb: FormBuilder,
@ -176,6 +178,9 @@ export class CreateTaskScheduleComponent implements OnInit{
const days = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
const months = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
// Calcular próxima ejecución y conteo
this.calculateNextExecutionAndCount();
if (recurrence === 'none') {
return `Esta acción se ejecutará una sola vez el ${ this.formatDate(start)} a las ${time}.`;
}
@ -183,6 +188,45 @@ export class CreateTaskScheduleComponent implements OnInit{
return `Esta acción se ejecutará todos los ${days.join(', ')} de ${months.join(', ')}, desde el ${this.formatDate(start)} hasta el ${this.formatDate(end)} a las ${time}.`;
}
private calculateNextExecutionAndCount(): void {
const recurrence = this.form.get('recurrenceType')?.value;
const time = this.form.get('executionTime')?.value;
if (recurrence === 'none') {
const execDate = this.form.get('executionDate')?.value;
if (execDate && time) {
this.nextExecutionDate = new Date(execDate);
const [hours, minutes] = time.split(':');
this.nextExecutionDate.setHours(parseInt(hours), parseInt(minutes), 0, 0);
this.executionCount = 1;
}
} else {
const startDate = this.form.get('recurrenceDetails.initDate')?.value;
const endDate = this.form.get('recurrenceDetails.endDate')?.value;
const days = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
const months = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
if (startDate && endDate && days.length > 0 && months.length > 0) {
// Calcular próxima ejecución (simplificado)
this.nextExecutionDate = new Date(startDate);
const [hours, minutes] = time.split(':');
this.nextExecutionDate.setHours(parseInt(hours), parseInt(minutes), 0, 0);
// Calcular número aproximado de ejecuciones
this.executionCount = this.calculateExecutionCount(startDate, endDate, days.length, months.length);
}
}
}
private calculateExecutionCount(startDate: Date, endDate: Date, daysCount: number, monthsCount: number): number {
const start = new Date(startDate);
const end = new Date(endDate);
const daysDiff = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
// Aproximación: días seleccionados por semana * semanas * meses activos
return Math.ceil((daysCount / 7) * (daysDiff / 7) * (monthsCount / 12));
}
formatDate(date: string | Date): string {
const realDate = (date instanceof Date) ? date : new Date(date);
return new Intl.DateTimeFormat('es-ES', { dateStyle: 'long' }).format(realDate);

View File

@ -275,10 +275,134 @@ table {
padding: 16px;
}
.toggle-options {
/* Estilos para las pestañas principales */
.action-tabs {
margin-top: 20px;
}
.action-tabs ::ng-deep .mat-tab-header {
border-bottom: 2px solid #e0e0e0;
margin-bottom: 20px;
}
.action-tabs ::ng-deep .mat-tab-label {
min-width: 160px;
padding: 12px 24px;
font-weight: 500;
color: #666;
transition: all 0.3s ease;
}
.action-tabs ::ng-deep .mat-tab-label:hover {
color: #1976d2;
background-color: rgba(25, 118, 210, 0.04);
}
.action-tabs ::ng-deep .mat-tab-label.mat-tab-label-active {
color: #1976d2;
font-weight: 600;
}
.action-tabs ::ng-deep .mat-ink-bar {
background-color: #1976d2;
height: 3px;
}
/* Contenido de las pestañas */
.tab-content {
padding: 20px 0;
}
.action-description {
display: flex;
justify-content: start;
margin: 16px 0;
align-items: center;
gap: 12px;
margin-bottom: 24px;
padding: 16px;
background-color: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #1976d2;
}
.action-description mat-icon {
color: #1976d2;
font-size: 24px;
width: 24px;
height: 24px;
}
.action-description span {
color: #495057;
font-size: 14px;
font-weight: 500;
}
/* Selector de tipo de script */
.script-type-selector {
margin-bottom: 24px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.radio-group {
display: flex;
gap: 24px;
}
.radio-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-radius: 6px;
transition: all 0.2s ease;
}
.radio-button:hover {
background-color: rgba(25, 118, 210, 0.04);
}
.radio-button mat-icon {
color: #1976d2;
font-size: 20px;
width: 20px;
height: 20px;
}
/* Contenedor de acciones básicas */
.basic-action-container {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Contenedor de scripts */
.script-options {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Responsive design */
@media (max-width: 768px) {
.action-tabs ::ng-deep .mat-tab-label {
min-width: 120px;
padding: 8px 16px;
font-size: 14px;
}
.radio-group {
flex-direction: column;
gap: 16px;
}
.action-description {
flex-direction: column;
text-align: center;
gap: 8px;
}
}

View File

@ -2,60 +2,113 @@
<mat-dialog-content class="dialog-content">
<div class="task-form">
<div class="toggle-options">
<mat-button-toggle-group [(ngModel)]="commandType" exclusive>
<mat-button-toggle value="new">
<mat-icon>edit</mat-icon> Nuevo Script
</mat-button-toggle>
<mat-button-toggle value="existing">
<mat-icon>storage</mat-icon> Script Guardado
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<div *ngIf="commandType === 'new'" class="new-command-container">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Orden de ejecucion </mat-label>
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Ingrese el script</mat-label>
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
</mat-form-field>
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
</div>
<div *ngIf="commandType === 'existing'">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Seleccione script a ejecutar</mat-label>
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Orden de ejecucion </mat-label>
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
</mat-form-field>
<div class="script-content">
<h3>Script:</h3>
<div class="script-preview" [innerHTML]="scriptContent"></div>
</div>
<div class="script-params" *ngIf="parameterNames.length > 0">
<h3>Ingrese los parámetros:</h3>
<div *ngFor="let paramName of parameterNames">
<!-- Pestañas principales -->
<mat-tab-group [(selectedIndex)]="selectedTabIndex" (selectedIndexChange)="onTabChange($event)" class="action-tabs">
<!-- Pestaña de Acciones Básicas -->
<mat-tab label="Acciones Básicas" class="basic-tab">
<div class="tab-content">
<div class="action-description">
<mat-icon>power_settings_new</mat-icon>
<span>Selecciona una acción básica del sistema</span>
</div>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ paramName }}</mat-label>
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
<mat-label>Tipo de Acción</mat-label>
<mat-select [(ngModel)]="selectedBasicAction" (selectionChange)="onBasicActionChange()">
<mat-option value="poweron">
<mat-icon>power</mat-icon> Encender Equipo
</mat-option>
<mat-option value="shutdown">
<mat-icon>power_off</mat-icon> Apagar Equipo
</mat-option>
<mat-option value="reboot">
<mat-icon>restart_alt</mat-icon> Reiniciar Equipo
</mat-option>
<mat-option value="login">
<mat-icon>login</mat-icon> Iniciar Sesión
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Orden de ejecución</mat-label>
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
</mat-form-field>
</div>
</div>
</div>
</mat-tab>
<!-- Pestaña de Scripts -->
<mat-tab label="Ejecutar Script" class="script-tab">
<div class="tab-content">
<div class="action-description">
<mat-icon>code</mat-icon>
<span>Ejecuta un script personalizado o crea uno nuevo</span>
</div>
<!-- Opciones de script -->
<div class="script-options">
<div class="script-type-selector">
<mat-radio-group [(ngModel)]="commandType" class="radio-group">
<mat-radio-button value="new" class="radio-button">
<mat-icon>edit</mat-icon> Nuevo Script
</mat-radio-button>
<mat-radio-button value="existing" class="radio-button">
<mat-icon>storage</mat-icon> Script Guardado
</mat-radio-button>
</mat-radio-group>
</div>
<!-- Nuevo Script -->
<div *ngIf="commandType === 'new'" class="new-command-container">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Orden de ejecución</mat-label>
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Ingrese el script</mat-label>
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
</mat-form-field>
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
</div>
<!-- Script Existente -->
<div *ngIf="commandType === 'existing'">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Seleccione script a ejecutar</mat-label>
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- Detalles del Script -->
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Orden de ejecución</mat-label>
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
</mat-form-field>
<div class="script-content">
<h3>Script:</h3>
<div class="script-preview" [innerHTML]="scriptContent"></div>
</div>
<div class="script-params" *ngIf="parameterNames.length > 0">
<h3>Ingrese los parámetros:</h3>
<div *ngFor="let paramName of parameterNames">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ paramName }}</mat-label>
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
</mat-form-field>
</div>
</div>
</div>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
</mat-dialog-content>

View File

@ -24,13 +24,15 @@ export class CreateTaskScriptComponent implements OnInit {
scripts: any[] = [];
scriptContent: string = "";
parameters: any = {};
selectedTabIndex: number = 0;
selectedBasicAction: string = '';
commandType: string = 'existing';
selectedScript: any = null;
newScript: string = '';
executionOrder: Number = 0;
selection = new SelectionModel(true, []);
parameterNames: string[] = Object.keys(this.parameters);
commandTask: any;
constructor(
private fb: FormBuilder,
private http: HttpClient,
@ -43,6 +45,8 @@ export class CreateTaskScriptComponent implements OnInit {
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.baseUrl = this.configService.apiUrl;
this.commandTask = this.data.commandTask;
this.loadScripts()
this.form = this.fb.group({
content: [''],
@ -66,6 +70,46 @@ export class CreateTaskScriptComponent implements OnInit {
});
}
onTabChange(index: number): void {
this.selectedTabIndex = index;
if (index === 0) {
this.selectedBasicAction = '';
} else {
this.selectedScript = null;
this.newScript = '';
}
}
onBasicActionChange(): void {
this.parameters = {};
this.parameterNames = Object.keys(this.parameters);
}
getBasicActionType(): string {
const actionMap: { [key: string]: string } = {
'poweron': 'POWER-ON',
'shutdown': 'SHUTDOWN',
'reboot': 'REBOOT',
'login': 'LOGIN',
};
return actionMap[this.selectedBasicAction] || '';
}
validateBasicAction(): boolean {
if (!this.selectedBasicAction) {
this.toastService.error('Debe seleccionar un tipo de acción');
return false;
}
if (this.executionOrder === null || this.executionOrder === undefined) {
this.toastService.error('Debe especificar el orden de ejecución');
return false;
}
return true;
}
saveNewScript() {
if (!this.newScript.trim()) {
this.toastService.error('Debe ingresar un script antes de guardar.');
@ -115,12 +159,60 @@ export class CreateTaskScriptComponent implements OnInit {
this.scriptContent = updatedScript;
}
validateScript(): boolean {
if (this.commandType === 'new') {
if (!this.newScript.trim()) {
this.toastService.error('Debe ingresar un script');
return false;
}
} else if (this.commandType === 'existing') {
if (!this.selectedScript) {
this.toastService.error('Debe seleccionar un script');
return false;
}
}
if (this.executionOrder === null || this.executionOrder === undefined) {
this.toastService.error('Debe especificar el orden de ejecución');
return false;
}
return true;
}
onSubmit() {
let isValid = false;
let content = '';
let type = '';
if (this.selectedTabIndex === 0) {
isValid = this.validateBasicAction();
if (isValid) {
content = this.getBasicActionType();
type = 'basic-action';
}
} else { // Pestaña de scripts
isValid = this.validateScript();
if (isValid) {
content = this.commandType === 'existing' ? this.scriptContent : this.newScript;
type = 'run-script';
}
}
if (!isValid) {
return;
}
if (!this.data) {
this.toastService.error('Error: No se recibieron datos del diálogo');
return;
}
this.http.post(`${this.baseUrl}/command-task-scripts`, {
commandTask: this.data.task['@id'],
content: this.commandType === 'existing' ? this.scriptContent : this.newScript,
commandTask: this.commandTask['@id'],
content: content,
order: this.executionOrder,
type: 'run-script',
type: this.selectedBasicAction,
}).subscribe({
next: () => {
this.toastService.success('Tarea creada con éxito');

View File

@ -269,7 +269,7 @@ export class CreateTaskComponent implements OnInit {
};
if (scope === 'clients') {
payload.clients = this.selectedClients.map(client => client.uuid);
payload.clients = this.selectedClients.map(client => client['@id'] ? client['@id'] : client.uuid);
payload.organizationalUnit = null;
} else {
payload.organizationalUnit = formData.organizationalUnit;

View File

@ -87,3 +87,122 @@ mat-spinner {
display: flex;
gap: 15px;
}
.header-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.action-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
color: white;
border: none;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.action-button:hover {
background: #1565c0;
}
.action-button mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
.create-button {
color: white;
border: none;
padding: 12px 24px;
font-weight: 500;
}
.create-button mat-icon {
margin-right: 8px;
}
.search-container {
display: flex;
gap: 16px;
margin: 20px 0;
flex-wrap: wrap;
}
.search-string {
min-width: 250px;
}
/* Estilos para los chips de días y meses */
.days-display, .months-display {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.day-chip, .month-chip {
background: #e3f2fd;
color: #1976d2;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
border: 1px solid #bbdefb;
}
/* Estilos para la próxima ejecución */
.next-execution {
display: flex;
align-items: center;
gap: 8px;
color: #555;
}
.execution-icon {
font-size: 18px;
color: #2196f3;
}
/* Mejoras en la tabla */
.mat-table {
border-radius: 8px;
overflow: hidden;
}
.mat-header-cell {
background: #f5f5f5;
font-weight: 600;
color: #333;
}
.mat-row:hover {
background: #f8f9fa;
}
/* Responsive */
@media (max-width: 768px) {
.search-container {
flex-direction: column;
}
.search-string {
min-width: auto;
width: 100%;
}
.header-actions {
justify-content: center;
}
.create-button {
width: 100%;
}
}

View File

@ -3,26 +3,33 @@
<h2 mat-dialog-title>Gestionar programaciones de tareas en {{ data.commandTask?.name }}</h2>
<mat-dialog-content>
<div class="header-actions">
<button (click)="createNewSchedule()" class="action-button">
<mat-icon>add</mat-icon>
Nueva Programación
</button>
</div>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
text="Busca subredes por nombre para localizar una subred específica rápidamente.">
<mat-label i18n="@@searchLabel">Buscar nombre del cliente</mat-label>
text="Busca programaciones por nombre para localizar una programación específica rápidamente.">
<mat-label i18n="@@searchLabel">Buscar programación</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder"
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<button *ngIf="filters['name']" mat-icon-button matSuffix aria-label="Clear tree search"
<button *ngIf="filters['name']" mat-icon-button matSuffix aria-label="Clear search"
(click)="filters['name'] = ''; loadData()">
<mat-icon>close</mat-icon>
</button>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchIpStep" text="Busca programaciones por tipo.">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchTypeStep" text="Busca programaciones por tipo de recurrencia.">
<mat-label i18n="@@searchLabel">Buscar por tipo</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['recurrence']" i18n-placeholder="@@searchPlaceholder"
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<button *ngIf="filters['ip']" mat-icon-button matSuffix aria-label="Clear tree search"
(click)="filters['ip'] = ''; loadData()">
<button *ngIf="filters['recurrence']" mat-icon-button matSuffix aria-label="Clear type search"
(click)="filters['recurrence'] = ''; loadData()">
<mat-icon>close</mat-icon>
</button>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
@ -31,7 +38,7 @@
<app-loading [isLoading]="loading"></app-loading>
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
text="Visualiza y administra las subredes listadas según los filtros aplicados.">
text="Visualiza y administra las programaciones de tareas según los filtros aplicados.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let schedule">
@ -47,19 +54,45 @@
<ng-template #scheduledTemplate>
Programado
<div style="font-size: 12px;">
{{ schedule.recurrenceDetails.initDate | date }} → {{ schedule.recurrenceDetails.endDate | date}}
{{ schedule.recurrenceDetails?.initDate | date }} → {{ schedule.recurrenceDetails?.endDate | date}}
</div>
</ng-template>
</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef === 'executionTime'">
{{ schedule.executionTime | date: 'HH:mm' }}
</ng-container>
<ng-container *ngIf="column.columnDef !== 'recurrenceType' && column.columnDef !== 'executionTime' && column.columnDef !== 'enabled'">
<ng-container *ngIf="column.columnDef === 'nextExecution'">
<div class="next-execution">
<mat-icon class="execution-icon">schedule</mat-icon>
<span>{{ column.cell(schedule) || 'No calculada' }}</span>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef === 'daysOfWeek'">
<div class="days-display">
<span *ngFor="let day of schedule.recurrenceDetails?.daysOfWeek"
class="day-chip">{{ day | slice:0:3 }}</span>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef === 'months'">
<div class="months-display">
<span *ngFor="let month of schedule.recurrenceDetails?.months"
class="month-chip">{{ month | slice:0:3 }}</span>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'recurrenceType' && column.columnDef !== 'executionTime' &&
column.columnDef !== 'daysOfWeek' && column.columnDef !== 'months' &&
column.columnDef !== 'nextExecution' && column.columnDef !== 'enabled'">
{{ column.cell(schedule) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'enabled'">
<mat-chip>
<mat-chip [color]="schedule.enabled ? 'accent' : 'warn'" selected>
<ng-container *ngIf="schedule.enabled">
Activo
</ng-container>
@ -74,10 +107,10 @@
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let schedule" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editSchedule(schedule)">
<mat-icon i18n="@@deleteElementTooltip">edit</mat-icon>
<button mat-icon-button color="primary" (click)="editSchedule(schedule)" matTooltip="Editar programación">
<mat-icon i18n="@@editElementTooltip">edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteSchedule(schedule)">
<button mat-icon-button color="warn" (click)="deleteSchedule(schedule)" matTooltip="Eliminar programación">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</td>
@ -88,7 +121,7 @@
</table>
<div class="paginator-container" joyrideStep="paginationStep"
text="Navega entre las páginas de subredes usando el paginador.">
text="Navega entre las páginas de programaciones usando el paginador.">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>

View File

@ -28,13 +28,14 @@ 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') },
{ 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 }
{ columnDef: 'executionTime', header: 'Hora de ejecución', cell: (schedule: any) => this.datePipe.transform(schedule.executionTime, 'HH:mm') },
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (schedule: any) => this.calculateNextExecution(schedule) },
{ columnDef: 'daysOfWeek', header: 'Días de la semana', cell: (schedule: any) => schedule.recurrenceDetails?.daysOfWeek || [] },
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => schedule.recurrenceDetails?.months || [] },
{ columnDef: 'enabled', header: 'Estado', cell: (schedule: any) => schedule.enabled }
];
displayedColumns: string[] = ['id', 'recurrenceType', 'time', 'daysOfWeek', 'months', 'enabled', 'actions'];
displayedColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'daysOfWeek', 'months', 'enabled', 'actions'];
constructor(
private toastService: ToastrService,
@ -63,21 +64,39 @@ export class ShowTaskScheduleComponent implements OnInit{
},
(error) => {
this.loading = false;
this.toastService.error('Error al cargar las programaciones');
}
);
}
createNewSchedule(): void {
this.dialog.open(CreateTaskScheduleComponent, {
width: '800px',
data: { task: this.data.commandTask }
}).afterClosed().subscribe(result => {
if (result) {
this.loadData();
this.toastService.success('Programación creada correctamente');
}
});
}
editSchedule(schedule: any): void {
this.dialog.open(CreateTaskScheduleComponent, {
width: '800px',
data: { schedule: schedule, task: this.data.commandTask }
}).afterClosed().subscribe(() => this.loadData());
}).afterClosed().subscribe(result => {
if (result) {
this.loadData();
this.toastService.success('Programación actualizada correctamente');
}
});
}
deleteSchedule(schedule: any): void {
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: 'tarea programada' }
data: { name: 'programación de tarea' }
});
dialogRef.afterClosed().subscribe(result => {
@ -88,20 +107,52 @@ export class ShowTaskScheduleComponent implements OnInit{
this.loadData();
},
(error) => {
this.toastService.error(error.error['hydra:description']);
this.toastService.error(error.error['hydra:description'] || 'Error al eliminar la programación');
}
);
}
})
}
calculateNextExecution(schedule: any): string {
if (!schedule.enabled) {
return 'Deshabilitada';
}
try {
if (schedule.recurrenceType === 'none') {
if (schedule.executionDate) {
const execDate = new Date(schedule.executionDate);
const now = new Date();
if (execDate < now) {
return 'Ya ejecutada';
}
return this.datePipe.transform(execDate, 'dd/MM/yyyy HH:mm') || 'Fecha inválida';
}
return 'Sin fecha';
}
if (schedule.recurrenceDetails) {
const days = schedule.recurrenceDetails.daysOfWeek?.join(', ') || 'Todos';
const months = schedule.recurrenceDetails.months?.join(', ') || 'Todos';
return `${days} - ${months}`;
}
return 'Configuración incompleta';
} catch (error) {
return 'Error en cálculo';
}
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.loadData();
}
onNoClick(): void {
this.dialogRef.close(false);
}
onPageChange(event: any) {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.loadData()
}
}

View File

@ -87,3 +87,222 @@ mat-spinner {
display: flex;
gap: 15px;
}
/* Estilos para el header de acciones */
.header-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.action-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
.action-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
background: linear-gradient(135deg, #45a049 0%, #3d8b40 100%);
}
.action-button mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
/* Estilos para el contenido del script */
.script-content {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
}
.script-content:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #4caf50;
}
.script-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
font-weight: 500;
}
.script-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
.script-title {
font-size: 14px;
}
.script-body {
padding: 16px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
background: #ffffff;
color: #333;
max-height: 200px;
overflow-y: auto;
}
/* Estilos para el botón de parámetros */
.parameters-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
color: white;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(33, 150, 243, 0.3);
}
.parameters-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
}
.parameters-button mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
.parameters-button span {
font-size: 13px;
}
/* Mejoras en la tabla */
.mat-table {
border-radius: 8px;
overflow: hidden;
}
.mat-header-cell {
background: #f5f5f5;
font-weight: 600;
color: #333;
}
.mat-row:hover {
background: #f8f9fa;
}
/* Responsive */
@media (max-width: 768px) {
.search-container {
flex-direction: column;
}
.search-string {
min-width: auto;
width: 100%;
}
.header-actions {
justify-content: center;
}
.action-button {
width: 100%;
}
.script-body {
max-height: 150px;
}
}
/* Estilos para los chips de tipo */
.type-chip {
font-weight: 500;
padding: 8px 12px;
border-radius: 16px;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.type-script {
background: #e8f5e8;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
.type-command {
background: #fff3e0;
color: #f57c00;
border: 1px solid #ffcc80;
}
.type-python {
background: #e3f2fd;
color: #1976d2;
border: 1px solid #bbdefb;
}
.type-bash {
background: #f3e5f5;
color: #7b1fa2;
border: 1px solid #e1bee7;
}
.type-default {
background: #f5f5f5;
color: #616161;
border: 1px solid #e0e0e0;
}
/* Estilos para el indicador de orden */
.order-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: #f8f9fa;
border-radius: 20px;
border: 1px solid #e9ecef;
}
.order-icon {
font-size: 16px;
width: 16px;
height: 16px;
color: #6c757d;
}
.order-number {
font-weight: 600;
color: #495057;
font-size: 14px;
}

View File

@ -3,6 +3,13 @@
<h2 mat-dialog-title>Gestionar scripts de tareas en {{ data.commandTask?.name }}</h2>
<mat-dialog-content>
<div class="header-actions">
<button (click)="createNewScript()" class="action-button">
<mat-icon>add</mat-icon>
Nueva Acción
</button>
</div>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
text="Busca subredes por nombre para localizar una subred específica rápidamente.">
@ -32,8 +39,9 @@
<ng-template #checkOtherColumn>
<ng-container *ngIf="column.columnDef === 'parameters'; else normalCell">
<button mat-stroked-button color="primary" (click)="openParametersModal(schedule.parameters)">
Ver parámetros
<button class="parameters-button" (click)="openParametersModal(schedule.parameters)">
<mat-icon>settings</mat-icon>
<span>Ver parámetros</span>
</button>
</ng-container>
</ng-template>

View File

@ -5,6 +5,7 @@ import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
import {ConfigService} from "@services/config.service";
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
import {CreateTaskScriptComponent} from "../create-task-script/create-task-script.component";
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
import {ViewParametersModalComponent} from "./view-parameters-modal/view-parameters-modal.component";
@ -96,6 +97,19 @@ export class ShowTaskScriptComponent implements OnInit{
});
}
createNewScript(): void {
const dialogRef = this.dialog.open(CreateTaskScriptComponent, {
width: '800px',
data: { commandTask: this.data.commandTask }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadData();
}
});
}
onPageChange(event: any) {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;

View File

@ -129,8 +129,9 @@
.button-row {
display: flex;
padding-right: 1em;
gap: 12px;
padding-right: 0;
align-items: center;
}
/* Tabla de particiones modernizada */

View File

@ -23,6 +23,13 @@
<div class="button-row">
<button class="action-button" id="execute-button" [disabled]="!selectedPartition || loading" (click)="save()">Ejecutar</button>
</div>
<div class="button-row">
<button class="action-button" color="accent"
[disabled]="!isFormValid()"
(click)="openScheduleModal()">
Opciones de programación
</button>
</div>
</div>
<div class="select-container">

View File

@ -9,6 +9,7 @@ import {MatDialog} from "@angular/material/dialog";
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
import {CreateRepositoryModalComponent} from "./create-repository-modal/create-repository-modal.component";
import {CreateBranchModalComponent} from "../../../../repositories/show-git-images/create-branch-modal/create-branch-modal.component";
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
@Component({
selector: 'app-create-image',
@ -108,7 +109,6 @@ export class CreateClientImageComponent implements OnInit{
}
ngOnInit() {
console.log('CreateImageComponent ngOnInit ejecutado');
this.clientId = this.route.snapshot.paramMap.get('id');
this.loadPartitions();
this.loadImages();
@ -612,8 +612,102 @@ export class CreateClientImageComponent implements OnInit{
this.isDestinationBranchEditable = !this.isDestinationBranchEditable;
if (!this.isDestinationBranchEditable) {
// Opcional: Aquí se pueden agregar validaciones adicionales
console.log('Rama destino guardada:', this.destinationBranch);
}
}
isFormValid(): boolean {
if (!this.selectedPartition) {
return false;
}
if (this.imageType === 'monolithic') {
if (this.monolithicAction === 'create') {
return this.name !== null && this.name.trim().length > 0;
} else if (this.monolithicAction === 'update') {
return this.selectedImage !== null;
}
} else if (this.imageType === 'git') {
if (this.hasValidGitData) {
return this.destinationBranch !== null && this.destinationBranch.trim().length > 0;
} else {
return this.selectedGitRepository !== null;
}
}
return false;
}
openScheduleModal(): void {
let scope = 'clients';
// Verificar que tenemos la información del cliente
if (!this.client) {
this.toastService.error('No se ha cargado la información del cliente');
return;
}
// Crear un array con el objeto cliente completo
let selectedClients = [this.client];
console.log(selectedClients);
const dialogRef = this.dialog.open(CreateTaskComponent, {
width: '800px',
data: {
scope: scope,
selectedClients: selectedClients,
organizationalUnit: this.client['@id'],
source: 'create-image',
runScriptContext: null
}
});
dialogRef.afterClosed().subscribe((result: any) => {
if (result) {
// Verificar que tenemos la partición seleccionada
if (!this.selectedPartition) {
this.toastService.error('Debe seleccionar una partición');
return;
}
let payload: any = {};
if (this.imageType === 'monolithic') {
payload = {
type: 'monolithic',
action: this.monolithicAction,
diskNumber: this.selectedPartition.diskNumber,
partitionNumber: this.selectedPartition.partitionNumber,
imageName: this.monolithicAction === 'create' ? this.name : this.selectedImage?.name
};
} else if (this.imageType === 'git') {
payload = {
type: 'git',
diskNumber: this.selectedPartition.diskNumber,
partitionNumber: this.selectedPartition.partitionNumber,
repository: this.selectedGitRepository?.name || this.gitData?.repo,
sourceBranch: this.gitData?.branch,
destinationBranch: this.destinationBranch
};
}
const taskId = result['taskId'] ? result['taskId']['@id'] : result['@id'];
const executionOrder = result['executionOrder'] || 1;
this.http.post(`${this.baseUrl}/command-task-scripts`, {
commandTask: taskId,
parameters: payload,
order: executionOrder,
type: 'create-image',
}).subscribe({
next: () => {
this.toastService.success('Tarea de creación de imagen programada con éxito');
},
error: (error) => {
this.toastService.error(error.error['hydra:description'] || 'Error al programar la tarea');
}
});
}
});
}
}

View File

@ -341,6 +341,12 @@ table {
font-weight: 500;
}
.chip-busy {
background-color: #ff8c00 !important;
color: white !important;
font-weight: 500;
}
.status-progress-flex {
display: flex;
align-items: center;
@ -366,6 +372,7 @@ table {
.status-indicator.in-progress { background-color: #ffc107; }
.status-indicator.cancelled { background-color: #6c757d; }
.status-indicator.sent { background-color: #b19cd9; }
.status-indicator.busy { background-color: #ff8c00; }
/* Opciones de cliente */
.client-option {

View File

@ -34,13 +34,14 @@
<mat-form-field appearance="fill" class="search-boolean">
<mat-label i18n="@@searchLabel">Estado</mat-label>
<mat-select (selectionChange)="onOptionStatusSelected($event.value)" placeholder="Seleccionar opción"
<mat-select (selectionChange)="onOptionStatusSelected($event.value)" placeholder="Seleccionar opción"
#commandStatusInput>
<mat-option [value]="'failed'">Fallido</mat-option>
<mat-option [value]="'pending'">Pendiente de ejecutar</mat-option>
<mat-option [value]="'in-progress'">Ejecutando</mat-option>
<mat-option [value]="'success'">Completado con éxito</mat-option>
<mat-option [value]="'cancelled'">Cancelado</mat-option>
<mat-option [value]="'busy'">Ocupado</mat-option>
</mat-select>
<button *ngIf="commandStatusInput.value" mat-icon-button matSuffix aria-label="Clear input search"
(click)="clearStatusFilter($event, commandStatusInput)">
@ -97,7 +98,8 @@
'chip-pending': trace.status === 'pending',
'chip-in-progress': trace.status === 'in-progress',
'chip-cancelled': trace.status === 'cancelled',
'chip-sent': trace.status === 'sent'
'chip-sent': trace.status === 'sent',
'chip-busy': trace.status === 'busy'
}">
{{
trace.status === 'failed' ? 'Error' :
@ -106,6 +108,7 @@
trace.status === 'pending' ? 'Pendiente' :
trace.status === 'cancelled' ? 'Cancelado' :
trace.status === 'sent' ? 'Enviado' :
trace.status === 'busy' ? 'Ocupado' :
trace.status
}}
</mat-chip>

View File

@ -235,7 +235,6 @@ export class ClientTaskLogsComponent implements OnInit {
params['executedAt[before]'] = this.datePipe.transform(params['endDate'], 'yyyy-MM-dd');
delete params['endDate'];
}
console.log('🌐 GET', `${this.baseUrl}/traces`, params);
const url = `${this.baseUrl}/traces`;

View File

@ -497,6 +497,12 @@ table {
flex-shrink: 0;
}
/* Ajuste para el botón de éxito en la barra de progreso */
.progress-container .success-button {
margin-left: 5px;
flex-shrink: 0;
}
.paginator-container {
display: flex;
justify-content: end;
@ -540,10 +546,16 @@ table {
font-weight: 500;
}
.chip-busy {
background-color: #ff8c00 !important;
color: white !important;
font-weight: 500;
}
.status-progress-flex {
display: flex;
align-items: center;
gap: 8px;
gap: 4px;
}
.status-option {
@ -564,6 +576,7 @@ table {
.status-indicator.in-progress { background-color: #ffc107; }
.status-indicator.cancelled { background-color: #6c757d; }
.status-indicator.sent { background-color: #b19cd9; }
.status-indicator.busy { background-color: #ff8c00; }
.client-option {
display: flex;
@ -610,6 +623,26 @@ button.cancel-button {
color: #dc3545;
}
.success-button {
display: flex;
align-items: center;
justify-content: center;
color: #28a745;
background-color: transparent;
border: none;
padding: 0px;
transition: all 0.3s ease;
}
.success-button:hover {
background-color: rgba(40, 167, 69, 0.1);
border-radius: 50%;
}
.success-button mat-icon {
color: #28a745;
}
.selected-row {
background-color: rgba(102, 126, 234, 0.1) !important;
}

View File

@ -159,6 +159,12 @@
{{ 'sent' | translate }}
</div>
</mat-option>
<mat-option [value]="'busy'">
<div class="status-option">
<div class="status-indicator busy"></div>
{{ 'busy' | translate }}
</div>
</mat-option>
</mat-select>
<button *ngIf="commandStatusInput.value" mat-icon-button matSuffix aria-label="Clear input search"
(click)="clearStatusFilter($event, commandStatusInput)">
@ -235,6 +241,10 @@
(click)="cancelTrace(trace)" class="cancel-button" matTooltip="{{ 'cancelTask' | translate }}">
<mat-icon>cancel</mat-icon>
</button>
<button mat-icon-button
(click)="markTraceAsSuccess(trace)" class="success-button" matTooltip="Marcar como exitosa">
<mat-icon>flag</mat-icon>
</button>
</div>
</ng-container>
<ng-template #statusChip>
@ -246,7 +256,8 @@
'chip-pending': trace.status === 'pending',
'chip-in-progress': trace.status === 'in-progress',
'chip-cancelled': trace.status === 'cancelled',
'chip-sent': trace.status === 'sent'
'chip-sent': trace.status === 'sent',
'chip-busy': trace.status === 'busy'
}">
{{
trace.status === 'failed' ? ('failed' | translate) :
@ -255,6 +266,7 @@
trace.status === 'pending' ? ('pending' | translate) :
trace.status === 'cancelled' ? ('cancelled' | translate) :
trace.status === 'sent' ? ('sent' | translate) :
trace.status === 'busy' ? ('busy' | translate) :
trace.status
}}
</mat-chip>
@ -262,6 +274,10 @@
(click)="cancelTrace(trace)" class="cancel-button" matTooltip="{{ 'cancelTask' | translate }}">
<mat-icon>cancel</mat-icon>
</button>
<button *ngIf="trace.status === 'in-progress'" mat-icon-button
(click)="markTraceAsSuccess(trace)" class="success-button" matTooltip="Marcar como exitosa">
<mat-icon>flag</mat-icon>
</button>
</div>
</ng-template>
</ng-container>

View File

@ -321,6 +321,41 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
});
}
markTraceAsSuccess(trace: any): void {
if (trace.status !== 'in-progress') {
this.toastService.warning('Solo se pueden marcar como exitosas las trazas en progreso', 'Advertencia');
return;
}
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '400px',
data: {
title: 'Marcar Traza como Exitosa',
message: `¿Estás seguro de que quieres marcar la traza #${trace.id} como exitosa?`,
confirmText: 'Marcar como Exitosa',
cancelText: 'Cancelar'
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loading = true;
this.http.post(`${this.baseUrl}${trace['@id']}/mark-as-success`, {}).subscribe(
() => {
this.toastService.success('Traza marcada como exitosa correctamente', 'Éxito');
this.loadTraces();
this.loadTotalStats();
},
(error) => {
console.error('Error marking trace as success:', error);
this.toastService.error('Error al marcar la traza como exitosa', 'Error');
this.loading = false;
}
);
}
});
}
private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void {
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
if (traceIndex !== -1) {

View File

@ -1,10 +1,11 @@
<h1 mat-dialog-title i18n="@@deleteDialogTitle">Eliminar</h1>
<h1 mat-dialog-title>{{ title }}</h1>
<div mat-dialog-content>
<p i18n="@@deleteConfirmationMessage">
¿Estás seguro que deseas eliminar <strong>{{ data.name }}</strong>?
<p>
{{ message }}
<strong *ngIf="showName">{{ name }}</strong>
</p>
</div>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onNoClick()" i18n="@@cancelButton">Cancelar</button>
<button class="delete-button" (click)="onYesClick()" i18n="@@confirmButton">Eliminar</button>
<button class="ordinary-button" (click)="onNoClick()">{{ cancelText }}</button>
<button class="delete-button" (click)="onYesClick()">{{ confirmText }}</button>
</div>

View File

@ -7,10 +7,27 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
styleUrl: './delete-modal.component.css'
})
export class DeleteModalComponent {
title: string = 'Eliminar';
message: string = '¿Estás seguro que deseas eliminar este elemento?';
confirmText: string = 'Eliminar';
cancelText: string = 'Cancelar';
showName: boolean = false;
name: string = '';
constructor(
public dialogRef: MatDialogRef<DeleteModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: { name: string }
) {}
@Inject(MAT_DIALOG_DATA) public data: any
) {
// Configurar valores por defecto o usar los proporcionados
if (data) {
this.title = data.title || this.title;
this.message = data.message || this.message;
this.confirmText = data.confirmText || this.confirmText;
this.cancelText = data.cancelText || this.cancelText;
this.name = data.name || '';
this.showName = !!data.name;
}
}
onNoClick(): void {
this.dialogRef.close(false);