commit
8f4e7a7319
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -129,8 +129,9 @@
|
|||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
gap: 12px;
|
||||
padding-right: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Tabla de particiones modernizada */
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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`;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue