refs #1906. Updated UX to CommandTask
testing/ogGui-multibranch/pipeline/head There was a failure building this commit
Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit
Details
parent
1df6578efd
commit
bf100943a0
|
@ -144,6 +144,11 @@ import { ShowGitImagesComponent } from './components/repositories/show-git-image
|
|||
import { RenameImageComponent } from './components/repositories/rename-image/rename-image.component';
|
||||
import { ClientDetailsComponent } from './components/groups/shared/client-details/client-details.component';
|
||||
import { PartitionTypeOrganizatorComponent } from './components/groups/shared/partition-type-organizator/partition-type-organizator.component';
|
||||
import { CreateTaskScheduleComponent } from './components/commands/commands-task/create-task-schedule/create-task-schedule.component';
|
||||
import { ShowTaskScheduleComponent } from './components/commands/commands-task/show-task-schedule/show-task-schedule.component';
|
||||
import { ShowTaskScriptComponent } from './components/commands/commands-task/show-task-script/show-task-script.component';
|
||||
import { CreateTaskScriptComponent } from './components/commands/commands-task/create-task-script/create-task-script.component';
|
||||
import { ViewParametersModalComponent } from './components/commands/commands-task/show-task-script/view-parameters-modal/view-parameters-modal.component';
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
|
@ -245,7 +250,12 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
ShowGitImagesComponent,
|
||||
RenameImageComponent,
|
||||
ClientDetailsComponent,
|
||||
PartitionTypeOrganizatorComponent
|
||||
PartitionTypeOrganizatorComponent,
|
||||
CreateTaskScheduleComponent,
|
||||
ShowTaskScheduleComponent,
|
||||
ShowTaskScriptComponent,
|
||||
CreateTaskScriptComponent,
|
||||
ViewParametersModalComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
|
@ -23,36 +25,28 @@
|
|||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="tasks" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container matColumnDef="taskid">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'idColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.id }} </td>
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let task">
|
||||
<ng-container *ngIf="column.columnDef !== 'management'">
|
||||
{{ column.cell(task) }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="notes">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'infoColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.notes }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'createdByColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.createdBy }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="scheduledDate">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'executionDateColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.dateTime | date:'short' }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="enabled">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'statusColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.enabled ? ('enabled' | translate) : ('disabled' | translate) }} </td>
|
||||
<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>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<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="info" (click)="viewTaskDetails(task)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
<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>
|
||||
|
|
|
@ -7,6 +7,13 @@ 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({
|
||||
selector: 'app-commands-task',
|
||||
|
@ -21,7 +28,18 @@ export class CommandsTaskComponent implements OnInit {
|
|||
itemsPerPage: number = 10;
|
||||
page: number = 1;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||
displayedColumns: string[] = ['taskid', 'notes', 'name', 'scheduledDate', 'enabled', 'actions'];
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
|
||||
columns = [
|
||||
{ columnDef: 'id', header: 'ID', cell: (task: any) => task.id },
|
||||
{ columnDef: 'name', header: 'Nombre de tarea', cell: (task: any) => task.name },
|
||||
{ columnDef: 'organizationalUnit', header: 'Ámbito', cell: (task: any) => task.organizationalUnit.name },
|
||||
{ columnDef: 'management', header: 'Gestiones', cell: (task: any) => task.schedules },
|
||||
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (task: any) => this.datePipe.transform(task.nextExecution, 'dd/MM/yyyy HH:mm:ss') },
|
||||
{ columnDef: 'createdBy', header: 'Creado por', cell: (task: any) => task.createdBy },
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['id', 'name', 'organizationalUnit', 'management', 'nextExecution', 'createdBy', 'actions'];
|
||||
loading: boolean = false;
|
||||
private apiUrl: string;
|
||||
|
||||
|
@ -56,24 +74,25 @@ export class CommandsTaskComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
viewTaskDetails(task: any): void {
|
||||
this.dialog.open(DetailTaskComponent, {
|
||||
width: '800px',
|
||||
data: { task },
|
||||
}).afterClosed().subscribe(() => this.loadTasks());
|
||||
}
|
||||
|
||||
openCreateTaskModal(): void {
|
||||
this.dialog.open(CreateTaskComponent, {
|
||||
width: '800px',
|
||||
}).afterClosed().subscribe(() => this.loadTasks());
|
||||
}).afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadTasks();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
editTask(task: any): void {
|
||||
this.dialog.open(CreateTaskComponent, {
|
||||
width: '800px',
|
||||
data: { task },
|
||||
}).afterClosed().subscribe(() => this.loadTasks());
|
||||
}).afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadTasks();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deleteTask(task: any): void {
|
||||
|
@ -95,12 +114,66 @@ 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;
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
openShowScheduleDialog(commandTask: any) {
|
||||
const dialogRef = this.dialog.open(ShowTaskScheduleComponent, {
|
||||
width: '85vw',
|
||||
height: '85vh',
|
||||
maxWidth: '85vw',
|
||||
maxHeight: '85vh',
|
||||
data: { commandTask: commandTask }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openShowScriptDialog(commandTask: any) {
|
||||
const dialogRef = this.dialog.open(ShowTaskScriptComponent, {
|
||||
width: '85vw',
|
||||
height: '85vh',
|
||||
maxWidth: '85vw',
|
||||
maxHeight: '85vh',
|
||||
data: { commandTask: commandTask }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.loadTasks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
.dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.task-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.weekday-toggle-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.weekday-toggle {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #f5f5f5;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.weekday-toggle.selected {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
.month-toggle-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.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;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.month-toggle.selected {
|
||||
background-color: #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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
color: #0d47a1;
|
||||
line-height: 1.4;
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
<h2 mat-dialog-title class="dialog-title">Programar accion</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="task-form">
|
||||
|
||||
<mat-form-field appearance="fill" class="w-full">
|
||||
<mat-label>Repetición</mat-label>
|
||||
<mat-select formControlName="recurrenceType">
|
||||
<mat-option *ngFor="let type of recurrenceTypes" [value]="type">{{ type | titlecase }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="w-full" *ngIf="form.get('recurrenceType')?.value === 'none'">
|
||||
<mat-label>Fecha de ejecución</mat-label>
|
||||
<input matInput [matDatepicker]="oneTimePicker" formControlName="executionDate">
|
||||
<mat-datepicker-toggle matSuffix [for]="oneTimePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #oneTimePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="w-full">
|
||||
<mat-label>Hora</mat-label>
|
||||
<input matInput formControlName="executionTime" placeholder="08:00" type="time">
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Mostrar solo si no es 'none' -->
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
|
||||
<label>Días de la semana:</label>
|
||||
<div class="weekday-toggle-group">
|
||||
<button
|
||||
*ngFor="let day of weekDays"
|
||||
type="button"
|
||||
class="weekday-toggle"
|
||||
[class.selected]="selectedDays[day]"
|
||||
(click)="toggleDay(day)">
|
||||
{{ day.slice(0, 3) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selección de meses -->
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
|
||||
<label>Meses:</label>
|
||||
<div class="month-toggle-row" *ngFor="let row of monthRows">
|
||||
<button
|
||||
*ngFor="let month of row"
|
||||
type="button"
|
||||
class="month-toggle"
|
||||
[class.selected]="selectedMonths[month]"
|
||||
(click)="toggleMonth(month)">
|
||||
{{ month.slice(0, 3) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rango de fechas -->
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails">
|
||||
<mat-form-field appearance="fill" class="w-half">
|
||||
<mat-label>Desde</mat-label>
|
||||
<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>
|
||||
<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-card *ngIf="summaryText" class="summary-card">
|
||||
<mat-icon color="primary" style="width: 50px;">info</mat-icon>
|
||||
<span class="summary-text">
|
||||
{{ summaryText }}
|
||||
</span>
|
||||
</mat-card>
|
||||
</form>
|
||||
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" >{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -0,0 +1,100 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CreateTaskScheduleComponent } from './create-task-schedule.component';
|
||||
import {LoadingComponent} from "../../../../shared/loading/loading.component";
|
||||
import {HttpClientTestingModule} from "@angular/common/http/testing";
|
||||
import {ToastrModule} from "ngx-toastr";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {MatPaginatorModule} from "@angular/material/paginator";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatTreeModule} from "@angular/material/tree";
|
||||
import {TranslateModule, TranslateService} from "@ngx-translate/core";
|
||||
import {JoyrideModule} from "ngx-joyride";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {MatDatepickerModule} from "@angular/material/datepicker";
|
||||
import {MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
import {
|
||||
DateAdapter,
|
||||
MAT_DATE_FORMATS,
|
||||
MAT_NATIVE_DATE_FORMATS,
|
||||
MatNativeDateModule,
|
||||
provideNativeDateAdapter
|
||||
} from "@angular/material/core";
|
||||
import {MatCheckboxModule} from "@angular/material/checkbox";
|
||||
|
||||
describe('CreateTaskScheduleComponent', () => {
|
||||
let component: CreateTaskScheduleComponent;
|
||||
let fixture: ComponentFixture<CreateTaskScheduleComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CreateTaskScheduleComponent, LoadingComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatTabsModule,
|
||||
MatAutocompleteModule,
|
||||
MatListModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatTreeModule,
|
||||
MatDatepickerModule,
|
||||
MatButtonToggleModule,
|
||||
MatNativeDateModule,
|
||||
MatCheckboxModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
|
||||
{ provide: MAT_DATE_FORMATS, useValue: MAT_NATIVE_DATE_FORMATS },
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CreateTaskScheduleComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,196 @@
|
|||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {FormBuilder, FormGroup} from "@angular/forms";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-task-schedule',
|
||||
templateUrl: './create-task-schedule.component.html',
|
||||
styleUrl: './create-task-schedule.component.css'
|
||||
})
|
||||
export class CreateTaskScheduleComponent implements OnInit{
|
||||
form: FormGroup;
|
||||
baseUrl: string;
|
||||
apiUrl: string;
|
||||
recurrenceTypes = ['none', 'custom'];
|
||||
weekDays: string[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
isSingleDateSelected: boolean = true;
|
||||
monthsList: string[] = [
|
||||
'january', 'february', 'march', 'april', 'may', 'june',
|
||||
'july', 'august', 'september', 'october', 'november', 'december'
|
||||
];
|
||||
|
||||
monthRows: string[][] = [];
|
||||
editing: boolean = false;
|
||||
selectedMonths: { [key: string]: boolean } = {};
|
||||
selectedDays: { [key: string]: boolean } = {};
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
public dialogRef: MatDialogRef<CreateTaskScheduleComponent>,
|
||||
private http: HttpClient,
|
||||
private toastr: ToastrService,
|
||||
private configService: ConfigService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/command-task-schedules`;
|
||||
this.form = this.fb.group({
|
||||
executionDate: [new Date()],
|
||||
executionTime: ['08:00'],
|
||||
recurrenceType: ['none'],
|
||||
recurrenceDetails: this.fb.group({
|
||||
daysOfWeek: [[]],
|
||||
months: this.fb.control([]),
|
||||
initDate: [null],
|
||||
endDate: [null]
|
||||
}),
|
||||
enabled: [true]
|
||||
});
|
||||
|
||||
if (this.data.schedule) {
|
||||
this.editing = true;
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
this.form.get('recurrenceType')?.valueChanges.subscribe((value) => {
|
||||
if (value === 'none') {
|
||||
this.form.get('recurrenceDetails')?.disable();
|
||||
} else {
|
||||
this.form.get('recurrenceDetails')?.enable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.monthRows = [
|
||||
this.monthsList.slice(0, 6),
|
||||
this.monthsList.slice(6, 12)
|
||||
];
|
||||
}
|
||||
|
||||
loadData(): void {
|
||||
this.http.get<any>(`${this.baseUrl}${this.data.schedule['@id']}`).subscribe(
|
||||
(data) => {
|
||||
const formattedExecutionTime = this.formatExecutionTime(data.executionTime);
|
||||
|
||||
this.form.patchValue({
|
||||
executionDate: data.executionDate,
|
||||
executionTime: formattedExecutionTime,
|
||||
recurrenceType: data.recurrenceType,
|
||||
recurrenceDetails: {
|
||||
...data.recurrenceDetails,
|
||||
initDate: data.recurrenceDetails.initDate || null,
|
||||
endDate: data.recurrenceDetails.endDate || null,
|
||||
daysOfWeek: data.recurrenceDetails.daysOfWeek || [],
|
||||
months: data.recurrenceDetails.months || []
|
||||
},
|
||||
enabled: data.enabled
|
||||
});
|
||||
this.selectedDays = data.recurrenceDetails.daysOfWeek.reduce((acc: any, day: string) => {
|
||||
acc[day] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
this.selectedMonths = data.recurrenceDetails.months.reduce((acc: any, month: string) => {
|
||||
acc[month] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading schedule data', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
formatExecutionTime(time: string | Date): string {
|
||||
const date = (time instanceof Date) ? time : new Date(time);
|
||||
if (isNaN(date.getTime())) {
|
||||
console.error('Invalid execution time:', time);
|
||||
return '';
|
||||
}
|
||||
return date.toISOString().substring(11, 16);
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
const formData = this.form.value;
|
||||
|
||||
const payload: any = {
|
||||
commandTask: this.data.task['@id'],
|
||||
executionDate: formData.recurrenceType === 'none' ? formData.executionDate : null,
|
||||
executionTime: formData.executionTime,
|
||||
recurrenceType: formData.recurrenceType,
|
||||
recurrenceDetails: {
|
||||
...formData.recurrenceDetails,
|
||||
initDate: formData.recurrenceDetails.initDate || null,
|
||||
endDate: formData.recurrenceDetails.endDate || null,
|
||||
daysOfWeek: formData.recurrenceDetails.daysOfWeek || [],
|
||||
months: formData.recurrenceDetails.months || []
|
||||
},
|
||||
enabled: formData.enabled
|
||||
}
|
||||
|
||||
if (this.editing) {
|
||||
const taskId = this.data.task.uuid;
|
||||
this.http.patch<any>(`${this.baseUrl}${this.data.schedule['@id']}`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastr.success('Programacion de tarea actualizada con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: () => {
|
||||
this.toastr.error('Error al actualizar la tarea');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.http.post<any>(this.apiUrl, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastr.success('Programacion de tarea creada con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: () => {
|
||||
this.toastr.error('Error al crear la tarea');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
get summaryText(): string {
|
||||
const recurrence = this.form.get('recurrenceType')?.value;
|
||||
const start = this.form.get('recurrenceType')?.value === 'none' ? this.form.get('executionDate')?.value : this.form.get('recurrenceDetails.initDate')?.value;
|
||||
const end = this.form.get('recurrenceType')?.value === 'none' ? this.form.get('executionDate')?.value : this.form.get('recurrenceDetails.endDate')?.value;
|
||||
const time = this.form.get('executionTime')?.value;
|
||||
const days = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
|
||||
const months = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
|
||||
|
||||
if (recurrence === 'none') {
|
||||
return `Esta acción se ejecutará una sola vez el ${ this.formatDate(start)} a las ${time}.`;
|
||||
}
|
||||
|
||||
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}.`;
|
||||
}
|
||||
|
||||
formatDate(date: string | Date): string {
|
||||
const realDate = (date instanceof Date) ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat('es-ES', { dateStyle: 'long' }).format(realDate);
|
||||
}
|
||||
|
||||
toggleDay(day: string) {
|
||||
this.selectedDays[day] = !this.selectedDays[day];
|
||||
const days = Object.keys(this.selectedDays).filter(d => this.selectedDays[d]);
|
||||
this.form.get('recurrenceDetails.daysOfWeek')?.setValue(days);
|
||||
}
|
||||
|
||||
toggleMonth(month: string) {
|
||||
this.selectedMonths[month] = !this.selectedMonths[month];
|
||||
const months = Object.keys(this.selectedMonths).filter(m => this.selectedMonths[m]);
|
||||
this.form.get('recurrenceDetails.months')?.setValue(months);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.task-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.deploy-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.script-container {
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
background-color: #eaeff6;
|
||||
border-radius: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.script-content {
|
||||
flex: 2;
|
||||
min-width: 60%;
|
||||
}
|
||||
|
||||
.script-params {
|
||||
flex: 1;
|
||||
min-width: 35%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.script-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.script-content, .script-params {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.select-container {
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1 1 calc(33.33% - 16px);
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.script-preview {
|
||||
background-color: #f4f4f4;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.custom-width {
|
||||
width: 50%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .custom-tooltip {
|
||||
white-space: pre-line !important;
|
||||
max-width: 200px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.selected-client {
|
||||
background-color: #a0c2e5 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.disabled-client {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.new-command-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
background-color: #eaeff6;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.new-command-container mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.new-command-container textarea {
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.new-command-container .action-button {
|
||||
align-self: flex-end;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.script-selector-card {
|
||||
margin: 20px 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.toggle-options {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<h2 mat-dialog-title class="dialog-title">Añadir acción a: {{ data.task?.name }}</h2>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" >{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -0,0 +1,89 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CreateTaskScriptComponent } from './create-task-script.component';
|
||||
import {GroupsComponent} from "../../../groups/groups.component";
|
||||
import {ExecuteCommandComponent} from "../../main-commands/execute-command/execute-command.component";
|
||||
import {LoadingComponent} from "../../../../shared/loading/loading.component";
|
||||
import {HttpClientTestingModule} from "@angular/common/http/testing";
|
||||
import {ToastrModule} from "ngx-toastr";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {MatPaginatorModule} from "@angular/material/paginator";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatTreeModule} from "@angular/material/tree";
|
||||
import {TranslateModule, TranslateService} from "@ngx-translate/core";
|
||||
import {JoyrideModule} from "ngx-joyride";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
|
||||
describe('CreateTaskScriptComponent', () => {
|
||||
let component: CreateTaskScriptComponent;
|
||||
let fixture: ComponentFixture<CreateTaskScriptComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CreateTaskScriptComponent, LoadingComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatTabsModule,
|
||||
MatAutocompleteModule,
|
||||
MatListModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatButtonToggleModule,
|
||||
MatTreeModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CreateTaskScriptComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,137 @@
|
|||
import {Component, EventEmitter, Inject, OnInit, Output} from '@angular/core';
|
||||
import {SelectionModel} from "@angular/cdk/collections";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||
import {
|
||||
SaveScriptComponent
|
||||
} from "../../../groups/components/client-main-view/run-script-assistant/save-script/save-script.component";
|
||||
import {FormBuilder, FormGroup} from "@angular/forms";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-task-script',
|
||||
templateUrl: './create-task-script.component.html',
|
||||
styleUrl: './create-task-script.component.css'
|
||||
})
|
||||
export class CreateTaskScriptComponent implements OnInit {
|
||||
form: FormGroup;
|
||||
baseUrl: string;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
errorMessage = '';
|
||||
loading: boolean = false;
|
||||
scripts: any[] = [];
|
||||
scriptContent: string = "";
|
||||
parameters: any = {};
|
||||
commandType: string = 'existing';
|
||||
selectedScript: any = null;
|
||||
newScript: string = '';
|
||||
executionOrder: Number = 0;
|
||||
selection = new SelectionModel(true, []);
|
||||
parameterNames: string[] = Object.keys(this.parameters);
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<CreateTaskScriptComponent>,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
private router: Router,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.loadScripts()
|
||||
this.form = this.fb.group({
|
||||
content: [''],
|
||||
order: [''],
|
||||
})
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
}
|
||||
|
||||
loadScripts(): void {
|
||||
this.loading = true;
|
||||
|
||||
this.http.get(`${this.baseUrl}/commands?readOnly=false&enabled=true`).subscribe((data: any) => {
|
||||
this.scripts = data['hydra:member'];
|
||||
this.loading = false;
|
||||
}, (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
saveNewScript() {
|
||||
if (!this.newScript.trim()) {
|
||||
this.toastService.error('Debe ingresar un script antes de guardar.');
|
||||
return;
|
||||
}
|
||||
const dialogRef = this.dialog.open(SaveScriptComponent, {
|
||||
width: '400px',
|
||||
data: this.newScript
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success('Script guardado correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onScriptChange() {
|
||||
if (this.selectedScript) {
|
||||
this.scriptContent = this.selectedScript.script;
|
||||
|
||||
const matches = this.scriptContent.match(/@(\w+)/g) || [];
|
||||
const uniqueParams = Array.from(new Set(matches.map(m => m.slice(1))));
|
||||
|
||||
this.parameters = {};
|
||||
uniqueParams.forEach(param => this.parameters[param] = '');
|
||||
|
||||
this.parameterNames = uniqueParams;
|
||||
|
||||
this.updateScript();
|
||||
}
|
||||
}
|
||||
|
||||
onParamChange(name: string, value: string): void {
|
||||
this.parameters[name] = value;
|
||||
this.updateScript();
|
||||
}
|
||||
|
||||
updateScript(): void {
|
||||
let updatedScript = this.selectedScript.script;
|
||||
|
||||
for (const [key, value] of Object.entries(this.parameters)) {
|
||||
const regex = new RegExp(`@${key}\\b`, 'g');
|
||||
updatedScript = updatedScript.replace(regex, value || `@${key}`);
|
||||
}
|
||||
|
||||
this.scriptContent = updatedScript;
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.http.post(`${this.baseUrl}/command-task-scripts`, {
|
||||
commandTask: this.data.task['@id'],
|
||||
content: this.commandType === 'existing' ? this.scriptContent : this.newScript,
|
||||
order: this.executionOrder,
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Tarea creada con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
|
@ -10,19 +10,51 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
background-color: #f9f9f9;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.summary-block {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.date-time-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
|
|
@ -1,106 +1,45 @@
|
|||
<h2 mat-dialog-title class="dialog-title">{{ editing ? ('editTask' | translate) : ('createTask' | translate) }}</h2>
|
||||
|
||||
<form [formGroup]="taskForm" class="task-form">
|
||||
<mat-dialog-content>
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<h3 class="section-title">Información</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<form *ngIf="taskForm && !loading" [formGroup]="taskForm" class="task-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Información</mat-label>
|
||||
<textarea matInput formControlName="notes" placeholder="Ingresa tus notas aquí"></textarea>
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" placeholder="{{ 'nameLabel' | translate }}">
|
||||
<mat-error *ngIf="taskForm.get('name')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<h3 class="section-title">{{ 'informationSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'informationLabel' | translate }}</mat-label>
|
||||
<mat-label>{{ 'notesLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="notes" placeholder="{{ 'notesPlaceholder' | translate }}"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<h3 class="section-title">{{ 'commandSelectionSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectCommandsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="commandGroup" (selectionChange)="onCommandGroupChange()">
|
||||
<mat-option *ngFor="let group of availableCommandGroups" [value]="group.uuid">
|
||||
{{ group.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="taskForm.get('commandGroup')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectIndividualCommandsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="extraCommands" multiple>
|
||||
<mat-option *ngFor="let command of availableIndividualCommands" [value]="command.uuid">
|
||||
{{ command.name }}
|
||||
</mat-option>
|
||||
<mat-label>Ámbito</mat-label>
|
||||
<mat-select formControlName="scope" class="full-width" (selectionChange)="onScopeChange($event.value)">
|
||||
<mat-option [value]="'organizational-unit'">Unidad Organizativa</mat-option>
|
||||
<mat-option [value]="'classrooms-group'">Grupo de aulas</mat-option>
|
||||
<mat-option [value]="'classroom'">Aulas</mat-option>
|
||||
<mat-option [value]="'clients-group'">Grupos de clientes</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<h3 class="section-title">{{ 'executionDateTimeSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'executionDateLabel' | translate }}</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="date" placeholder="{{ 'selectDatePlaceholder' | translate }}">
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
<mat-error *ngIf="taskForm.get('date')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'executionTimeLabel' | translate }}</mat-label>
|
||||
<input matInput type="time" formControlName="time" placeholder="{{ 'selectTimePlaceholder' | translate }}">
|
||||
<mat-error *ngIf="taskForm.get('time')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<h3 class="section-title">{{ 'destinationSelectionSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectOrganizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" >
|
||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="taskForm.get('organizationalUnit')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectClassroomLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedChild" (selectionChange)="onChildChange()">
|
||||
<mat-option *ngFor="let child of selectedUnitChildren" [value]="child['@id']">
|
||||
{{ child.name }}
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectClientsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple>
|
||||
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
|
||||
{{ 'selectAllClients' | translate }}
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
|
||||
{{ client.name }} ({{ client.ip }})
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-checkbox *ngIf="!editing" formControlName="scheduleAfterCreate">¿Quieres programar la tarea al finalizar su creación?</mat-checkbox>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Clientes</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple>
|
||||
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
|
||||
Seleccionar todos
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
|
||||
{{ client.name }} ({{ client.ip }})
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
</form>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="submit-button" (click)="saveTask()">{{ 'buttonSave' | translate }}</button>
|
||||
</div>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" [disabled]="!taskForm.valid" (click)="saveTask()">{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {of} from "rxjs";
|
||||
import {startWith, switchMap} from "rxjs/operators";
|
||||
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-task',
|
||||
|
@ -19,170 +22,143 @@ export class CreateTaskComponent implements OnInit {
|
|||
apiUrl: string;
|
||||
editing: boolean = false;
|
||||
availableOrganizationalUnits: any[] = [];
|
||||
selectedUnitChildren: any[] = [];
|
||||
selectedClients: any[] = [];
|
||||
selectedClientIds: Set<string> = new Set();
|
||||
clients: any[] = [];
|
||||
allOrganizationalUnits: any[] = [];
|
||||
loading: boolean = false;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService,
|
||||
private toastr: ToastrService,
|
||||
private dialog: MatDialog,
|
||||
public dialogRef: MatDialogRef<CreateTaskComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/command-tasks`;
|
||||
this.taskForm = this.fb.group({
|
||||
commandGroup: ['', Validators.required],
|
||||
extraCommands: [[]],
|
||||
date: ['', Validators.required],
|
||||
time: ['', Validators.required],
|
||||
scope: [ this.data?.scope ? this.data.scope : '', Validators.required],
|
||||
name: ['', Validators.required],
|
||||
organizationalUnit: [ this.data?.organizationalUnit ? this.data.organizationalUnit : null, Validators.required],
|
||||
notes: [''],
|
||||
organizationalUnit: ['', Validators.required],
|
||||
selectedChild: [''],
|
||||
selectedClients: [[]]
|
||||
scheduleAfterCreate: [false]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadCommandGroups();
|
||||
this.loadIndividualCommands();
|
||||
this.loadOrganizationalUnits();
|
||||
if (this.data && this.data.task) {
|
||||
this.editing = true;
|
||||
this.loadTaskData(this.data.task);
|
||||
}
|
||||
this.loading = true;
|
||||
const observables = [
|
||||
this.loadCommandGroups(),
|
||||
this.loadIndividualCommands(),
|
||||
this.loadOrganizationalUnits(),
|
||||
this.startUnitsFilter(),
|
||||
];
|
||||
|
||||
Promise.all(observables).then(() => {
|
||||
|
||||
if (this.data.task) {
|
||||
this.editing = true;
|
||||
this.loadData().then(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
} else {
|
||||
this.loading = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
}
|
||||
|
||||
loadCommandGroups(): void {
|
||||
loadData(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.http.get<any>(`${this.baseUrl}${this.data.task['@id']}`).subscribe(
|
||||
(data) => {
|
||||
this.taskForm.patchValue({
|
||||
name: data.name,
|
||||
scope: data.scope,
|
||||
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null,
|
||||
notes: data.notes,
|
||||
});
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los datos de la tarea');
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
onScopeChange(scope: string): void {
|
||||
this.filterUnits(scope).subscribe(filteredUnits => {
|
||||
this.availableOrganizationalUnits = filteredUnits;
|
||||
this.taskForm.get('organizationalUnit')?.setValue('');
|
||||
});
|
||||
}
|
||||
|
||||
startUnitsFilter(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.taskForm.get('scope')?.valueChanges.pipe(
|
||||
startWith(this.taskForm.get('scope')?.value),
|
||||
switchMap((value) => this.filterUnits(value))
|
||||
).subscribe(filteredUnits => {
|
||||
this.availableOrganizationalUnits = filteredUnits;
|
||||
resolve();
|
||||
}, error => {
|
||||
this.toastr.error('Error al filtrar las unidades organizacionales');
|
||||
reject(error);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
filterUnits(value: string) {
|
||||
const filtered = this.allOrganizationalUnits.filter(unit => unit.type === value);
|
||||
return of(filtered);
|
||||
}
|
||||
|
||||
loadCommandGroups(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.http.get<any>(`${this.baseUrl}/command-groups`).subscribe(
|
||||
(data) => {
|
||||
this.availableCommandGroups = data['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los grupos de comandos');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadIndividualCommands(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/commands`).subscribe(
|
||||
(data) => {
|
||||
this.availableIndividualCommands = data['hydra:member'];
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los comandos individuales');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadOrganizationalUnits(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`).subscribe(
|
||||
(data) => {
|
||||
this.availableOrganizationalUnits = data['hydra:member'].filter((unit: any) => unit['type'] === 'organizational-unit');
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar las unidades organizacionales');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadTaskData(task: any): void {
|
||||
this.taskForm.patchValue({
|
||||
commandGroup: task.commandGroup ? task.commandGroup['@id'] : '',
|
||||
extraCommands: task.commands ? task.commands.map((cmd: any) => cmd['@id']) : [],
|
||||
date: task.dateTime ? task.dateTime.split('T')[0] : '',
|
||||
time: task.dateTime ? task.dateTime.split('T')[1].slice(0, 5) : '',
|
||||
notes: task.notes || '',
|
||||
organizationalUnit: task.organizationalUnit ? task.organizationalUnit['@id'] : ''
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
if (task.commandGroup) {
|
||||
this.selectedGroupCommands = task.commandGroup.commands;
|
||||
}
|
||||
}
|
||||
|
||||
private collectClassrooms(unit: any): any[] {
|
||||
let classrooms = [];
|
||||
if (unit.type === 'classroom') {
|
||||
classrooms.push(unit);
|
||||
}
|
||||
if (unit.children && unit.children.length > 0) {
|
||||
for (let child of unit.children) {
|
||||
classrooms = classrooms.concat(this.collectClassrooms(child));
|
||||
}
|
||||
}
|
||||
return classrooms;
|
||||
loadIndividualCommands(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.http.get<any>(`${this.baseUrl}/commands`).subscribe(
|
||||
(data) => {
|
||||
this.availableIndividualCommands = data['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los comandos individuales');
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onOrganizationalUnitChange(): void {
|
||||
const selectedUnitId = this.taskForm.get('organizationalUnit')?.value;
|
||||
const selectedUnit = this.availableOrganizationalUnits.find(unit => unit['@id'] === selectedUnitId);
|
||||
|
||||
if (selectedUnit) {
|
||||
this.selectedUnitChildren = this.collectClassrooms(selectedUnit);
|
||||
} else {
|
||||
this.selectedUnitChildren = [];
|
||||
}
|
||||
|
||||
this.taskForm.patchValue({ selectedChild: '', selectedClients: [] });
|
||||
this.selectedClients = [];
|
||||
this.selectedClientIds.clear();
|
||||
}
|
||||
|
||||
onChildChange(): void {
|
||||
const selectedChildId = this.taskForm.get('selectedChild')?.value;
|
||||
|
||||
if (!selectedChildId) {
|
||||
this.selectedClients = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}${selectedChildId}`.replace(/([^:]\/)\/+/g, '$1');
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
(data) => {
|
||||
if (Array.isArray(data.clients) && data.clients.length > 0) {
|
||||
this.selectedClients = data.clients;
|
||||
} else {
|
||||
this.selectedClients = [];
|
||||
this.toastr.warning('El aula seleccionada no tiene clientes.');
|
||||
loadOrganizationalUnits(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=100`).subscribe(
|
||||
(data) => {
|
||||
this.allOrganizationalUnits = data['hydra:member'];
|
||||
this.availableOrganizationalUnits = [...this.allOrganizationalUnits];
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar las unidades organizacionales');
|
||||
reject(error);
|
||||
}
|
||||
|
||||
this.taskForm.patchValue({ selectedClients: [] });
|
||||
this.selectedClientIds.clear();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los detalles del aula seleccionada');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
const allSelected = this.areAllSelected();
|
||||
if (allSelected) {
|
||||
this.selectedClientIds.clear();
|
||||
} else {
|
||||
this.selectedClients.forEach(client => this.selectedClientIds.add(client.uuid));
|
||||
}
|
||||
this.taskForm.get('selectedClients')!.setValue(Array.from(this.selectedClientIds));
|
||||
}
|
||||
|
||||
areAllSelected(): boolean {
|
||||
return this.selectedClients.length > 0 && this.selectedClients.every(client => this.selectedClientIds.has(client.uuid));
|
||||
}
|
||||
|
||||
onCommandGroupChange(): void {
|
||||
const selectedGroupId = this.taskForm.get('commandGroup')?.value;
|
||||
this.http.get<any>(`${this.baseUrl}/command-groups/${selectedGroupId}`).subscribe(
|
||||
(data) => {
|
||||
this.selectedGroupCommands = data.commands;
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los comandos del grupo seleccionado');
|
||||
}
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
saveTask(): void {
|
||||
|
@ -192,22 +168,14 @@ export class CreateTaskComponent implements OnInit {
|
|||
}
|
||||
|
||||
const formData = this.taskForm.value;
|
||||
const dateTime = this.combineDateAndTime(formData.date, formData.time);
|
||||
const selectedCommands = formData.extraCommands && formData.extraCommands.length > 0
|
||||
? formData.extraCommands.map((id: any) => `/commands/${id}`)
|
||||
: null;
|
||||
|
||||
const payload: any = {
|
||||
commandGroups: formData.commandGroup ? [`/command-groups/${formData.commandGroup}`] : null,
|
||||
dateTime: dateTime,
|
||||
name: formData.name,
|
||||
scope: formData.scope,
|
||||
organizationalUnit: formData.organizationalUnit,
|
||||
notes: formData.notes || '',
|
||||
clients: Array.from(this.selectedClientIds).map((uuid: string) => `/clients/${uuid}`),
|
||||
};
|
||||
|
||||
if (selectedCommands) {
|
||||
payload.commands = selectedCommands;
|
||||
}
|
||||
|
||||
if (this.editing) {
|
||||
const taskId = this.data.task.uuid;
|
||||
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({
|
||||
|
@ -221,9 +189,21 @@ export class CreateTaskComponent implements OnInit {
|
|||
});
|
||||
} else {
|
||||
this.http.post<any>(this.apiUrl, payload).subscribe({
|
||||
next: () => {
|
||||
next: response => {
|
||||
this.toastr.success('Tarea creada con éxito');
|
||||
this.dialogRef.close(true);
|
||||
this.dialogRef.close(response);
|
||||
if (formData.scheduleAfterCreate) {
|
||||
const dialogRef = this.dialog.open(CreateTaskScheduleComponent, {
|
||||
width: '800px',
|
||||
data: { task: response }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastr.success('Tarea programada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.toastr.error('Error al crear la tarea');
|
||||
|
@ -232,14 +212,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
combineDateAndTime(date: string, time: string): string {
|
||||
const dateObj = new Date(date);
|
||||
const [hours, minutes] = time.split(':').map(Number);
|
||||
dateObj.setHours(hours, minutes, 0);
|
||||
return dateObj.toISOString();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.spacing-container {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
flex-grow: 1;
|
||||
margin-right: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right-icon {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.lists-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.subnets-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<h2 mat-dialog-title>Gestionar programaciones de tareas en {{ data.commandTask?.name }}</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<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>
|
||||
<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"
|
||||
(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-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()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<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.">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let schedule">
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'recurrenceType'">
|
||||
<mat-chip style="padding: 10px; margin: 5px;">
|
||||
<ng-container *ngIf="column.cell(schedule) === 'none'; else scheduledTemplate">
|
||||
No programado
|
||||
<div style="font-size: 12px;">
|
||||
{{ schedule.executionDate | date }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #scheduledTemplate>
|
||||
Programado
|
||||
<div style="font-size: 12px;">
|
||||
{{ 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'">
|
||||
{{ column.cell(schedule) }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'enabled'">
|
||||
<mat-chip>
|
||||
<ng-container *ngIf="schedule.enabled">
|
||||
Activo
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!schedule.enabled">
|
||||
Inactivo
|
||||
</ng-container>
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<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>
|
||||
<button mat-icon-button color="warn" (click)="deleteSchedule(schedule)">
|
||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep"
|
||||
text="Navega entre las páginas de subredes usando el paginador.">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">Cerrar</button>
|
||||
</mat-dialog-actions>
|
|
@ -0,0 +1,86 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ShowTaskScheduleComponent } from './show-task-schedule.component';
|
||||
import {LoadingComponent} from "../../../../shared/loading/loading.component";
|
||||
import {HttpClientTestingModule} from "@angular/common/http/testing";
|
||||
import {ToastrModule} from "ngx-toastr";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {MatPaginatorModule} from "@angular/material/paginator";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatTreeModule} from "@angular/material/tree";
|
||||
import {TranslateModule} from "@ngx-translate/core";
|
||||
import {JoyrideModule} from "ngx-joyride";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
|
||||
|
||||
describe('ShowTaskScheduleComponent', () => {
|
||||
let component: ShowTaskScheduleComponent;
|
||||
let fixture: ComponentFixture<ShowTaskScheduleComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ShowTaskScheduleComponent, LoadingComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatTabsModule,
|
||||
MatAutocompleteModule,
|
||||
MatListModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatTreeModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ShowTaskScheduleComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {Client} from "../../../groups/model/model";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
|
||||
import {DatePipe} from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-task-schedule',
|
||||
templateUrl: './show-task-schedule.component.html',
|
||||
styleUrl: './show-task-schedule.component.css'
|
||||
})
|
||||
export class ShowTaskScheduleComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
dataSource = new MatTableDataSource<any>([]);
|
||||
length = 0;
|
||||
itemsPerPage: number = 10;
|
||||
pageSizeOptions: number[] = [5, 10, 20];
|
||||
page = 0;
|
||||
loading: boolean = false;
|
||||
filters: { [key: string]: string } = {};
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
|
||||
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 }
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['id', 'recurrenceType', 'time', 'daysOfWeek', 'months', 'enabled', 'actions'];
|
||||
|
||||
constructor(
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<ShowTaskScheduleComponent>,
|
||||
public dialog: MatDialog,
|
||||
private configService: ConfigService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.baseUrl}/command-task-schedules?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&commandTask.id=${this.data.commandTask?.id}`, { params: this.filters }).subscribe(
|
||||
(data) => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
editSchedule(schedule: any): void {
|
||||
this.dialog.open(CreateTaskScheduleComponent, {
|
||||
width: '800px',
|
||||
data: { schedule: schedule, task: this.data.commandTask }
|
||||
}).afterClosed().subscribe(() => this.loadData());
|
||||
}
|
||||
|
||||
deleteSchedule(schedule: any): void {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: 'tarea programada' }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.http.delete(`${this.baseUrl}${schedule['@id']}`).subscribe(
|
||||
() => {
|
||||
this.toastService.success('Programación eliminada correctamente');
|
||||
this.loadData();
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
onPageChange(event: any) {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.loadData()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.spacing-container {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
flex-grow: 1;
|
||||
margin-right: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right-icon {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.lists-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.subnets-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<h2 mat-dialog-title>Gestionar scripts de tareas en {{ data.commandTask?.name }}</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<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 contenido de script</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"
|
||||
(click)="filters['name'] = ''; loadData()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<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.">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let schedule">
|
||||
<ng-container *ngIf="column.columnDef === 'content'; else checkOtherColumn">
|
||||
<div style="background-color: #f5f5f5; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-family: monospace; white-space: pre-wrap; font-size: 13px;">
|
||||
{{ column.cell(schedule) }}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<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>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #normalCell>
|
||||
{{ column.cell(schedule) }}
|
||||
</ng-template>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<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="warn" (click)="deleteTaskScript(schedule)">
|
||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep"
|
||||
text="Navega entre las páginas de subredes usando el paginador.">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">Cerrar</button>
|
||||
</mat-dialog-actions>
|
|
@ -0,0 +1,86 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ShowTaskScriptComponent } from './show-task-script.component';
|
||||
import {LoadingComponent} from "../../../../shared/loading/loading.component";
|
||||
import {HttpClientTestingModule} from "@angular/common/http/testing";
|
||||
import {ToastrModule} from "ngx-toastr";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {MatPaginatorModule} from "@angular/material/paginator";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatTreeModule} from "@angular/material/tree";
|
||||
import {TranslateModule} from "@ngx-translate/core";
|
||||
import {JoyrideModule} from "ngx-joyride";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {ShowTaskScheduleComponent} from "../show-task-schedule/show-task-schedule.component";
|
||||
|
||||
describe('ShowTaskScriptComponent', () => {
|
||||
let component: ShowTaskScriptComponent;
|
||||
let fixture: ComponentFixture<ShowTaskScriptComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ShowTaskScriptComponent, LoadingComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatTabsModule,
|
||||
MatAutocompleteModule,
|
||||
MatListModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatTreeModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: ActivatedRoute, useValue: { queryParams: { subscribe: () => {} } } },
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ShowTaskScriptComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,104 @@
|
|||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
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 {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {ViewParametersModalComponent} from "./view-parameters-modal/view-parameters-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-task-script',
|
||||
templateUrl: './show-task-script.component.html',
|
||||
styleUrl: './show-task-script.component.css'
|
||||
})
|
||||
export class ShowTaskScriptComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
dataSource = new MatTableDataSource<any>([]);
|
||||
length = 0;
|
||||
itemsPerPage: number = 10;
|
||||
pageSizeOptions: number[] = [5, 10, 20];
|
||||
page = 0;
|
||||
loading: boolean = false;
|
||||
filters: { [key: string]: string } = {};
|
||||
|
||||
columns = [
|
||||
{ columnDef: 'id', header: 'ID', cell: (client: any) => client.id },
|
||||
{ columnDef: 'order', header: 'Orden', cell: (client: any) => client.order },
|
||||
{ columnDef: 'content', header: 'Script', cell: (client: any) => client.content },
|
||||
{ columnDef: 'type', header: 'Type', cell: (client: any) => client.type },
|
||||
{ columnDef: 'parameters', header: 'Parameters', cell: (client: any) => client.parameters },
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['id', 'order', 'type', 'parameters', 'content', 'actions'];
|
||||
|
||||
constructor(
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<ShowTaskScriptComponent>,
|
||||
public dialog: MatDialog,
|
||||
private configService: ConfigService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.baseUrl}/command-task-scripts?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&commandTask.id=${this.data.commandTask?.id}`, { params: this.filters }).subscribe(
|
||||
(data) => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteTaskScript(schedule: any): void {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: 'script de una tarea' }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.http.delete(`${this.baseUrl}${schedule['@id']}`).subscribe(
|
||||
() => {
|
||||
this.toastService.success('Eliminado correctamente');
|
||||
this.loadData();
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
openParametersModal(parameters: any): void {
|
||||
this.dialog.open(ViewParametersModalComponent, {
|
||||
width: '900px',
|
||||
data: parameters
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: any) {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.loadData()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<h2 mat-dialog-title>Parámetros</h2>
|
||||
<mat-dialog-content>
|
||||
<pre>{{ data | json }}</pre>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="close()">Cerrar</button>
|
||||
</mat-dialog-actions>
|
|
@ -0,0 +1,69 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ViewParametersModalComponent } from './view-parameters-modal.component';
|
||||
import {LoadingComponent} from "../../../../../shared/loading/loading.component";
|
||||
import {HttpClientTestingModule, provideHttpClientTesting} from "@angular/common/http/testing";
|
||||
import {ToastrModule, ToastrService} from "ngx-toastr";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {MatDividerModule} from "@angular/material/divider";
|
||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||
import {MatInputModule} from "@angular/material/input";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatTableModule} from "@angular/material/table";
|
||||
import {MatPaginatorModule} from "@angular/material/paginator";
|
||||
import {MatTooltipModule} from "@angular/material/tooltip";
|
||||
import {FormBuilder, FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
|
||||
import {MatSelectModule} from "@angular/material/select";
|
||||
import {MatTabsModule} from "@angular/material/tabs";
|
||||
import {MatAutocompleteModule} from "@angular/material/autocomplete";
|
||||
import {MatListModule} from "@angular/material/list";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatMenuModule} from "@angular/material/menu";
|
||||
import {MatTreeModule} from "@angular/material/tree";
|
||||
import {TranslateModule} from "@ngx-translate/core";
|
||||
import {JoyrideModule} from "ngx-joyride";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {InputDialogComponent} from "../../task-logs/input-dialog/input-dialog.component";
|
||||
import {provideHttpClient} from "@angular/common/http";
|
||||
|
||||
describe('ViewParametersModalComponent', () => {
|
||||
let component: ViewParametersModalComponent;
|
||||
let fixture: ComponentFixture<ViewParametersModalComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ViewParametersModalComponent],
|
||||
imports: [
|
||||
MatDialogModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
}
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ViewParametersModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import {Component, Inject} from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
|
||||
@Component({
|
||||
selector: 'app-view-parameters-modal',
|
||||
templateUrl: './view-parameters-modal.component.html',
|
||||
styleUrl: './view-parameters-modal.component.css'
|
||||
})
|
||||
export class ViewParametersModalComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ViewParametersModalComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
|
@ -189,9 +189,10 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}));
|
||||
|
||||
this.router.navigate(['/clients/partition-assistant'], {
|
||||
queryParams: { clientData: JSON.stringify(clientDataToSend) }
|
||||
}).then(r => {
|
||||
console.log('Navigated to partition assistant with data:', this.clientData);
|
||||
queryParams: {
|
||||
clientData: JSON.stringify(clientDataToSend),
|
||||
runScriptContext: JSON.stringify(this.runScriptContext)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -212,9 +213,10 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}));
|
||||
|
||||
this.router.navigate(['/clients/deploy-image'], {
|
||||
queryParams: { clientData: JSON.stringify(clientDataToSend) }
|
||||
}).then(r => {
|
||||
console.log('Navigated to deploy image with data:', this.clientData);
|
||||
queryParams: {
|
||||
clientData: JSON.stringify(clientDataToSend),
|
||||
runScriptContext: JSON.stringify(this.runScriptContext)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -229,7 +231,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}));
|
||||
|
||||
this.router.navigate(['/clients/run-script'], {
|
||||
queryParams: {
|
||||
queryParams: {
|
||||
clientData: JSON.stringify(clientDataToSend) ,
|
||||
runScriptContext: JSON.stringify(this.runScriptContext)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,358 @@
|
|||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.table-header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.client-container {
|
||||
flex-grow: 1;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0rem 1rem 0rem 0.5rem;
|
||||
}
|
||||
|
||||
.client-icon {
|
||||
flex-shrink: 0;
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 120px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.row-container {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.charts-wrapper {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.disk-usage {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.circular-chart {
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chart {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-pc {
|
||||
font-size: 25px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.client-title h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.client-title p {
|
||||
margin: 2px 0;
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.client-info {
|
||||
margin: 20px 0;
|
||||
border-radius: 12px;
|
||||
background-color: #f5f7fa;
|
||||
padding: 20px;
|
||||
border: 2px solid #d1d9e6;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.info-section {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.two-column-table {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.column.property {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.column.value {
|
||||
text-align: right;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.mat-tab-group {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.mat-tab-body-wrapper {
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
.info-section h2 {
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: 10px;
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.info-section p {
|
||||
font-size: 1rem;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.second-section {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.client-button-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.buttons-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.buttons-row button {
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.circular-chart {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.circle-bg {
|
||||
fill: none;
|
||||
stroke: #f0f0f0;
|
||||
stroke-width: 3.8;
|
||||
}
|
||||
|
||||
.circle {
|
||||
fill: none;
|
||||
stroke-width: 3.8;
|
||||
stroke: #00bfa5;
|
||||
stroke-linecap: round;
|
||||
animation: progress 1s ease-out forwards;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
fill: #333;
|
||||
font-size: 0.7rem;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.disk-usage h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 1.2rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
0% {
|
||||
stroke-dasharray: 0, 100;
|
||||
}
|
||||
}
|
||||
|
||||
.assistants-container {
|
||||
background-color: #fff;
|
||||
margin-top: 10px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.circular-chart {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.circle-bg {
|
||||
fill: none;
|
||||
stroke: #f0f0f0;
|
||||
stroke-width: 3.8;
|
||||
}
|
||||
|
||||
.circle {
|
||||
fill: none;
|
||||
stroke-width: 3.8;
|
||||
stroke-linecap: round;
|
||||
animation: progress 1s ease-out forwards;
|
||||
}
|
||||
|
||||
.partition-0 {
|
||||
stroke: #00bfa5;
|
||||
}
|
||||
|
||||
.partition-1 {
|
||||
stroke: #ff6f61;
|
||||
}
|
||||
|
||||
.partition-2 {
|
||||
stroke: #ffb400;
|
||||
}
|
||||
|
||||
.partition-3 {
|
||||
stroke: #3498db;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
fill: #333;
|
||||
font-size: 0.7rem;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.disk-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
background-color: #f5f7fa;
|
||||
border: 2px solid #d1d9e6;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
table.mat-elevation-z8 {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mat-header-cell {
|
||||
background-color: #d1d9e6 !important;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mat-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mat-chip {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.disk-usage {
|
||||
background-color: white;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
justify-self: center;
|
||||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-left: 0.5em;
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
padding: 8px 18px 8px 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: transform 0.3s ease;
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.back-button:hover:not(:disabled) {
|
||||
background-color: #485ac0d7;
|
||||
}
|
||||
|
||||
.back-button:disabled {
|
||||
background-color: #ced0df;
|
||||
cursor: not-allowed;
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div class="client-container">
|
||||
<div class="header-container">
|
||||
<h2 class="title">{{ 'clientDetailsTitle' | translate }} {{ clientData.name }}</h2>
|
||||
<div class="client-button-row">
|
||||
<button class="back-button" (click)="navigateToGroups()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
{{ 'back' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div *ngIf="!loading" class="client-info">
|
||||
<div class="info-section">
|
||||
<div class="two-column-table">
|
||||
<div class="table-row" *ngFor="let clientData of generalData">
|
||||
<div class="column property">{{ clientData?.property }}</div>
|
||||
<div class="column value">{{ clientData?.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two-column-table">
|
||||
<div class="table-row" *ngFor="let clientData of networkData">
|
||||
<div class="column property">{{ clientData?.property }}</div>
|
||||
<div class="column value">{{ clientData?.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
|
||||
<mat-chip> {{ clientData.firmwareType }}</mat-chip>
|
||||
</div>
|
||||
|
||||
<div class="disk-container">
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef !== 'size'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<mat-chip color="primary">
|
||||
{{ (image.size / 1024).toFixed(2) }} GB
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="charts-container">
|
||||
<ng-container *ngIf="diskUsageData && diskUsageData.length > 0">
|
||||
<div *ngFor="let disk of chartDisk" class="disk-usage">
|
||||
<ngx-charts-pie-chart class="chart" [view]="view" [results]="disk.chartData" [doughnut]="true">
|
||||
</ngx-charts-pie-chart>
|
||||
|
||||
<h3>Disco {{ disk.diskNumber }}</h3>
|
||||
<p>Usado: {{ (disk.used).toFixed(2) }} GB ({{ disk.percentage }}%)</p>
|
||||
<p>Total: {{ disk.total }} GB</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -5,11 +5,22 @@
|
|||
<h2>
|
||||
{{ 'deployImage' | translate }}
|
||||
</h2>
|
||||
<h4>
|
||||
{{ runScriptTitle }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="action-button" [disabled]="!allSelected || !selectedModelClient || !selectedImage || !selectedMethod || !selectedPartition" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button mat-stroked-button color="accent" [disabled]="!allSelected || !selectedModelClient || !selectedImage || !selectedMethod || !selectedPartition" (click)="openScheduleModal()">
|
||||
<mat-icon>schedule</mat-icon> Opciones de programación
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import {Component, EventEmitter, OnInit, Output} from '@angular/core';
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
|
||||
@Component({
|
||||
selector: 'app-deploy-image',
|
||||
templateUrl: './deploy-image.component.html',
|
||||
styleUrl: './deploy-image.component.css'
|
||||
})
|
||||
export class DeployImageComponent {
|
||||
export class DeployImageComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
|
||||
|
@ -34,6 +36,7 @@ export class DeployImageComponent {
|
|||
clientData: any = [];
|
||||
loading: boolean = false;
|
||||
allSelected = true;
|
||||
runScriptContext: any = null;
|
||||
|
||||
protected p2pModeOptions = [
|
||||
{ name: 'Leecher', value: 'leecher' },
|
||||
|
@ -95,13 +98,17 @@ export class DeployImageComponent {
|
|||
private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute
|
||||
private route: ActivatedRoute,
|
||||
private dialog: MatDialog,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.route.queryParams.subscribe(params => {
|
||||
if (params['clientData']) {
|
||||
this.clientData = JSON.parse(params['clientData']);
|
||||
}
|
||||
if (params['runScriptContext']) {
|
||||
this.runScriptContext = params['runScriptContext'];
|
||||
}
|
||||
});
|
||||
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
||||
this.clientData.forEach((client: { selected: boolean; status: string}) => {
|
||||
|
@ -122,6 +129,27 @@ export class DeployImageComponent {
|
|||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.runScriptContext = params['runScriptContext'] ? JSON.parse(params['runScriptContext']) : null;
|
||||
});
|
||||
}
|
||||
|
||||
get runScriptTitle(): string {
|
||||
const ctx = this.runScriptContext;
|
||||
if (!ctx) {
|
||||
return '';
|
||||
}
|
||||
if (Array.isArray(ctx)) {
|
||||
return ctx.map(c => c.name).join(', ');
|
||||
}
|
||||
if (typeof ctx === 'object' && 'name' in ctx) {
|
||||
return ctx.name;
|
||||
}
|
||||
return String(ctx);
|
||||
}
|
||||
|
||||
|
||||
isMethod(method: string): boolean {
|
||||
return this.selectedMethod === method;
|
||||
}
|
||||
|
@ -291,4 +319,47 @@ export class DeployImageComponent {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
openScheduleModal(): void {
|
||||
const dialogRef = this.dialog.open(CreateTaskComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
scope: this.runScriptContext.type,
|
||||
organizationalUnit: this.runScriptContext['@id']
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: { [x: string]: any; }) => {
|
||||
if (result) {
|
||||
const payload = {
|
||||
method: this.selectedMethod,
|
||||
diskNumber: this.selectedPartition.diskNumber,
|
||||
partitionNumber: this.selectedPartition.partitionNumber,
|
||||
p2pMode: this.selectedMethod === 'torrent' ? this.p2pMode : null,
|
||||
p2pTime: this.selectedMethod === 'torrent' ? this.p2pTime : null,
|
||||
mcastIp: this.selectedMethod === 'multicast' ? this.mcastIp : null,
|
||||
mcastPort: this.selectedMethod === 'multicast' ? this.mcastPort : null,
|
||||
mcastMode: this.selectedMethod === 'multicast' ? this.mcastMode : null,
|
||||
mcastSpeed: this.selectedMethod === 'multicast' ? this.mcastSpeed : null,
|
||||
maxTime: this.selectedMethod === 'multicast' ? this.mcastMaxTime : null,
|
||||
maxClients: this.selectedMethod === 'multicast' ? this.mcastMaxClients : null,
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/command-task-scripts`, {
|
||||
commandTask: result['@id'],
|
||||
parameters: payload,
|
||||
order: 1,
|
||||
type: 'deploy-image',
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Script añadido con éxito a la tarea');
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,16 +50,6 @@
|
|||
padding-top: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button.mat-button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
|
@ -82,6 +72,7 @@ button.remove-btn {
|
|||
background-color: #dc3545;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
button.remove-btn:hover {
|
||||
|
@ -186,6 +177,7 @@ button.remove-btn:hover {
|
|||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
|
|
|
@ -2,12 +2,21 @@
|
|||
|
||||
<div class="header-container">
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
||||
Asistente de particionado
|
||||
<h2>
|
||||
{{ 'partitionTitle' | translate }}
|
||||
</h2>
|
||||
<h4>
|
||||
{{ runScriptTitle }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="subnets-button-row">
|
||||
<button class="action-button" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected" (click)="save()">Ejecutar</button>
|
||||
<div class="button-row">
|
||||
<button class="action-button" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected || !selectedDisk" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button mat-stroked-button color="accent" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected || !selectedDisk" (click)="openScheduleModal()">
|
||||
<mat-icon>schedule</mat-icon> Opciones de programación
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -80,7 +89,7 @@
|
|||
<div class="row-button">
|
||||
<button class="action-button" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
|
||||
<mat-chip *ngIf="selectedModelClient.firmwareType">
|
||||
Tabla de particiones: {{ selectedModelClient.firmwareType }}
|
||||
Tabla de firmware: {{ selectedModelClient.firmwareType }}
|
||||
</mat-chip>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ import {ActivatedRoute, Router} from "@angular/router";
|
|||
import { PARTITION_TYPES } from '../../../../../shared/constants/partition-types';
|
||||
import { FILESYSTEM_TYPES } from '../../../../../shared/constants/filesystem-types';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
|
||||
interface Partition {
|
||||
uuid?: string;
|
||||
|
@ -25,7 +27,7 @@ interface Partition {
|
|||
templateUrl: './partition-assistant.component.html',
|
||||
styleUrls: ['./partition-assistant.component.css']
|
||||
})
|
||||
export class PartitionAssistantComponent {
|
||||
export class PartitionAssistantComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
|
@ -41,6 +43,7 @@ export class PartitionAssistantComponent {
|
|||
disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[]; chartData: any[]; used: number; percentage: number }[] = [];
|
||||
clientData: any = [];
|
||||
loading: boolean = false;
|
||||
runScriptContext: any = null;
|
||||
|
||||
view: [number, number] = [400, 300];
|
||||
showLegend = true;
|
||||
|
@ -55,6 +58,7 @@ export class PartitionAssistantComponent {
|
|||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
private dialog: MatDialog,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = this.baseUrl + '/partitions';
|
||||
|
@ -62,8 +66,11 @@ export class PartitionAssistantComponent {
|
|||
if (params['clientData']) {
|
||||
this.clientData = JSON.parse(params['clientData']);
|
||||
}
|
||||
if (params['runScriptContext']) {
|
||||
this.runScriptContext = params['runScriptContext'];
|
||||
}
|
||||
});
|
||||
this.clientId = this.clientData?.[0]['@id'];
|
||||
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
||||
this.clientData.forEach((client: { selected: boolean; status: string}) => {
|
||||
if (client.status === 'og-live') {
|
||||
client.selected = true;
|
||||
|
@ -83,6 +90,12 @@ export class PartitionAssistantComponent {
|
|||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.runScriptContext = params['runScriptContext'] ? JSON.parse(params['runScriptContext']) : null;
|
||||
});
|
||||
}
|
||||
|
||||
get selectedDisk():any {
|
||||
return this.disks.find(disk => disk.diskNumber === this.selectedDiskNumber) || null;
|
||||
}
|
||||
|
@ -103,6 +116,20 @@ export class PartitionAssistantComponent {
|
|||
);
|
||||
}
|
||||
|
||||
get runScriptTitle(): string {
|
||||
const ctx = this.runScriptContext;
|
||||
if (!ctx) {
|
||||
return '';
|
||||
}
|
||||
if (Array.isArray(ctx)) {
|
||||
return ctx.map(c => c.name).join(', ');
|
||||
}
|
||||
if (typeof ctx === 'object' && 'name' in ctx) {
|
||||
return ctx.name;
|
||||
}
|
||||
return String(ctx);
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
this.allSelected = !this.allSelected;
|
||||
this.clientData.forEach((client: { selected: boolean; status: string }) => {
|
||||
|
@ -380,4 +407,57 @@ export class PartitionAssistantComponent {
|
|||
disk.used = this.calculateUsedSpace(disk.partitions);
|
||||
disk.percentage = (disk.used / disk.totalDiskSize) * 100;
|
||||
}
|
||||
|
||||
openScheduleModal(): void {
|
||||
const dialogRef = this.dialog.open(CreateTaskComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
scope: this.runScriptContext.type,
|
||||
organizationalUnit: this.runScriptContext['@id']
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
const modifiedPartitions = this.selectedDisk.partitions.filter((partition: { removed: any; format: any; }) => !partition.removed || partition.format);
|
||||
|
||||
if (modifiedPartitions.length === 0) {
|
||||
this.loading = false;
|
||||
this.toastService.info('No hay cambios para guardar en el disco seleccionado.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newPartitions = modifiedPartitions.map((partition: { partitionNumber: any; memoryUsage: any; size: any; partitionCode: any; filesystem: any; uuid: any; removed: any; format: any; }) => ({
|
||||
diskNumber: this.selectedDisk.diskNumber,
|
||||
partitionNumber: partition.partitionNumber,
|
||||
memoryUsage: partition.memoryUsage,
|
||||
size: partition.size,
|
||||
partitionCode: partition.partitionCode,
|
||||
filesystem: partition.filesystem,
|
||||
uuid: partition.uuid,
|
||||
removed: partition.removed || false,
|
||||
format: partition.format || false,
|
||||
}));
|
||||
|
||||
const bulkPayload = {
|
||||
partitions: newPartitions,
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/command-task-scripts`, {
|
||||
commandTask: result['@id'],
|
||||
parameters: bulkPayload.partitions,
|
||||
order: 1,
|
||||
type: 'partition-assistant',
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Script añadido con éxito a la tarea');
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -261,4 +261,15 @@ table {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.script-selector-card {
|
||||
margin: 20px 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.toggle-options {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -3,12 +3,21 @@
|
|||
<div class="header-container">
|
||||
<div class="header-container-title">
|
||||
<h2>
|
||||
{{ 'runScript' | translate }} {{runScriptTitle}}
|
||||
{{ 'runScript' | translate }}
|
||||
</h2>
|
||||
<h4>
|
||||
{{ runScriptTitle }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript)" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button mat-stroked-button color="accent" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript)" (click)="openScheduleModal()">
|
||||
<mat-icon>schedule</mat-icon> Opciones de programación
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
|
@ -55,12 +64,18 @@
|
|||
|
||||
<mat-divider style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
<div class="command-toggle">
|
||||
<mat-radio-group [(ngModel)]="commandType">
|
||||
<mat-radio-button value="new">Comando nuevo</mat-radio-button>
|
||||
<mat-radio-button value="existing">Comando existente</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<mat-card class="script-selector-card">
|
||||
<mat-card-title>Seleccione el tipo de comando</mat-card-title>
|
||||
|
||||
<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">
|
||||
|
@ -68,10 +83,10 @@
|
|||
<mat-label>Ingrese el script</mat-label>
|
||||
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
|
||||
</mat-form-field>
|
||||
<button class="action-button" (click)="saveNewScript()">Guardar Comando</button>
|
||||
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="commandType === 'existing'" class="select-container">
|
||||
<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()">
|
||||
|
@ -82,22 +97,20 @@
|
|||
|
||||
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
|
||||
<div class="script-content">
|
||||
<h3> Script:</h3>
|
||||
<h3>Script:</h3>
|
||||
<div class="script-preview" [innerHTML]="scriptContent"></div>
|
||||
</div>
|
||||
|
||||
<div class="script-params" *ngIf="parameterNames.length > 0">
|
||||
<h3>Ingrese los valores de los parámetros detectados:</h3>
|
||||
<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="Ingrese el valor">
|
||||
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
|||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {MatCardModule} from "@angular/material/card";
|
||||
import {MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http);
|
||||
|
@ -59,6 +61,8 @@ describe('RunScriptAssistantComponent', () => {
|
|||
MatSelectModule,
|
||||
BrowserAnimationsModule,
|
||||
MatIconModule,
|
||||
MatCardModule,
|
||||
MatButtonToggleModule,
|
||||
ToastrModule.forRoot(),
|
||||
HttpClientTestingModule,
|
||||
TranslateModule.forRoot({
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import {Component, EventEmitter, OnInit, Output} from '@angular/core';
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
|
@ -6,13 +6,14 @@ import { ConfigService } from "@services/config.service";
|
|||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { SaveScriptComponent } from "./save-script/save-script.component";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-run-script-assistant',
|
||||
templateUrl: './run-script-assistant.component.html',
|
||||
styleUrl: './run-script-assistant.component.css'
|
||||
})
|
||||
export class RunScriptAssistantComponent {
|
||||
export class RunScriptAssistantComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
|
||||
|
@ -52,24 +53,25 @@ export class RunScriptAssistantComponent {
|
|||
}
|
||||
});
|
||||
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
||||
this.clientData.forEach((client: { selected: boolean; status: string }) => {
|
||||
this.clientData.forEach((client: { selected: boolean; status: string}) => {
|
||||
if (client.status === 'og-live') {
|
||||
client.selected = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.selectedClients = this.clientData.filter(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
);
|
||||
|
||||
this.loadScripts()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.runScriptContext = params['runScriptContext'] ? JSON.parse(params['runScriptContext']) : null;
|
||||
this.clientData = params['clientData'] ? JSON.parse(params['clientData']) : [];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
get runScriptTitle(): string {
|
||||
const ctx = this.runScriptContext;
|
||||
if (!ctx) {
|
||||
|
@ -179,11 +181,6 @@ export class RunScriptAssistantComponent {
|
|||
this.scriptContent = updatedScript;
|
||||
}
|
||||
|
||||
trackByIndex(index: number): number {
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
save(): void {
|
||||
this.loading = true;
|
||||
|
||||
|
@ -203,4 +200,32 @@ export class RunScriptAssistantComponent {
|
|||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
openScheduleModal(): void {
|
||||
const dialogRef = this.dialog.open(CreateTaskComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
scope: this.runScriptContext.type,
|
||||
organizationalUnit: this.runScriptContext['@id']
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.http.post(`${this.baseUrl}/command-task-scripts`, {
|
||||
commandTask: result['@id'],
|
||||
content: this.commandType === 'existing' ? this.scriptContent : this.newScript,
|
||||
order: 1,
|
||||
type: 'run-script',
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Script añadido con éxito a la tarea');
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.container {
|
||||
width: 100vw;
|
||||
height: calc(100vh - 7vh);
|
||||
min-width: 375px;
|
||||
}
|
||||
|
@ -7,4 +6,4 @@
|
|||
.sidebar {
|
||||
width: 15vw;
|
||||
min-width: 250px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,14 @@
|
|||
"addUserlabelUsername": "Username",
|
||||
"addUserlabelPassword": "Password",
|
||||
"labelRole": "Role",
|
||||
"partitionTitle": "Partitions assistant",
|
||||
"labelOrganizationalUnit": "Organizational Unit",
|
||||
"buttonCancel": "Cancel",
|
||||
"buttonAdd": "Add",
|
||||
"firmwareType": "Firmware",
|
||||
"addRule": "Add rule",
|
||||
"remotePcStatusUnavailable": "Remote PC status unavailable",
|
||||
"remotePcStatusAvailable": "Remote PC status available",
|
||||
"rulesHeader": "Rules",
|
||||
"statusUnavailable": "Unavailable",
|
||||
"statusAvailable": "Available",
|
||||
|
@ -58,7 +61,7 @@
|
|||
"tableStepText": "Here are the existing calendars with their characteristics and settings.",
|
||||
"actionsStepText": "Access the available actions for each calendar here.",
|
||||
"editCalendar": "Edit calendar",
|
||||
"remoteAvailability": "Remote availability?",
|
||||
"remoteAvailability": "Check availability to RemotePC",
|
||||
"selectWeekDays": "Select the days of the week",
|
||||
"startTime": "Start time",
|
||||
"startTimePlaceholder": "Select start time",
|
||||
|
@ -245,8 +248,9 @@
|
|||
"selectCommandGroupLabel": "Select Command Group",
|
||||
"noClientsMessage": "No clients available",
|
||||
"editClientDialogTitle": "Edit Client",
|
||||
"organizationalUnitLabel": "Parent",
|
||||
"organizationalUnitLabel": "Organizational Unit",
|
||||
"ogLiveLabel": "OgLive",
|
||||
"notesLabel": "Notes",
|
||||
"serialNumberLabel": "Serial Number",
|
||||
"netifaceLabel": "Network interface",
|
||||
"netDriverLabel": "Network driver",
|
||||
|
|
|
@ -38,7 +38,9 @@
|
|||
"addRule": "Añadir regla",
|
||||
"rulesHeader": "Reglas",
|
||||
"statusUnavailable": "No disponible",
|
||||
"remotePcStatusUnavailable": "No disponible para Remote PC",
|
||||
"statusAvailable": "Disponible",
|
||||
"remotePcStatusAvailable": "Disponible para Remote PC",
|
||||
"parameters": "Parámetros",
|
||||
"labelRoleName": "Nombre",
|
||||
"runScript": "Ejecutar comando",
|
||||
|
@ -59,7 +61,7 @@
|
|||
"tableStepText": "Aquí se muestran los calendarios existentes con sus características y configuraciones.",
|
||||
"actionsStepText": "Accede a las acciones disponibles para cada calendario aquí.",
|
||||
"editCalendar": "Editar calendario",
|
||||
"remoteAvailability": "¿Disponibilidad remota?",
|
||||
"remoteAvailability": "Marcar como disponible para Remote PC",
|
||||
"selectWeekDays": "Selecciona los días de la semana",
|
||||
"startTime": "Hora de inicio",
|
||||
"startTimePlaceholder": "Selecciona la hora de inicio",
|
||||
|
@ -244,8 +246,9 @@
|
|||
"selectCommandGroupLabel": "Seleccione Grupo de Comandos",
|
||||
"noClientsMessage": "No hay clientes disponibles",
|
||||
"editClientDialogTitle": "Editar Cliente",
|
||||
"organizationalUnitLabel": "Padre",
|
||||
"organizationalUnitLabel": "Unidad organizativa",
|
||||
"ogLiveLabel": "OgLive",
|
||||
"notesLabel": "Notas",
|
||||
"imageNameLabel": "Nombre de la imagen",
|
||||
"importImageButton": "Importar imagen",
|
||||
"convertImageButton": "Convertir imagen virtual",
|
||||
|
@ -281,6 +284,7 @@
|
|||
"excludeParentChanges": "Excluir cambios de las unidades organizativas superiores",
|
||||
"networkPropertiesTab": "Propiedades de red",
|
||||
"disksPartitionsTitle": "Discos/Particiones",
|
||||
"partitionTitle": "Asistente de particionado",
|
||||
"diskTitle": "Disco",
|
||||
"diskUsedLabel": "Usado",
|
||||
"diskTotalLabel": "Total",
|
||||
|
|
Loading…
Reference in New Issue