develop #22
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -1,4 +1,23 @@
|
|||
# Changelog
|
||||
## [0.12.0] - 2025-5-13
|
||||
### Added
|
||||
- Se ha añadido un nuevo modal del detalle de las acciones ejecutadas por cada cliente.
|
||||
- Se ha añadido un modulo para la gestion de las tareas y acciones programadas.
|
||||
- Se han añadido nuevos campos en el listado general de clientes.
|
||||
|
||||
### Improved
|
||||
- Se ha cambiado la pagina de detalles de un cliente, por un modal.
|
||||
- Se han actualizado gran parte de las ayudas contextuales de las distintas parrillas de datos.
|
||||
- Se ha mejorado y corregido los errores del particionador.
|
||||
- Mejoras en la pantalla de trazas.
|
||||
- Cambios en la estetica general de la aplicacion
|
||||
- Añadida la primera version de la la integracion con ogGit
|
||||
- Se ha mejorado la responsividad de la aplicacion, para pantallas pequeñas.
|
||||
|
||||
### Fixed
|
||||
- Se ha corregido un error que hacia que no apareciesen los calendarios en la pantalla de editar OU.
|
||||
- En la pantalla de hacer deploy, al seleccionar imagen ahora deja desmarcarla.
|
||||
|
||||
## [0.11.2] - 2025-4-16
|
||||
### Fixed
|
||||
- Se ha corregido un error en la actualizacion del estado de los pcs en la vista tarjetas.
|
||||
|
|
|
@ -17,9 +17,7 @@ import { CalendarComponent } from "./components/calendar/calendar.component";
|
|||
import { CommandsComponent } from './components/commands/main-commands/commands.component';
|
||||
import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component';
|
||||
import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component';
|
||||
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
|
||||
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||
import { ImagesComponent } from './components/images/images.component';
|
||||
import { TaskLogsComponent } from './components/task-logs/task-logs.component';
|
||||
import {SoftwareComponent} from "./components/software/software.component";
|
||||
import {SoftwareProfileComponent} from "./components/software-profile/software-profile.component";
|
||||
import {OperativeSystemComponent} from "./components/operative-system/operative-system.component";
|
||||
|
@ -67,7 +65,6 @@ const routes: Routes = [
|
|||
{ path: 'clients/deploy-image', component: DeployImageComponent },
|
||||
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent },
|
||||
{ path: 'clients/run-script', component: RunScriptAssistantComponent },
|
||||
{ path: 'clients/:id', component: ClientMainViewComponent },
|
||||
{ path: 'clients/:id/create-image', component: CreateClientImageComponent },
|
||||
{ path: 'repositories', component: RepositoriesComponent },
|
||||
{ path: 'repository/:id', component: MainRepositoryViewComponent },
|
||||
|
|
|
@ -67,7 +67,7 @@ import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.co
|
|||
import { CreatePXEImageComponent } from './components/ogboot/pxe-images/create-image/create-image/create-image.component';
|
||||
import { InfoImageComponent } from './components/ogboot/pxe-images/info-image/info-image/info-image.component';
|
||||
import { PxeComponent } from './components/ogboot/pxe/pxe.component';
|
||||
import { CreatePxeTemplateComponent } from './components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component';
|
||||
import { CreatePxeTemplateComponent } from './components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component';
|
||||
import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component';
|
||||
import { MatExpansionPanel, MatExpansionPanelDescription, MatExpansionPanelTitle } from "@angular/material/expansion";
|
||||
import { OgbootStatusComponent } from './components/ogboot/ogboot-status/ogboot-status.component';
|
||||
|
@ -87,9 +87,8 @@ import { CreateCommandGroupComponent } from './components/commands/commands-grou
|
|||
import { DetailCommandGroupComponent } from './components/commands/commands-groups/detail-command-group/detail-command-group.component';
|
||||
import { CreateTaskComponent } from './components/commands/commands-task/create-task/create-task.component';
|
||||
import { DetailTaskComponent } from './components/commands/commands-task/detail-task/detail-task.component';
|
||||
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
|
||||
import { TaskLogsComponent } from './components/task-logs/task-logs.component';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||
import { ImagesComponent } from './components/images/images.component';
|
||||
import { CreateImageComponent } from './components/images/create-image/create-image.component';
|
||||
import { CreateClientImageComponent } from './components/groups/components/client-main-view/create-image/create-image.component';
|
||||
|
@ -118,7 +117,7 @@ import { CreateMultipleClientComponent } from './components/groups/shared/client
|
|||
import { ExportImageComponent } from './components/images/export-image/export-image.component';
|
||||
import { ImportImageComponent } from "./components/repositories/import-image/import-image.component";
|
||||
import { LoadingComponent } from './shared/loading/loading.component';
|
||||
import { InputDialogComponent } from './components/commands/commands-task/task-logs/input-dialog/input-dialog.component';
|
||||
import { InputDialogComponent } from './components/task-logs/input-dialog/input-dialog.component';
|
||||
import { ManageOrganizationalUnitComponent } from './components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
|
||||
import { BackupImageComponent } from './components/repositories/backup-image/backup-image.component';
|
||||
import { ServerInfoDialogComponent } from "./components/ogdhcp/server-info-dialog/server-info-dialog.component";
|
||||
|
@ -130,7 +129,7 @@ import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clie
|
|||
import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component';
|
||||
import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component';
|
||||
import { ConvertImageComponent } from './components/repositories/convert-image/convert-image.component';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import {NgOptimizedImage, registerLocaleData} from '@angular/common';
|
||||
import localeEs from '@angular/common/locales/es';
|
||||
import { GlobalStatusComponent } from './components/global-status/global-status.component';
|
||||
import { ShowMonoliticImagesComponent } from './components/repositories/show-monolitic-images/show-monolitic-images.component';
|
||||
|
@ -143,6 +142,15 @@ import {
|
|||
import { EditImageComponent } from './components/repositories/edit-image/edit-image.component';
|
||||
import { ShowGitImagesComponent } from './components/repositories/show-git-images/show-git-images.component';
|
||||
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';
|
||||
import { OutputDialogComponent } from './components/task-logs/output-dialog/output-dialog.component';
|
||||
import { ClientTaskLogsComponent } from './components/task-logs/client-task-logs/client-task-logs.component';
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
|
@ -204,7 +212,6 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
TaskLogsComponent,
|
||||
ServerInfoDialogComponent,
|
||||
StatusComponent,
|
||||
ClientMainViewComponent,
|
||||
ImagesComponent,
|
||||
CreateImageComponent,
|
||||
PartitionAssistantComponent,
|
||||
|
@ -243,7 +250,16 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
SaveScriptComponent,
|
||||
EditImageComponent,
|
||||
ShowGitImagesComponent,
|
||||
RenameImageComponent
|
||||
RenameImageComponent,
|
||||
ClientDetailsComponent,
|
||||
PartitionTypeOrganizatorComponent,
|
||||
CreateTaskScheduleComponent,
|
||||
ShowTaskScheduleComponent,
|
||||
ShowTaskScriptComponent,
|
||||
CreateTaskScriptComponent,
|
||||
ViewParametersModalComponent,
|
||||
OutputDialogComponent,
|
||||
ClientTaskLogsComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
@ -292,7 +308,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
progressAnimation: 'increasing',
|
||||
closeButton: true
|
||||
}
|
||||
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger
|
||||
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger, NgOptimizedImage
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
|
|
|
@ -22,7 +22,12 @@
|
|||
|
||||
.time-fields {
|
||||
display: flex;
|
||||
gap: 15px; /* Espacio entre los campos */
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.hour-fields {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.time-field {
|
||||
|
@ -34,4 +39,74 @@
|
|||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-text {
|
||||
font-style: italic;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
.weekday-toggle-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 40px 0 40px 0;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.weekday-toggle {
|
||||
flex: 1 1 calc(14.28% - 10px);
|
||||
padding: 10px 0;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f5f5f5;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.weekday-toggle.selected {
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
|
||||
.availability-summary {
|
||||
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;
|
||||
}
|
||||
|
||||
.unavailability-summary {
|
||||
background-color: #ffebee;
|
||||
border-left: 4px solid #d32f2f;
|
||||
padding: 12px 16px;
|
||||
margin-top: 16px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
color: #b71c1c;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
<h2 mat-dialog-title>{{ isEditMode ? ('editCalendar' | translate) : ('addCalendar' | translate) }}</h2>
|
||||
<mat-dialog-content class="form-container">
|
||||
<mat-slide-toggle [(ngModel)]="isRemoteAvailable" class="example-margin">
|
||||
<mat-checkbox [(ngModel)]="isRemoteAvailable">
|
||||
{{ 'remoteAvailability' | translate }}
|
||||
</mat-slide-toggle>
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-divider style="margin: 10px 0;"></mat-divider>
|
||||
|
||||
<div *ngIf="!isRemoteAvailable" class="form-group">
|
||||
<mat-label>{{ 'selectWeekDays' | translate }}</mat-label>
|
||||
<div class="row">
|
||||
<div class="col-md-6 checkbox-group">
|
||||
<mat-checkbox *ngFor="let day of weekDays.slice(0, (weekDays.length / 2) + 1)" [(ngModel)]="busyWeekDays[day]">
|
||||
{{ day }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div class="col-md-6 checkbox-group">
|
||||
<mat-checkbox *ngFor="let day of weekDays.slice(weekDays.length / 2 + 1)" [(ngModel)]="busyWeekDays[day]">
|
||||
{{ day }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<p class="custom-text"> (Los dias y horas seleccionados se marcarán como aula no disponible para remote pc.) </p>
|
||||
<div class="weekday-toggle-group full-width">
|
||||
<button
|
||||
*ngFor="let day of weekDays"
|
||||
type="button"
|
||||
class="weekday-toggle"
|
||||
[class.selected]="busyWeekDays[day]"
|
||||
(click)="busyWeekDays[day] = !busyWeekDays[day]">
|
||||
{{ day.slice(0, 3) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="time-fields">
|
||||
<mat-form-field appearance="fill" class="time-field">
|
||||
<mat-label>{{ 'startTime' | translate }}</mat-label>
|
||||
|
@ -30,12 +32,24 @@
|
|||
<input matInput [(ngModel)]="busyToHour" type="time" placeholder="{{ 'endTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="unavailability-summary" *ngIf="busyFromHour && busyToHour && busyWeekDays && getSelectedDays().length">
|
||||
<mat-icon style="width: 50px;" color="warn">block</mat-icon>
|
||||
<span class="summary-text">
|
||||
El aula estará <strong>no disponible</strong> para Remote PC los días:
|
||||
<strong>{{ getSelectedDays().join(', ') }}</strong> de
|
||||
<strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isRemoteAvailable" class="form-group">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'reasonLabel' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="availableReason" placeholder="{{ 'reasonPlaceholder' | translate }}" [required]="isRemoteAvailable">
|
||||
<mat-hint>Razón por la cual el aula SI está disponible para su uso en Remote PC</mat-hint>
|
||||
</mat-form-field>
|
||||
<div class="time-fields">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
|
@ -53,6 +67,32 @@
|
|||
<mat-datepicker #picker2></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="hour-fields">
|
||||
<mat-form-field appearance="fill" class="time-field">
|
||||
<mat-label>{{ 'startTime' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="busyFromHour" type="time" placeholder="{{ 'startTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="time-field">
|
||||
<mat-label>{{ 'endTime' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="busyToHour" type="time" placeholder="{{ 'endTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="availability-summary" *ngIf="availableFromDate && availableToDate">
|
||||
<mat-icon color="primary" style="width: 50px;">info</mat-icon>
|
||||
<span class="summary-text">
|
||||
El aula estará <strong>disponible</strong> para reserva desde el
|
||||
<strong>{{ availableFromDate | date:'fullDate' }}</strong> hasta el
|
||||
<strong>{{ availableToDate | date:'fullDate' }}</strong>
|
||||
<span *ngIf="busyFromHour && busyToHour">
|
||||
en el horario de <strong>{{ busyFromHour }}</strong> a <strong>{{ busyToHour }}</strong>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
|
|
|
@ -64,8 +64,8 @@ export class CreateCalendarRuleComponent {
|
|||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
toggleAdditionalForm(): void {
|
||||
this.showAdditionalForm = !this.showAdditionalForm;
|
||||
getSelectedDays(): string[] {
|
||||
return Object.keys(this.busyWeekDays || {}).filter(day => this.busyWeekDays[day]);
|
||||
}
|
||||
|
||||
getSelectedDaysIndices() {
|
||||
|
@ -74,6 +74,11 @@ export class CreateCalendarRuleComponent {
|
|||
.filter(index => index !== -1);
|
||||
}
|
||||
|
||||
convertDateToLocalISO(date: Date): string {
|
||||
const adjustedDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
return adjustedDate.toISOString();
|
||||
}
|
||||
|
||||
submitRule(): void {
|
||||
this.getSelectedDaysIndices()
|
||||
const selectedDaysArray = Object.keys(this.busyWeekDays).map((day, index) => this.busyWeekDays[index]);
|
||||
|
@ -83,8 +88,8 @@ export class CreateCalendarRuleComponent {
|
|||
busyWeekDays: this.selectedDaysIndices,
|
||||
busyFromHour: this.busyFromHour,
|
||||
busyToHour: this.busyToHour,
|
||||
availableFromDate: this.availableFromDate,
|
||||
availableToDate: this.availableToDate,
|
||||
availableFromDate: this.availableFromDate ? this.convertDateToLocalISO(this.availableFromDate) : null,
|
||||
availableToDate: this.availableToDate ? this.convertDateToLocalISO(this.availableToDate) : null,
|
||||
isRemoteAvailable: this.isRemoteAvailable,
|
||||
availableReason: this.availableReason
|
||||
};
|
||||
|
@ -93,7 +98,7 @@ export class CreateCalendarRuleComponent {
|
|||
this.http.put(`${this.baseUrl}${this.ruleId}`, formData)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Calendar updated successfully');
|
||||
this.toastService.success('Calendar rule updated successfully');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
|
@ -105,7 +110,7 @@ export class CreateCalendarRuleComponent {
|
|||
this.http.post(`${this.baseUrl}/remote-calendar-rules`, formData)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Calendar created successfully');
|
||||
this.toastService.success('Calendar rule created successfully');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
.time-fields {
|
||||
display: flex;
|
||||
gap: 15px; /* Espacio entre los campos */
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.time-field {
|
||||
|
@ -34,24 +34,25 @@
|
|||
|
||||
.list-item-content {
|
||||
display: flex;
|
||||
align-items: flex-start; /* Alinea el contenido al inicio */
|
||||
justify-content: space-between; /* Espacio entre los textos y los íconos */
|
||||
width: 100%; /* Asegúrate de que el contenido ocupe todo el ancho */
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
flex-grow: 1; /* Permite que este contenedor ocupe el espacio disponible */
|
||||
margin-right: 16px; /* Espaciado a la derecha para separar de los íconos */
|
||||
flex-grow: 1;
|
||||
margin-right: 16px;
|
||||
margin-left: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center; /* Alinea los íconos verticalmente */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right-icon {
|
||||
margin-left: 8px; /* Espaciado entre los íconos */
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -60,4 +61,15 @@
|
|||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-available {
|
||||
background-color: #e8f5e9;
|
||||
border-left: 4px solid #4caf50;
|
||||
}
|
||||
|
||||
.rule-unavailable {
|
||||
background-color: #ffebee;
|
||||
border-left: 4px solid #f44336;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<input matInput [(ngModel)]="name" required>
|
||||
<mat-icon *ngIf="isEditMode" matSuffix (click)="submitForm()">mode_edit</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div *ngIf="isEditMode" mat-subheader>{{ 'rulesHeader' | translate }}</div>
|
||||
<button class="action-button" *ngIf="isEditMode" (click)="createRule()" style="padding: 10px;">
|
||||
|
@ -18,14 +18,20 @@
|
|||
|
||||
<mat-list *ngIf="isEditMode">
|
||||
<ng-container *ngFor="let rule of remoteCalendarRules;">
|
||||
<mat-list-item>
|
||||
<mat-list-item
|
||||
[ngClass]="{
|
||||
'rule-available': rule.isRemoteAvailable,
|
||||
'rule-unavailable': !rule.isRemoteAvailable
|
||||
}"
|
||||
>
|
||||
<div class="list-item-content">
|
||||
<mat-icon matListItemIcon>event_available</mat-icon>
|
||||
<div class="text-content">
|
||||
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('statusAvailable' | translate) : ('statusUnavailable' | translate) }}</div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyWeekDaysMap }}</div>
|
||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">{{ rule.availableReason }} | {{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</div>
|
||||
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('remotePcStatusAvailable' | translate) : ('remotePcStatusUnavailable' | translate) }}</div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">Días: <strong>{{ rule.busyWeekDaysMap }}</strong></div>
|
||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">Razón: {{ rule.availableReason }}</div>
|
||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">Días: <strong>{{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</strong></div>
|
||||
<div matListItemLine>Horario: {{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
|
||||
</div>
|
||||
<div class="icon-container">
|
||||
<button mat-icon-button color="primary" class="right-icon" (click)="createRule(rule)">
|
||||
|
|
|
@ -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', 'UTC') },
|
||||
{ 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,202 @@
|
|||
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);
|
||||
}
|
||||
|
||||
convertDateToLocalISO(date: Date): string {
|
||||
const adjustedDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
return adjustedDate.toISOString();
|
||||
}
|
||||
|
||||
|
||||
onSubmit() {
|
||||
const formData = this.form.value;
|
||||
|
||||
const payload: any = {
|
||||
commandTask: this.data.task['@id'],
|
||||
executionDate: formData.recurrenceType === 'none' ? this.convertDateToLocalISO(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,138 @@
|
|||
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,
|
||||
type: 'run-script',
|
||||
}).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', 'UTC') },
|
||||
{ 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();
|
||||
}
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' |
|
||||
translate }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" (click)="resetFilters()" joyrideStep="resetFiltersStep"
|
||||
text="{{ 'resetFiltersStepText' | translate }}">
|
||||
{{ 'resetFilters' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-select" joyrideStep="clientSelectStep"
|
||||
text="{{ 'clientSelectStepText' | translate }}">
|
||||
<input type="text" matInput [formControl]="clientControl" [matAutocomplete]="clientAuto"
|
||||
placeholder="{{ 'filterClientPlaceholder' | translate }}">
|
||||
<mat-autocomplete #clientAuto="matAutocomplete" [displayWith]="displayFnClient"
|
||||
(optionSelected)="onOptionClientSelected($event.option.value)">
|
||||
<mat-option *ngFor="let client of filteredClients | async" [value]="client">
|
||||
{{ client.name }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-select" joyrideStep="commandSelectStep"
|
||||
text="{{ 'commandSelectStepText' | translate }}">
|
||||
<input type="text" matInput [formControl]="commandControl" [matAutocomplete]="commandAuto"
|
||||
placeholder="{{ 'filterCommandPlaceholder' | translate }}">
|
||||
<mat-autocomplete #commandAuto="matAutocomplete" [displayWith]="displayFnCommand"
|
||||
(optionSelected)="onOptionCommandSelected($event.option.value)">
|
||||
<mat-option *ngFor="let command of filteredCommands | async" [value]="command">
|
||||
{{ command.name }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-boolean">
|
||||
<mat-label i18n="@@searchLabel">Estado</mat-label>
|
||||
<mat-select [(ngModel)]="filters['status']" (selectionChange)="loadTraces()" placeholder="Seleccionar opción">
|
||||
<mat-option [value]="undefined">Todos</mat-option>
|
||||
<mat-option [value]="'failed'">Fallido</mat-option>
|
||||
<mat-option [value]="'pending'">Pendiente de ejecutar</mat-option>
|
||||
<mat-option [value]="'in-progress'">Ejecutando</mat-option>
|
||||
<mat-option [value]="'success'">Completado con éxito</mat-option>
|
||||
<mat-option [value]="'cancelled'">Cancelado</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="traces" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let trace">
|
||||
|
||||
<ng-container [ngSwitch]="column.columnDef">
|
||||
<ng-container *ngSwitchCase="'status'">
|
||||
<ng-container *ngIf="trace.status === 'in-progress' && trace.progress; else statusChip">
|
||||
<div class="progress-container">
|
||||
<mat-progress-bar class="example-margin" [mode]="mode" [value]="trace.progress" [bufferValue]="bufferValue">
|
||||
</mat-progress-bar>
|
||||
<span>{{trace.progress}}%</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #statusChip>
|
||||
<div class="status-progress-flex">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-failed': trace.status === 'failed',
|
||||
'chip-success': trace.status === 'success',
|
||||
'chip-pending': trace.status === 'pending',
|
||||
'chip-in-progress': trace.status === 'in-progress',
|
||||
'chip-cancelled': trace.status === 'cancelled'
|
||||
}">
|
||||
{{
|
||||
trace.status === 'failed' ? 'Fallido' :
|
||||
trace.status === 'in-progress' ? 'En ejecución' :
|
||||
trace.status === 'success' ? 'Finalizado con éxito' :
|
||||
trace.status === 'pending' ? 'Pendiente de ejecutar' :
|
||||
trace.status === 'cancelled' ? 'Cancelado' :
|
||||
trace.status
|
||||
}}
|
||||
</mat-chip>
|
||||
<button *ngIf="trace.status === 'in-progress' && trace.command === 'deploy-image'"
|
||||
mat-icon-button
|
||||
(click)="cancelTrace(trace)"
|
||||
class="cancel-button"
|
||||
matTooltip="Cancelar transmisión de imagen">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'input'">
|
||||
<button mat-icon-button (click)="openInputModal(trace.input)">
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchDefault>
|
||||
{{ column.cell(trace) }}
|
||||
</ng-container>
|
||||
</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="paginator-container" joyrideStep="paginationStep" text="{{ 'paginationStepText' | translate }}">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
|
@ -19,6 +19,6 @@
|
|||
<button mat-menu-item [disabled]="command.disabled
|
||||
|| (command.slug === 'create-image' && clientData.length > 1)" *ngFor="let command of arrayCommands"
|
||||
(click)="onCommandSelect(command.slug)">
|
||||
{{ command.name }}
|
||||
{{ command.translationKey | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
|
@ -10,6 +10,8 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./execute-command.component.css']
|
||||
})
|
||||
export class ExecuteCommandComponent implements OnInit {
|
||||
@Input() runScriptContext: any = null;
|
||||
@Input() clientState: string = 'off';
|
||||
@Input() clientData: any[] = [];
|
||||
@Input() buttonType: 'icon' | 'text' | 'menu-item' = 'icon';
|
||||
@Input() buttonText: string = 'Ejecutar Comandos';
|
||||
|
@ -19,17 +21,17 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
loading: boolean = true;
|
||||
|
||||
arrayCommands: any[] = [
|
||||
{ name: 'Enceder', slug: 'power-on', disabled: false },
|
||||
{ name: 'Apagar', slug: 'power-off', disabled: false },
|
||||
{ name: 'Reiniciar', slug: 'reboot', disabled: false },
|
||||
{ name: 'Iniciar Sesión', slug: 'login', disabled: true },
|
||||
{ name: 'Crear imagen', slug: 'create-image', disabled: false },
|
||||
{ name: 'Clonar/desplegar imagen', slug: 'deploy-image', disabled: false },
|
||||
{ name: 'Eliminar Imagen Cache', slug: 'delete-image-cache', disabled: true },
|
||||
{ name: 'Particionar y Formatear', slug: 'partition', disabled: false },
|
||||
{ name: 'Inventario Software', slug: 'software-inventory', disabled: true },
|
||||
{ name: 'Inventario Hardware', slug: 'hardware-inventory', disabled: true },
|
||||
{ name: 'Ejecutar comando', slug: 'run-script', disabled: false },
|
||||
{ translationKey: 'executeCommands.powerOn', slug: 'power-on', disabled: false },
|
||||
{ translationKey: 'executeCommands.powerOff', slug: 'power-off', disabled: false },
|
||||
{ translationKey: 'executeCommands.reboot', slug: 'reboot', disabled: false },
|
||||
{ translationKey: 'executeCommands.login', slug: 'login', disabled: true },
|
||||
{ translationKey: 'executeCommands.createImage', slug: 'create-image', disabled: false },
|
||||
{ translationKey: 'executeCommands.deployImage', slug: 'deploy-image', disabled: false },
|
||||
{ translationKey: 'executeCommands.deleteImageCache', slug: 'delete-image-cache', disabled: true },
|
||||
{ translationKey: 'executeCommands.partition', slug: 'partition', disabled: false },
|
||||
{ translationKey: 'executeCommands.softwareInventory', slug: 'software-inventory', disabled: true },
|
||||
{ translationKey: 'executeCommands.hardwareInventory', slug: 'hardware-inventory', disabled: true },
|
||||
{ translationKey: 'executeCommands.runScript', slug: 'run-script', disabled: false },
|
||||
];
|
||||
|
||||
client: any = {};
|
||||
|
@ -45,6 +47,48 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
|
||||
ngOnInit(): void {
|
||||
this.clientData = this.clientData || [];
|
||||
this.updateCommandStates();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.updateCommandStates();
|
||||
}
|
||||
|
||||
private updateCommandStates(): void {
|
||||
let states: string[] = [];
|
||||
|
||||
if (this.clientData.length > 0) {
|
||||
states = this.clientData.map(client => client.status);
|
||||
} else if (this.clientState) {
|
||||
states = [this.clientState];
|
||||
}
|
||||
|
||||
const allOffOrDisconnected = states.every(state => state === 'off' || state === 'disconnected');
|
||||
const allSameState = states.every(state => state === states[0]);
|
||||
const multipleClients = this.clientData.length > 1;
|
||||
|
||||
this.arrayCommands = this.arrayCommands.map(command => {
|
||||
if (allOffOrDisconnected) {
|
||||
command.disabled = command.slug !== 'power-on';
|
||||
} else if (allSameState) {
|
||||
if (states[0] === 'off' || states[0] === 'disconnected') {
|
||||
command.disabled = command.slug !== 'power-on';
|
||||
} else {
|
||||
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'partition', 'run-script'].includes(command.slug);
|
||||
}
|
||||
} else {
|
||||
if (command.slug === 'create-image') {
|
||||
command.disabled = multipleClients;
|
||||
} else if (
|
||||
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'run-script'].includes(command.slug)
|
||||
) {
|
||||
command.disabled = false;
|
||||
} else {
|
||||
command.disabled = true;
|
||||
}
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}
|
||||
|
||||
onCommandSelect(action: any): void {
|
||||
|
@ -137,7 +181,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
const clientDataToSend = this.clientData.map(client => ({
|
||||
name: client.name,
|
||||
mac: client.mac,
|
||||
uuid: '/clients/'+client.uuid,
|
||||
uuid: '/clients/' + client.uuid,
|
||||
status: client.status,
|
||||
partitions: client.partitions,
|
||||
firmwareType: client.firmwareType,
|
||||
|
@ -145,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)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -161,16 +206,17 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
const clientDataToSend = this.clientData.map(client => ({
|
||||
name: client.name,
|
||||
mac: client.mac,
|
||||
uuid: '/clients/'+client.uuid,
|
||||
uuid: '/clients/' + client.uuid,
|
||||
status: client.status,
|
||||
partitions: client.partitions,
|
||||
ip: client.ip
|
||||
}));
|
||||
|
||||
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)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -178,18 +224,17 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
const clientDataToSend = this.clientData.map(client => ({
|
||||
name: client.name,
|
||||
mac: client.mac,
|
||||
uuid: '/clients/'+client.uuid,
|
||||
uuid: '/clients/' + client.uuid,
|
||||
status: client.status,
|
||||
partitions: client.partitions,
|
||||
ip: client.ip
|
||||
}));
|
||||
|
||||
this.router.navigate(['/clients/run-script'], {
|
||||
queryParams: { clientData: JSON.stringify(clientDataToSend) }
|
||||
}).then(() => {
|
||||
console.log('Navigated to run script with data:', clientDataToSend);
|
||||
});
|
||||
queryParams: {
|
||||
clientData: JSON.stringify(clientDataToSend),
|
||||
runScriptContext: JSON.stringify(this.runScriptContext)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,13 @@
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
.table-header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.client-container {
|
||||
flex-grow: 1;
|
||||
box-sizing: border-box;
|
||||
|
@ -273,7 +280,7 @@
|
|||
}
|
||||
|
||||
.table-container {
|
||||
flex: 3;
|
||||
flex: 5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
|
@ -29,8 +29,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-container">
|
||||
<div class="table-header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
|
||||
<mat-chip> {{ clientData.firmwareType }}</mat-chip>
|
||||
</div>
|
||||
|
||||
<div class="disk-container">
|
||||
|
|
|
@ -13,6 +13,16 @@
|
|||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
<div class="selector">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Tipo de imagen</mat-label>
|
||||
<mat-select [(ngModel)]="imageType" class="full-width" (selectionChange)="onImageTypeSelected($event.value)">
|
||||
<mat-option [value]="'monolithic'">Monolítica</mat-option>
|
||||
<mat-option [value]="'git'">Git</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="selector">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Nombre canónico</mat-label>
|
||||
|
@ -21,7 +31,7 @@
|
|||
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Seleccione imagen</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required>
|
||||
<mat-select [disabled]="!imageType" [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required>
|
||||
<mat-option *ngFor="let image of images" [value]="image">{{ image?.name }}</mat-option>
|
||||
</mat-select>
|
||||
<button *ngIf="selectedImage" mat-icon-button matSuffix aria-label="Clear client search"
|
||||
|
@ -51,7 +61,19 @@
|
|||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
{{ column.cell(image) }}
|
||||
<ng-container *ngIf="column.columnDef !== 'size'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.size }} MB</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -25,32 +25,38 @@ export class CreateClientImageComponent implements OnInit{
|
|||
client: any = null;
|
||||
loading: boolean = false;
|
||||
selectedImage: any = null;
|
||||
imageType : string = 'monolithic';
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'diskNumber',
|
||||
header: 'Disco',
|
||||
cell: (partition: any) => `${partition.diskNumber}`
|
||||
cell: (partition: any) => partition.diskNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionNumber',
|
||||
header: 'Particion',
|
||||
cell: (partition: any) => `${partition.partitionNumber}`
|
||||
cell: (partition: any) => partition.partitionNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'size',
|
||||
header: 'Tamaño',
|
||||
cell: (partition: any) => `${partition.size} MB`
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionCode',
|
||||
header: 'Tipo de partición',
|
||||
cell: (partition: any) => partition.partitionCode
|
||||
},
|
||||
{
|
||||
columnDef: 'filesystem',
|
||||
header: 'Sistema de ficheros',
|
||||
cell: (partition: any) => `${partition.filesystem}`
|
||||
cell: (partition: any) => partition.filesystem
|
||||
},
|
||||
{
|
||||
columnDef: 'operativeSystem',
|
||||
header: 'SO',
|
||||
cell: (partition: any) => `${partition.operativeSystem?.name}`
|
||||
cell: (partition: any) => partition.operativeSystem?.name
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -92,8 +98,13 @@ export class CreateClientImageComponent implements OnInit{
|
|||
);
|
||||
}
|
||||
|
||||
onImageTypeSelected(event: any) {
|
||||
this.imageType = event;
|
||||
this.loadImages();
|
||||
}
|
||||
|
||||
loadImages() {
|
||||
const url = `${this.baseUrl}/images?created=false&page=1&itemsPerPage=1000`;
|
||||
const url = `${this.baseUrl}/images?created=false&type=${this.imageType}&page=1&itemsPerPage=100`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
this.images = response['hydra:member'];
|
||||
|
@ -126,6 +137,7 @@ export class CreateClientImageComponent implements OnInit{
|
|||
name: this.name,
|
||||
partition: this.selectedPartition['@id'],
|
||||
source: 'assistant',
|
||||
type: this.imageType,
|
||||
selectedImage: this.selectedImage?.['@id']
|
||||
};
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
@ -67,10 +78,20 @@
|
|||
<mat-divider style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
<div class="deploy-container">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Tipo de imagen</mat-label>
|
||||
<mat-select [(ngModel)]="imageType" (selectionChange)="onImageTypeSelected($event.value)">
|
||||
<mat-option [value]="'monolithic'">Monolítica</mat-option>
|
||||
<mat-option [value]="'git'">Git</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="deploy-container">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione imagen</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage">
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage" [disabled] = "!imageType" >
|
||||
<mat-option *ngFor="let image of images" [value]="image">
|
||||
<div class="unit-name"> {{ image.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ image.description }}</div>
|
||||
|
|
|
@ -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' },
|
||||
|
@ -49,6 +52,7 @@ export class DeployImageComponent {
|
|||
selectedModelClient: any = null;
|
||||
filteredPartitions: any[] = [];
|
||||
selectedRepository: any = null;
|
||||
imageType: string = 'monolithic';
|
||||
|
||||
allMethods = [
|
||||
{ name: 'Multicast', value: 'udpcast' },
|
||||
|
@ -95,13 +99,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 +130,32 @@ export class DeployImageComponent {
|
|||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.runScriptContext = params['runScriptContext'] ? JSON.parse(params['runScriptContext']) : null;
|
||||
});
|
||||
}
|
||||
|
||||
onImageTypeSelected(event: any) {
|
||||
this.imageType = event;
|
||||
this.loadImages();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -201,7 +235,13 @@ export class DeployImageComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}/image-image-repositories?status=success&repository.id=${repositoryId}&page=1&itemsPerPage=1000`;
|
||||
let url = ''
|
||||
|
||||
if (this.imageType === 'monolithic') {
|
||||
url = `${this.baseUrl}/image-image-repositories?status=success&repository.id=${repositoryId}&page=1&itemsPerPage=1000`;
|
||||
} else {
|
||||
url = `${this.baseUrl}/git-image-repositories?status=success&repository.id=${repositoryId}&page=1&itemsPerPage=1000`;
|
||||
}
|
||||
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
|
@ -291,4 +331,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 10px;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -246,6 +238,34 @@ button.remove-btn:hover {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.disk-space-info-container {
|
||||
display: flex;
|
||||
justify-self: start;
|
||||
margin-top: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.disk-space-info {
|
||||
padding: 16px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 16px auto;
|
||||
}
|
||||
|
||||
.chip-free {
|
||||
background-color: #d0f0c0; /* verde claro */
|
||||
color: #2e7d32;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chip-full {
|
||||
background-color: #ffcccb; /* rojo claro */
|
||||
color: #c62828;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
@ -78,10 +87,30 @@
|
|||
|
||||
<div class="partition-assistant" *ngIf="selectedDisk">
|
||||
<div class="row-button">
|
||||
<button class="action-button" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
|
||||
<button class="action-button" [disabled]="partitionCode === 'MSDOS'" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
|
||||
<mat-chip *ngIf="selectedModelClient.firmwareType">
|
||||
Tabla de particiones: {{ selectedModelClient.firmwareType }}
|
||||
Firmware: {{ selectedModelClient.firmwareType }}
|
||||
</mat-chip>
|
||||
<mat-chip color="info" *ngIf="partitionCode">
|
||||
Tabla de particiones: {{ partitionCode }}
|
||||
</mat-chip>
|
||||
|
||||
</div>
|
||||
|
||||
<mat-divider style="padding: 10px;"></mat-divider>
|
||||
|
||||
<div class="disk-space-info-container">
|
||||
<div class="disk-space-info" [ngClass]="selectedDisk.used < selectedDisk.totalDiskSize ? 'chip-free' : 'chip-full'">
|
||||
Espacio usado: {{ selectedDisk.used }} MB
|
||||
</div>
|
||||
|
||||
<div class="disk-space-info" [ngClass]="selectedDisk.used < selectedDisk.totalDiskSize ? 'chip-free' : 'chip-full'">
|
||||
Espacio libre: {{ selectedDisk.totalDiskSize - selectedDisk.used}} MB
|
||||
</div>
|
||||
|
||||
<div class="disk-space-info">
|
||||
Espacio total: {{ selectedDisk.totalDiskSize }} MB
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
@ -95,7 +124,6 @@
|
|||
<th>Tamaño (MB)</th>
|
||||
<th>Tamaño (%)</th>
|
||||
<th>Formatear</th>
|
||||
<th>Eliminar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -103,32 +131,32 @@
|
|||
<tr *ngIf="!partition.removed">
|
||||
<td>{{ partition.partitionNumber }}</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.partitionCode" required>
|
||||
<select [(ngModel)]="partition.partitionCode" required [disabled]="partition.partitionNumber === 1 && partitionCode === 'GPT'">
|
||||
<option *ngFor="let type of partitionTypes" [value]="type.name">
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select [(ngModel)]="partition.filesystem" required>
|
||||
<select [(ngModel)]="partition.filesystem" required [disabled]="partition.partitionNumber === 1 && partitionCode === 'GPT'">
|
||||
<option *ngFor="let type of filesystemTypes" [value]="type.name">
|
||||
{{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" [(ngModel)]="partition.size" required
|
||||
<input [disabled]="partition.partitionNumber === 1 && partitionCode === 'GPT'" type="number" [(ngModel)]="partition.size" required
|
||||
(input)="updatePartitionSize(selectedDisk.diskNumber, j, partition.size)" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" [(ngModel)]="partition.percentage"
|
||||
<input [disabled]="partition.partitionNumber === 1 && partitionCode === 'GPT'" type="number" [(ngModel)]="partition.percentage"
|
||||
(input)="updatePartitionSizeFromPercentage(selectedDisk.diskNumber, j, partition.percentage)" />
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" [(ngModel)]="partition.format" />
|
||||
<mat-checkbox type="checkbox" [(ngModel)]="partition.format" />
|
||||
</td>
|
||||
<td>
|
||||
<button (click)="removePartition(selectedDisk.diskNumber, partition)" class="remove-btn">X</button>
|
||||
<button mat-button *ngIf="partitionCode !== 'MSDOS'" (click)="removePartition(selectedDisk.diskNumber, partition)" class="remove-btn">X</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
|
|
@ -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;
|
||||
|
@ -48,6 +51,7 @@ export class PartitionAssistantComponent {
|
|||
allSelected = true;
|
||||
selectedClients: any[] = [];
|
||||
selectedModelClient: any = null;
|
||||
partitionCode: string = '';
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
|
@ -55,6 +59,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 +67,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 +91,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;
|
||||
}
|
||||
|
@ -96,6 +110,7 @@ export class PartitionAssistantComponent {
|
|||
(response) => {
|
||||
this.data = response;
|
||||
this.initializeDisks();
|
||||
this.partitionCode = this.data.partitions[0].partitionCode;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar los datos del cliente:', error);
|
||||
|
@ -103,6 +118,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 }) => {
|
||||
|
@ -225,6 +254,8 @@ export class PartitionAssistantComponent {
|
|||
|
||||
if (disk) {
|
||||
const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize);
|
||||
console.log('Remaining GB:', remainingGB);
|
||||
console.log('Total Disk Size:', disk);
|
||||
|
||||
if (remainingGB > 0) {
|
||||
const removedPartitions = disk.partitions.filter((p) => !p.removed);
|
||||
|
@ -232,7 +263,6 @@ export class PartitionAssistantComponent {
|
|||
removedPartitions.length > 0 ? Math.max(...removedPartitions.map((p) => p.partitionNumber)) : 0;
|
||||
const newPartitionNumber = maxPartitionNumber + 1;
|
||||
|
||||
|
||||
disk.partitions.push({
|
||||
partitionNumber: newPartitionNumber,
|
||||
size: 0,
|
||||
|
@ -276,10 +306,14 @@ export class PartitionAssistantComponent {
|
|||
}
|
||||
|
||||
getRemainingGB(partitions: Partition[], totalDiskSize: number): number {
|
||||
const totalUsedGB = partitions.reduce((acc, partition) => acc + partition.size, 0);
|
||||
const totalUsedGB = partitions
|
||||
.filter(partition => !partition.removed)
|
||||
.reduce((acc, partition) => acc + partition.size, 0);
|
||||
|
||||
return Math.max(0, totalDiskSize - totalUsedGB);
|
||||
}
|
||||
|
||||
|
||||
save() {
|
||||
if (!this.selectedDisk) {
|
||||
this.toastService.error('No se ha seleccionado un disco.');
|
||||
|
@ -346,6 +380,10 @@ export class PartitionAssistantComponent {
|
|||
if (partitionToRemove) {
|
||||
partitionToRemove.removed = true;
|
||||
}
|
||||
|
||||
disk.used = this.calculateUsedSpace(disk.partitions);
|
||||
disk.percentage = (disk.used / disk.totalDiskSize) * 100;
|
||||
|
||||
this.updateDiskChart(disk);
|
||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||
}
|
||||
|
@ -364,9 +402,12 @@ export class PartitionAssistantComponent {
|
|||
}
|
||||
|
||||
calculateUsedSpace(partitions: Partition[]): number {
|
||||
return partitions.reduce((acc, partition) => acc + partition.size, 0);
|
||||
return partitions
|
||||
.filter(partition => !partition.removed)
|
||||
.reduce((acc, partition) => acc + partition.size, 0);
|
||||
}
|
||||
|
||||
|
||||
generateChartData(partitions: Partition[]): any[] {
|
||||
return partitions.map((partition) => ({
|
||||
name: `Partición ${partition.partitionNumber}`,
|
||||
|
@ -376,8 +417,62 @@ export class PartitionAssistantComponent {
|
|||
}
|
||||
|
||||
updateDiskChart(disk: any) {
|
||||
console.log('disk', disk);
|
||||
disk.chartData = this.generateChartData(disk.partitions);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -5,10 +5,19 @@
|
|||
<h2>
|
||||
{{ '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,18 +1,19 @@
|
|||
import {Component, EventEmitter, 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 {SaveScriptComponent} from "./save-script/save-script.component";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
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";
|
||||
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>();
|
||||
|
||||
|
@ -32,6 +33,7 @@ export class RunScriptAssistantComponent {
|
|||
newScript: string = '';
|
||||
selection = new SelectionModel(true, []);
|
||||
parameterNames: string[] = Object.keys(this.parameters);
|
||||
runScriptContext: any = null;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
|
@ -46,6 +48,9 @@ export class RunScriptAssistantComponent {
|
|||
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}) => {
|
||||
|
@ -53,12 +58,38 @@ export class RunScriptAssistantComponent {
|
|||
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;
|
||||
});
|
||||
}
|
||||
|
||||
get runScriptTitle(): string {
|
||||
const ctx = this.runScriptContext;
|
||||
if (!ctx) {
|
||||
return '';
|
||||
}
|
||||
// Si es un array de clientes
|
||||
if (Array.isArray(ctx)) {
|
||||
return ctx.map(c => c.name).join(', ');
|
||||
}
|
||||
// Si es un objeto con propiedad name
|
||||
if (typeof ctx === 'object' && 'name' in ctx) {
|
||||
return ctx.name;
|
||||
}
|
||||
// Si es un string plano
|
||||
return String(ctx);
|
||||
}
|
||||
|
||||
|
||||
loadScripts(): void {
|
||||
this.loading = true;
|
||||
|
||||
|
@ -114,7 +145,7 @@ export class RunScriptAssistantComponent {
|
|||
}
|
||||
|
||||
return client.partitions
|
||||
.map((p: { partitionNumber: any; size: any; filesystem: any }) => `#${p.partitionNumber} ${p.filesystem} - ${p.size / 1024 }GB`)
|
||||
.map((p: { partitionNumber: any; size: any; filesystem: any }) => `#${p.partitionNumber} ${p.filesystem} - ${p.size / 1024}GB`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
|
@ -150,17 +181,12 @@ export class RunScriptAssistantComponent {
|
|||
this.scriptContent = updatedScript;
|
||||
}
|
||||
|
||||
trackByIndex(index: number): number {
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
save(): void {
|
||||
this.loading = true;
|
||||
|
||||
this.http.post(`${this.baseUrl}/commands/run-script`, {
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
script: this.commandType === 'existing' ? this.scriptContent : this.newScript,
|
||||
script: this.commandType === 'existing' ? this.scriptContent : this.newScript,
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Script ejecutado correctamente');
|
||||
|
@ -174,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,25 +1,27 @@
|
|||
<div class="groups-container">
|
||||
<!-- HEADER -->
|
||||
<div class="header-container" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="initTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
|
||||
<h2 joyrideStep="groupsTitleStep" text="{{ 'groupsTitleStepText' | translate }}">
|
||||
{{ 'adminGroupsTitle' | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="groups-button-row" joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}">
|
||||
<button class="action-button" (click)="addOU($event)"
|
||||
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'newOrganizationalUnitButton' | translate }}
|
||||
</button>
|
||||
<button class="action-button" [matMenuTriggerFor]="menuClients">{{ 'newClientButton' | translate }}</button>
|
||||
<mat-menu #menuClients="matMenu">
|
||||
<button mat-menu-item (click)="addClient($event)">{{ 'newSingleClientButton' | translate }}</button>
|
||||
<button mat-menu-item (click)="addMultipleClients($event)">{{ 'newMultipleClientButton' | translate }}</button>
|
||||
</mat-menu>
|
||||
|
||||
<div class="groups-button-row">
|
||||
<div joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}" style="display: flex; gap: 15px;">
|
||||
<button class="action-button" (click)="addOU($event)"
|
||||
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'newOrganizationalUnitButton' | translate }}
|
||||
</button>
|
||||
<button class="action-button" [matMenuTriggerFor]="menuClients">{{ 'newClientButton' | translate }}</button>
|
||||
<mat-menu #menuClients="matMenu">
|
||||
<button mat-menu-item (click)="addClient($event)">{{ 'newSingleClientButton' | translate }}</button>
|
||||
<button mat-menu-item (click)="addMultipleClients($event)">{{ 'newMultipleClientButton' | translate
|
||||
}}</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
<button class="ordinary-button" (click)="openBottomSheet()" joyrideStep="keyStep"
|
||||
text="{{ 'keyStepText' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'legendButton' | translate }}
|
||||
|
@ -81,7 +83,7 @@
|
|||
</button>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field search-select" appearance="outline">
|
||||
<mat-select placeholder="Buscar por estado..." #clientSearchStatusInput
|
||||
<mat-select placeholder="{{ 'searchState' | translate }}" #clientSearchStatusInput
|
||||
(selectionChange)="onClientFilterStatusInput($event.value)">
|
||||
<mat-option *ngFor="let option of status" [value]="option.value">
|
||||
{{ option.name }}
|
||||
|
@ -116,7 +118,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Tree -->
|
||||
<div class="tree-container">
|
||||
<div class="tree-container" joyrideStep="treePanelStep" text="{{ 'treePanelStepText' | translate }}">
|
||||
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
|
||||
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}"
|
||||
*matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick($event, node)">
|
||||
|
@ -198,11 +200,16 @@
|
|||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="openPartitionTypeModal($event, selectedNode)">
|
||||
<mat-icon>storage</mat-icon>
|
||||
<span>{{ 'partitions' | translate }}</span>
|
||||
</button>
|
||||
<app-execute-command [clientData]="selectedNode?.clients || []" [buttonType]="'menu-item'"
|
||||
[buttonText]="'Ejecutar comandos'" [icon]="'terminal'" [disabled]="!((selectedNode?.clients ?? []).length > 0)">
|
||||
[buttonText]="'ejecutarComandos' | translate" [icon]="'terminal'"
|
||||
[disabled]="!((selectedNode?.clients ?? []).length > 0)" [runScriptContext]="selectedNode?.name || ''"
|
||||
[runScriptContext]="getRunScriptContext(selectedNode?.clients || [])">
|
||||
</app-execute-command>
|
||||
</mat-menu>
|
||||
|
||||
</div>
|
||||
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
|
@ -217,15 +224,18 @@
|
|||
<strong>{{ selectedNode?.name }}</strong>
|
||||
</span>
|
||||
<div class="view-type-container">
|
||||
<app-execute-command [clientData]="selection.selected" [buttonType]="'text'"
|
||||
[buttonText]="'Ejecutar comandos'" [disabled]="selection.selected.length === 0"></app-execute-command>
|
||||
<div joyrideStep="executeCommandStep" text="{{ 'executeCommandStepText' | translate }}">
|
||||
<app-execute-command [clientData]="selection.selected" [buttonType]="'text'"
|
||||
[buttonText]="'ejecutarComandos' | translate" [disabled]="selection.selected.length === 0"
|
||||
[runScriptContext]="getRunScriptContext(selection.selected)"></app-execute-command>
|
||||
</div>
|
||||
<mat-button-toggle-group name="viewType" aria-label="View Type" [hideSingleSelectionIndicator]="true"
|
||||
(change)="toggleView($event.value)">
|
||||
(change)="toggleView($event.value)" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
|
||||
<mat-button-toggle value="list" [disabled]="currentView === 'list'">
|
||||
<mat-icon>list</mat-icon> <span class="type-view-text">{{ 'Vista Lista' | translate }}</span>
|
||||
<mat-icon>list</mat-icon> <span class="type-view-text">{{ 'vistalista' | translate }}</span>
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle value="card" [disabled]="currentView === 'card'">
|
||||
<mat-icon>grid_view</mat-icon> <span class="type-view-text">{{ 'Vista Tarjeta' | translate }}</span>
|
||||
<mat-icon>grid_view</mat-icon> <span class="type-view-text">{{ 'vistatarjeta' | translate }}</span>
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
@ -234,7 +244,8 @@
|
|||
<app-loading [isLoading]="isLoadingClients"></app-loading>
|
||||
|
||||
<!-- CLIENTS VIEWS-->
|
||||
<div class="clients-view" *ngIf="!isLoadingClients">
|
||||
<div class="clients-view" *ngIf="!isLoadingClients" joyrideStep="clientsViewStep"
|
||||
text="{{ 'clientsViewStepText' | translate }}">
|
||||
<div *ngIf="hasClients; else noClientsTemplate">
|
||||
|
||||
<!-- Cards view -->
|
||||
|
@ -260,8 +271,10 @@
|
|||
<span class="client-ip">{{ client.mac }}</span>
|
||||
<div class="action-icons">
|
||||
|
||||
<app-execute-command [clientData]="[client]" [buttonType]="'icon'" [icon]="'terminal'"
|
||||
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"></app-execute-command>
|
||||
<app-execute-command [clientState]="client.status" [clientData]="[client]"
|
||||
[buttonType]="'icon'" [icon]="'terminal'"
|
||||
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
|
||||
[runScriptContext]="getRunScriptContext([client])"></app-execute-command>
|
||||
|
||||
<button
|
||||
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
|
||||
|
@ -287,6 +300,10 @@
|
|||
<mat-icon>sync</mat-icon>
|
||||
<span>{{ 'sync' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="openClientTaskLogs($event, client)">
|
||||
<mat-icon>list_alt</mat-icon>
|
||||
<span>{{ 'procedimientosCliente' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
|
@ -358,15 +375,19 @@
|
|||
<ng-container matColumnDef="firmwareType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'firmwareType' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<mat-chip s>
|
||||
{{ client.firmwareType ? client.firmwareType : 'N/A' }}
|
||||
<mat-chip *ngIf="client.firmwareType">
|
||||
{{ client.firmwareType }}
|
||||
</mat-chip>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="oglive">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.ogLive?.date | date }} </td>
|
||||
<td mat-cell *matCellDef="let client">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span>{{ client.ogLive?.kernel }} </span>
|
||||
<span style="font-size: 0.75rem; color: gray;"> {{ client.ogLive?.date | date }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="maintenace">
|
||||
|
@ -379,7 +400,7 @@
|
|||
</ng-container>
|
||||
<ng-container matColumnDef="pxeTemplate">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'pxeTemplate' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.template?.name }} </td>
|
||||
<td mat-cell *matCellDef="let client"> {{ client.pxeTemplate?.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="parentName">
|
||||
|
@ -395,8 +416,10 @@
|
|||
mat-icon-button [matMenuTriggerFor]="clientMenu" color="primary">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<app-execute-command [clientData]="[client]" [buttonType]="'icon'" [icon]="'terminal'"
|
||||
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))">
|
||||
<app-execute-command [clientState]="client.status" [clientData]="[client]" [buttonType]="'icon'"
|
||||
[icon]="'terminal'"
|
||||
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
|
||||
[runScriptContext]="getRunScriptContext([client])">
|
||||
</app-execute-command>
|
||||
<mat-menu #clientMenu="matMenu">
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||
|
@ -411,6 +434,10 @@
|
|||
<mat-icon>sync</mat-icon>
|
||||
<span>{{ 'sync' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="openClientTaskLogs($event, client)">
|
||||
<mat-icon>list_alt</mat-icon>
|
||||
<span>{{ 'procedimientosCliente' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
|
@ -442,4 +469,4 @@
|
|||
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
|
@ -1,4 +1,4 @@
|
|||
import {Component, OnInit, OnDestroy, ViewChild, QueryList, ViewChildren, ChangeDetectorRef} from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ViewChild, QueryList, ViewChildren, ChangeDetectorRef } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
@ -26,6 +26,9 @@ import { Subject } from 'rxjs';
|
|||
import { ConfigService } from '@services/config.service';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { MatMenuTrigger } from '@angular/material/menu';
|
||||
import { ClientDetailsComponent } from './shared/client-details/client-details.component';
|
||||
import { PartitionTypeOrganizatorComponent } from './shared/partition-type-organizator/partition-type-organizator.component';
|
||||
import { ClientTaskLogsComponent } from '../task-logs/client-task-logs/client-task-logs.component';
|
||||
|
||||
enum NodeType {
|
||||
OrganizationalUnit = 'organizational-unit',
|
||||
|
@ -85,6 +88,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
{ value: 'windows-session', name: 'Windows Session' },
|
||||
{ value: 'busy', name: 'Ocupado' },
|
||||
{ value: 'mac', name: 'Mac' },
|
||||
{ value: 'disconnected', name: 'Desconectado' }
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['select', 'status', 'ip', 'firmwareType', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
|
||||
|
@ -181,7 +185,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
const index = this.arrayClients.findIndex(client => client['@id'] === clientUuid);
|
||||
if (index !== -1) {
|
||||
const updatedClient = {...this.arrayClients[index], status};
|
||||
const updatedClient = { ...this.arrayClients[index], status };
|
||||
this.arrayClients = [
|
||||
...this.arrayClients.slice(0, index),
|
||||
updatedClient,
|
||||
|
@ -608,7 +612,11 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
onShowClientDetail(event: MouseEvent, client: Client): void {
|
||||
event.stopPropagation();
|
||||
this.router.navigate(['clients', client.uuid], { state: { clientData: client } });
|
||||
this.dialog.open(ClientDetailsComponent, {
|
||||
width: '70vw',
|
||||
height: '90vh',
|
||||
data: { clientData: client },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
@ -629,9 +637,9 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
|
||||
iniciarTour(): void {
|
||||
initTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: ['groupsTitleStepText', 'filtersPanelStep', 'addStep', 'keyStep', 'tabsStep'],
|
||||
steps: ['groupsTitleStep', 'filtersPanelStep', 'treePanelStep', 'addStep', 'keyStep', 'executeCommandStep', 'tabsStep', 'clientsViewStep'],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5',
|
||||
});
|
||||
|
@ -823,4 +831,45 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
clientSearchStatusInput.value = null;
|
||||
this.fetchClientsForNode(this.selectedNode);
|
||||
}
|
||||
|
||||
getRunScriptContext(clientData: any[]): any {
|
||||
const selectedClientNames = clientData.map(client => client.name);
|
||||
|
||||
if (clientData.length === 1) {
|
||||
return clientData[0]; // devuelve el objeto cliente completo
|
||||
} else if (
|
||||
clientData.length === this.selectedClients.data.length &&
|
||||
selectedClientNames.every(name => this.selectedClients.data.some(c => c.name === name))
|
||||
) {
|
||||
return this.selectedNode || null; // devuelve el nodo completo
|
||||
} else if (clientData.length > 1) {
|
||||
return clientData; // devuelve array de objetos cliente
|
||||
} else if (this.selectedNode && clientData.length === 0) {
|
||||
return this.selectedNode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
openPartitionTypeModal(event: MouseEvent, node: TreeNode | null = null): void {
|
||||
event.stopPropagation();
|
||||
|
||||
const simplifiedClientsData = node?.clients?.map((client: any) => ({
|
||||
name: client.name,
|
||||
partitions: client.partitions
|
||||
}));
|
||||
|
||||
this.dialog.open(PartitionTypeOrganizatorComponent, {
|
||||
width: '1200px',
|
||||
data: simplifiedClientsData
|
||||
});
|
||||
}
|
||||
|
||||
openClientTaskLogs(event: MouseEvent, client: Client): void {
|
||||
event.stopPropagation();
|
||||
|
||||
this.dialog.open(ClientTaskLogsComponent, {
|
||||
width: '1200px',
|
||||
data: {client}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,21 +21,24 @@ mat-card {
|
|||
}
|
||||
|
||||
.client-image {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 70%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.proyector-image {
|
||||
width: auto;
|
||||
height: 100px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.client-info {
|
||||
display: grid;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
font-size: medium;
|
||||
color: gray;
|
||||
align-items: center;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
|
@ -131,4 +134,4 @@ mat-dialog-content {
|
|||
|
||||
.submit-button {
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
<img mat-card-image src="assets/images/client.png" alt="{{ 'clientAlt' | translate }}" class="client-image"/>
|
||||
</div>
|
||||
<div class="client-info">
|
||||
<span>{{ client.name }}</span>
|
||||
<span><strong>{{ client.name }}</strong></span>
|
||||
<span>{{ client.ip }}</span>
|
||||
<span>{{ client.mac }}</span>
|
||||
</div>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
.modal-content {
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1em 4em 2em 4em;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.client-info-card {
|
||||
background-color: #f1f1f1;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.info-columns {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.info-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-pair {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 550;
|
||||
color: #3f51b5;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 1rem;
|
||||
color: #222;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.disk-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.disk-layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 5;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
flex: 2;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 2px solid #f3f3f3;
|
||||
border-radius: 0px !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.table-header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.charts-container.single-disk {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.charts-container.single-disk .disk-usage {
|
||||
max-width: 400px;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.disk-usage {
|
||||
flex: 1 1 260px;
|
||||
min-width: 240px;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.client-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
margin-bottom: 40px;
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
<mat-dialog-content class="modal-content">
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div class="client-container" *ngIf="!loading">
|
||||
<div class="header-container">
|
||||
<h2 class="title">{{ 'clientDetailsTitle' | translate }} {{ clientData.name }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="client-info-card">
|
||||
<div class="info-columns">
|
||||
<div class="info-column">
|
||||
<div class="info-pair" *ngFor="let data of generalData">
|
||||
<div class="label">{{ data?.property }}</div>
|
||||
<div class="value">{{ data?.value || '--' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-column">
|
||||
<div class="info-pair" *ngFor="let data of networkData">
|
||||
<div class="label">{{ data?.property }}</div>
|
||||
<div class="value">{{ data?.value || '--' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-header-container">
|
||||
<h2 i18n="@@adminImagesTitle">Discos/Particiones</h2>
|
||||
<mat-chip *ngIf="clientData.firmwareType"> {{ clientData.firmwareType }}</mat-chip>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="disk-container">
|
||||
<div class="disk-layout">
|
||||
<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.columnDef !== 'operativeSystem'">
|
||||
{{ 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>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'operativeSystem'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span>{{image.operativeSystem?.name }}</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.image?.name }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
|
||||
<ng-container>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [hidden]="row.partitionNumber === 0"></tr>
|
||||
</ng-container>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="charts-container" [ngClass]="{'single-disk': chartDisk.length === 1}">
|
||||
<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>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'closeButton' | translate }}</button>
|
||||
</mat-dialog-actions>
|
|
@ -0,0 +1,52 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ClientDetailsComponent } from './client-details.component';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { LoadingComponent } from 'src/app/shared/loading/loading.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
|
||||
describe('ClientDetailsComponent', () => {
|
||||
let component: ClientDetailsComponent;
|
||||
let fixture: ComponentFixture<ClientDetailsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ClientDetailsComponent, LoadingComponent],
|
||||
imports: [
|
||||
MatDialogModule,
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
MatDividerModule,
|
||||
TranslateModule.forRoot(),
|
||||
CommonModule,
|
||||
MatTableModule,
|
||||
MatProgressSpinnerModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ClientDetailsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
|
@ -14,11 +15,11 @@ interface ClientInfo {
|
|||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-client-main-view',
|
||||
templateUrl: './client-main-view.component.html',
|
||||
styleUrl: './client-main-view.component.css'
|
||||
selector: 'app-client-details',
|
||||
templateUrl: './client-details.component.html',
|
||||
styleUrl: './client-details.component.css'
|
||||
})
|
||||
export class ClientMainViewComponent implements OnInit {
|
||||
export class ClientDetailsComponent {
|
||||
baseUrl: string;
|
||||
@ViewChild('assistantContainer') assistantContainer!: ElementRef;
|
||||
clientUuid: string;
|
||||
|
@ -34,39 +35,30 @@ export class ClientMainViewComponent implements OnInit {
|
|||
partitions: any[] = [];
|
||||
commands: any[] = [];
|
||||
chartDisk: any[] = [];
|
||||
view: [number, number] = [300, 200];
|
||||
view: [number, number] = [260, 160];
|
||||
showLegend: boolean = true;
|
||||
|
||||
arrayCommands: any[] = [
|
||||
{ name: 'Enceder', slug: 'power-on' },
|
||||
{ name: 'Apagar', slug: 'power-off' },
|
||||
{ name: 'Reiniciar', slug: 'reboot' },
|
||||
{ name: 'Iniciar Sesión', slug: 'login' },
|
||||
{ name: 'Crear imagen', slug: 'create-image' },
|
||||
{ name: 'Clonar/desplegar imagen', slug: 'deploy-image' },
|
||||
{ name: 'Eliminar Imagen Cache', slug: 'delete-image-cache' },
|
||||
{ name: 'Particionar y Formatear', slug: 'partition' },
|
||||
{ name: 'Inventario Software', slug: 'software-inventory' },
|
||||
{ name: 'Inventario Hardware', slug: 'hardware-inventory' },
|
||||
{ name: 'Ejecutar comando', slug: 'run-script' },
|
||||
];
|
||||
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'diskNumber',
|
||||
header: 'Disco',
|
||||
cell: (partition: any) => `${partition.diskNumber}`,
|
||||
cell: (partition: any) => partition.diskNumber,
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionNumber',
|
||||
header: 'Particion',
|
||||
cell: (partition: any) => `${partition.partitionNumber}`
|
||||
cell: (partition: any) => partition.partitionNumber
|
||||
},
|
||||
{
|
||||
columnDef: 'partitionCode',
|
||||
header: 'Tipo de partición',
|
||||
cell: (partition: any) => partition.partitionCode
|
||||
},
|
||||
{
|
||||
columnDef: 'description',
|
||||
header: 'Sistema de ficheros',
|
||||
cell: (partition: any) => `${partition.filesystem}`
|
||||
cell: (partition: any) => partition.filesystem
|
||||
},
|
||||
{
|
||||
columnDef: 'size',
|
||||
|
@ -76,13 +68,13 @@ export class ClientMainViewComponent implements OnInit {
|
|||
{
|
||||
columnDef: 'memoryUsage',
|
||||
header: 'Uso',
|
||||
cell: (partition: any) => `${partition.memoryUsage} %`
|
||||
cell: (partition: any) => `${partition.memoryUsage}%`
|
||||
},
|
||||
{
|
||||
columnDef: 'operativeSystem',
|
||||
header: 'SO',
|
||||
cell: (partition: any) => `${partition.operativeSystem?.name}`
|
||||
},
|
||||
header: 'SO/Imagen',
|
||||
cell: (partition: any) => partition.operativeSystem?.name
|
||||
}
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef)];
|
||||
isDiskUsageEmpty: boolean = true;
|
||||
|
@ -93,7 +85,8 @@ export class ClientMainViewComponent implements OnInit {
|
|||
private dialog: MatDialog,
|
||||
private configService: ConfigService,
|
||||
private router: Router,
|
||||
private toastService: ToastrService
|
||||
private toastService: ToastrService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { clientData: any }
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
const url = window.location.href;
|
||||
|
@ -102,31 +95,16 @@ export class ClientMainViewComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.clientData = history.state.clientData['@id'];
|
||||
this.loadClient(this.clientData)
|
||||
this.loadCommands()
|
||||
this.calculateDiskUsage();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
|
||||
loadClient = (uuid: string) => {
|
||||
this.http.get<any>(`${this.baseUrl}${uuid}`).subscribe({
|
||||
next: data => {
|
||||
this.clientData = data;
|
||||
this.updateGeneralData();
|
||||
this.updateNetworkData();
|
||||
this.loadPartitions()
|
||||
this.loading = false;
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error al obtener el cliente:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
navigateToGroups() {
|
||||
this.router.navigate(['/groups']);
|
||||
if (this.data && this.data.clientData) {
|
||||
this.clientData = this.data.clientData;
|
||||
this.updateGeneralData();
|
||||
this.updateNetworkData();
|
||||
this.calculateDiskUsage();
|
||||
this.loadPartitions();
|
||||
this.loading = false;
|
||||
} else {
|
||||
console.error('No se recibieron datos del cliente.');
|
||||
}
|
||||
}
|
||||
|
||||
updateGeneralData() {
|
||||
|
@ -143,7 +121,7 @@ export class ClientMainViewComponent implements OnInit {
|
|||
updateNetworkData() {
|
||||
this.networkData = [
|
||||
{ property: 'Padre', value: this.clientData?.organizationalUnit?.name || '' },
|
||||
{ property: 'Pxe', value: this.clientData?.template?.name || '' },
|
||||
{ property: 'Pxe', value: this.clientData?.pxeTemplate?.name || '' },
|
||||
{ property: 'Remote Pc', value: this.clientData.remotePc || '' },
|
||||
{ property: 'Subred', value: this.clientData?.subnet || '' },
|
||||
{ property: 'OGlive', value: this.clientData?.ogLive?.name || '' },
|
||||
|
@ -198,16 +176,14 @@ export class ClientMainViewComponent implements OnInit {
|
|||
this.isDiskUsageEmpty = this.diskUsageData.length === 0;
|
||||
}
|
||||
|
||||
onEditClick(event: MouseEvent, uuid: string): void {
|
||||
event.stopPropagation();
|
||||
const dialogRef = this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' });
|
||||
dialogRef.afterClosed().subscribe();
|
||||
}
|
||||
|
||||
loadPartitions(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[diskNumber, partitionNumber]=ASC`).subscribe({
|
||||
if (!this.clientData?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData.id}&order[diskNumber, partitionNumber]=ASC`).subscribe({
|
||||
next: data => {
|
||||
const filteredPartitions = data['hydra:member'].filter((partition: any) => partition.partitionNumber !== 0);
|
||||
const filteredPartitions = data['hydra:member'];
|
||||
this.dataSource = filteredPartitions;
|
||||
this.partitions = filteredPartitions;
|
||||
this.calculateDiskUsage();
|
||||
|
@ -218,96 +194,7 @@ export class ClientMainViewComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
loadCommands(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/commands?`).subscribe({
|
||||
next: data => {
|
||||
this.commands = data['hydra:member'];
|
||||
},
|
||||
error: error => {
|
||||
console.error('Error al obtener las particiones:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCommandSelect(action: any): void {
|
||||
if (action === 'partition') {
|
||||
this.openPartitionAssistant();
|
||||
}
|
||||
|
||||
if (action === 'create-image') {
|
||||
this.openCreateImageAssistant();
|
||||
}
|
||||
|
||||
if (action === 'deploy-image') {
|
||||
this.openDeployImageAssistant();
|
||||
}
|
||||
|
||||
if (action === 'reboot') {
|
||||
this.rebootClient();
|
||||
}
|
||||
|
||||
if (action === 'power-off') {
|
||||
this.powerOffClient();
|
||||
}
|
||||
|
||||
if (action === 'power-on') {
|
||||
this.powerOnClient();
|
||||
}
|
||||
}
|
||||
|
||||
rebootClient(): void {
|
||||
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/reboot`, {}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
powerOnClient(): void {
|
||||
const payload = {
|
||||
client: this.clientData['@id']
|
||||
}
|
||||
|
||||
this.http.post(`${this.baseUrl}${this.clientData.repository['@id']}/wol`, payload).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
powerOffClient(): void {
|
||||
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/power-off`, {}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
openPartitionAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/partition-assistant`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
||||
openCreateImageAssistant(): void {
|
||||
this.router.navigate([`/clients/${this.clientData.uuid}/create-image`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
}
|
||||
|
||||
openDeployImageAssistant(): void {
|
||||
this.router.navigate([`/clients/deploy-image`]).then(r => {
|
||||
console.log('navigated', r);
|
||||
});
|
||||
onNoClick(): void {
|
||||
this.dialog.closeAll();
|
||||
}
|
||||
}
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="template">
|
||||
<mat-select formControlName="pxeTemplate">
|
||||
<mat-option *ngFor="let template of templates" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
</mat-option>
|
||||
|
|
|
@ -86,7 +86,7 @@ export class ManageClientComponent implements OnInit {
|
|||
netDriver: null,
|
||||
mac: ['', Validators.pattern(/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/)],
|
||||
ip: ['', Validators.required],
|
||||
template: [null],
|
||||
pxeTemplate: [null],
|
||||
hardwareProfile: [null],
|
||||
ogLive: [null],
|
||||
repository: [null],
|
||||
|
@ -110,7 +110,8 @@ export class ManageClientComponent implements OnInit {
|
|||
repository: unit.networkSettings?.repository?.['@id'],
|
||||
hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'],
|
||||
ogLive: unit.networkSettings?.ogLive?.['@id'],
|
||||
menu: unit.networkSettings?.menu?.['@id']
|
||||
menu: unit.networkSettings?.menu?.['@id'],
|
||||
pxeTemplate: unit.networkSettings?.pxeTemplate?.['@id'],
|
||||
}));
|
||||
|
||||
const initialUnitId = this.clientForm.get('organizationalUnit')?.value;
|
||||
|
@ -229,6 +230,7 @@ export class ManageClientComponent implements OnInit {
|
|||
ogLive: selectedUnit.ogLive || null,
|
||||
menu: selectedUnit.menu || null,
|
||||
netiface: selectedUnit.netiface || null,
|
||||
pxeTemplate: selectedUnit.pxeTemplate || null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -250,7 +252,7 @@ export class ManageClientComponent implements OnInit {
|
|||
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null,
|
||||
repository: data.repository ? data.repository['@id'] : null,
|
||||
ogLive: data.ogLive ? data.ogLive['@id'] : null,
|
||||
template: data.template ? data.template['@id'] : null,
|
||||
pxeTemplate: data.pxeTemplate ? data.pxeTemplate['@id'] : null,
|
||||
menu: data.menu ? data.menu['@id'] : null,
|
||||
maintenance: data.maintenance
|
||||
});
|
||||
|
|
|
@ -22,11 +22,11 @@
|
|||
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon style="color: green;">school</mat-icon>
|
||||
<div matListItemTitle>Disponible acceso remoto</div>
|
||||
<div matListItemTitle>{{ 'remoteAccess' | translate }}</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item>
|
||||
<mat-icon matListItemIcon style="color: rgb(209, 5, 5);">school</mat-icon>
|
||||
<div matListItemTitle>No disponible acceso remoto</div>
|
||||
<div matListItemTitle>{{ 'noRemoteAccess' | translate }}</div>
|
||||
</mat-list-item>
|
||||
|
||||
</mat-list>
|
||||
</mat-list>
|
|
@ -1,12 +1,13 @@
|
|||
<div class="create-ou-container">
|
||||
<h1 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Crear' }} Unidad Organizativa</h1>
|
||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
||||
<h1 mat-dialog-title>{{ isEditMode ? ('edit' | translate) : ('createButton' | translate) }} {{
|
||||
'labelOrganizationalUnit' | translate }}</h1>
|
||||
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
|
||||
<!-- Paso 1: General -->
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<span *ngIf="!loading" class="step-title">General</span>
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<span *ngIf="!loading" class="step-title">{{ 'generalTabLabel' | translate }}</span>
|
||||
<form *ngIf="generalFormGroup && !loading" [formGroup]="generalFormGroup" class="grid-form">
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>Tipo</mat-label>
|
||||
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="type" required>
|
||||
<mat-option *ngFor="let type of filteredTypes" [value]="type">
|
||||
{{ typeTranslations[type] }}
|
||||
|
@ -14,11 +15,11 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>Nombre</mat-label>
|
||||
<mat-label>{{ 'nameColumnHeader' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill">
|
||||
<mat-label>Padre</mat-label>
|
||||
<mat-label>{{ 'createOrgUnitparentLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="parent" (selectionChange)="onParentChange($event)">
|
||||
<mat-select-trigger>
|
||||
{{ getSelectedParentName() }}
|
||||
|
@ -31,7 +32,7 @@
|
|||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field description-form-field" appearance="fill">
|
||||
<mat-label>Descripción</mat-label>
|
||||
<mat-label>{{ 'descriptionLabel' |translate }}</mat-label>
|
||||
<textarea matInput formControlName="description"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
|
@ -41,22 +42,23 @@
|
|||
</form>
|
||||
|
||||
<!-- Paso 2: Información del Aula -->
|
||||
<span *ngIf="generalFormGroup.value.type === 'classroom' && !loading" class="step-title">Información del aula</span>
|
||||
<span *ngIf="generalFormGroup.value.type === 'classroom' && !loading"
|
||||
class="step-title">{{'classroomInfoStepLabel' | translate}}</span>
|
||||
<form *ngIf="generalFormGroup.value.type === 'classroom' && !loading" class="grid-form"
|
||||
[formGroup]="classroomInfoFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Localización</mat-label>
|
||||
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="location">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Aforo</mat-label>
|
||||
<mat-label>{{ 'capacityLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="capacity" type="number" min="0">
|
||||
<mat-error *ngIf="classroomInfoFormGroup.get('capacity')?.hasError('min')">
|
||||
El aforo no puede ser negativo
|
||||
{{ 'capacityWarning' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field" appearance="fill" style="grid-column: span 1;">
|
||||
<mat-label>Calendario Asociado</mat-label>
|
||||
<mat-label>{{ 'associatedCalendarLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
|
||||
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
|
||||
{{ calendar.name }}
|
||||
|
@ -64,13 +66,13 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div class="projector-board-field">
|
||||
<mat-slide-toggle formControlName="projector">Proyector</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board">Pizarra</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="projector">{{ 'projectorAlt' | translate }}</mat-slide-toggle>
|
||||
<mat-slide-toggle formControlName="board">{{ 'boardToggle' | translate }}</mat-slide-toggle>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Paso 3: Configuración de Red -->
|
||||
<span *ngIf="!loading" class="step-title">Configuración de Red</span>
|
||||
<span *ngIf="!loading" class="step-title">{{ 'networkSettingsStepLabel' | translate }}</span>
|
||||
<form *ngIf="networkSettingsFormGroup && !loading" [formGroup]="networkSettingsFormGroup" class="grid-form">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>OgLive</mat-label>
|
||||
|
@ -81,7 +83,15 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Repositorio</mat-label>
|
||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="pxeTemplate" (selectionChange)="onPxeTemplateChange($event)">
|
||||
<mat-option *ngFor="let template of pxeTemplates" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
|
@ -97,17 +107,17 @@
|
|||
<input matInput formControlName="dns">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Máscara de Red</mat-label>
|
||||
<mat-label>{{ 'netmaskLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="netmask">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@netiface-label">Interfaz de red</mat-label>
|
||||
<mat-select formControlName="netiface">
|
||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@netiface-label">{{ 'netifaceLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netiface">
|
||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Router</mat-label>
|
||||
<input matInput formControlName="router">
|
||||
|
@ -117,7 +127,7 @@
|
|||
<input matInput formControlName="ntp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Modo P2P</mat-label>
|
||||
<mat-label>{{ 'p2pModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
|
@ -125,23 +135,23 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Tiempo P2P</mat-label>
|
||||
<mat-label>{{ 'p2pTimeLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="p2pTime" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast IP</mat-label>
|
||||
<mat-label>{{ 'mcastIpLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastIp">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast Speed</mat-label>
|
||||
<mat-label>{{ 'mcastSpeedLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastSpeed" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast Port</mat-label>
|
||||
<mat-label>{{ 'mcastPortLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="mcastPort" type="number">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Mcast Mode</mat-label>
|
||||
<mat-label>{{ 'mcastModeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="mcastMode">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
|
@ -149,7 +159,7 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Menu</mat-label>
|
||||
<mat-label>{{ 'menuLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="menu">
|
||||
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
|
||||
{{ menu.name }}
|
||||
|
@ -157,28 +167,28 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Perfil de Hardware</mat-label>
|
||||
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="hardwareProfile">
|
||||
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description
|
||||
}}</mat-option>
|
||||
</mat-select>
|
||||
<mat-error>Formato de URL incorrecto</mat-error>
|
||||
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
||||
<!-- Paso 4: Información Adicional -->
|
||||
<span *ngIf="!loading" class="step-title">Información Adicional</span>
|
||||
<span *ngIf="!loading" class="step-title">{{ 'additionalInfoStepLabel' | translate }}</span>
|
||||
<form *ngIf="additionalInfoFormGroup && !loading" [formGroup]="additionalInfoFormGroup">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Comentarios</mat-label>
|
||||
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="comments"></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mat-dialog-actions">
|
||||
<button class="ordinary-button" (click)="onNoClick()">Cancelar</button>
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()"
|
||||
[disabled]="!generalFormGroup.valid || !additionalInfoFormGroup.valid || !networkSettingsFormGroup.valid">{{
|
||||
isEditMode ? 'Editar' : 'Crear' }}</button>
|
||||
isEditMode ? ('edit' | translate) : ('createButton' | translate) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -30,6 +30,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
isEditMode: boolean;
|
||||
currentCalendar: any = [];
|
||||
ogLives: any[] = [];
|
||||
pxeTemplates: any[] = [];
|
||||
menus: any[] = [];
|
||||
repositories: any[] = [];
|
||||
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||
|
@ -77,6 +78,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
this.networkSettingsFormGroup = this._formBuilder.group({
|
||||
ogLive: [null],
|
||||
repository: [null],
|
||||
pxeTemplate: [null],
|
||||
proxy: [null],
|
||||
dns: [null],
|
||||
netmask: [null],
|
||||
|
@ -111,6 +113,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
this.loadOgLives(),
|
||||
this.loadRepositories(),
|
||||
this.loadMenus(),
|
||||
this.loadPxeTemplates()
|
||||
];
|
||||
|
||||
Promise.all(observables).then(() => {
|
||||
|
@ -144,6 +147,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
repository: unit.networkSettings?.repository?.['@id'],
|
||||
hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'],
|
||||
ogLive: unit.networkSettings?.ogLive?.['@id'],
|
||||
pxeTemplate: unit.networkSettings?.pxeTemplate?.['@id'],
|
||||
menu: unit.networkSettings?.menu?.['@id'],
|
||||
mcastIp: unit.networkSettings?.mcastIp,
|
||||
mcastSpeed: unit.networkSettings?.mcastSpeed,
|
||||
|
@ -183,6 +187,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
repository: selectedUnit.repository || null,
|
||||
hardwareProfile: selectedUnit.hardwareProfile || null,
|
||||
ogLive: selectedUnit.ogLive || null,
|
||||
pxeTemplate: selectedUnit.pxeTemplate || null,
|
||||
menu: selectedUnit.menu || null,
|
||||
mcastIp: selectedUnit.mcastIp || null,
|
||||
mcastSpeed: selectedUnit.mcastSpeed || null,
|
||||
|
@ -219,6 +224,23 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
loadPxeTemplates(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.pxeTemplates = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching pxe templates:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadOgLives(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`;
|
||||
|
@ -287,22 +309,6 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
loadCurrentCalendar(uuid: string): void {
|
||||
this.loading = true;
|
||||
const apiUrl = `${this.baseUrl}/remote-calendars/${uuid}`;
|
||||
this.http.get<any>(apiUrl).subscribe(
|
||||
response => {
|
||||
this.currentCalendar = response;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error loading current calendar', error);
|
||||
this.toastService.error('Error loading current calendar');
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onCalendarChange(event: any) {
|
||||
this.generalFormGroup.value.remoteCalendar = event.value;
|
||||
}
|
||||
|
@ -315,6 +321,10 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
this.networkSettingsFormGroup.value.repository = event.value;
|
||||
}
|
||||
|
||||
onPxeTemplateChange(event: any) {
|
||||
this.networkSettingsFormGroup.value.pxeTemplate = event.value;
|
||||
}
|
||||
|
||||
loadData(uuid: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/organizational-units/${uuid}`;
|
||||
|
@ -347,7 +357,8 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
menu: data.networkSettings?.menu ? data.networkSettings.menu['@id'] : null,
|
||||
hardwareProfile: data.networkSettings?.hardwareProfile ? data.networkSettings.hardwareProfile['@id'] : null,
|
||||
ogLive: data.networkSettings?.ogLive ? data.networkSettings.ogLive['@id'] : null,
|
||||
repository: data.networkSettings?.repository ? data.networkSettings.repository['@id'] : null
|
||||
repository: data.networkSettings?.repository ? data.networkSettings.repository['@id'] : null,
|
||||
pxeTemplate: data.networkSettings?.pxeTemplate ? data.networkSettings.pxeTemplate['@id'] : null
|
||||
});
|
||||
this.classroomInfoFormGroup.patchValue({
|
||||
location: data.location,
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
.modal-content {
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1em 4em 2em 4em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.client-section {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.client-title {
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
color: #3f51b5;
|
||||
margin-bottom: 1em;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.partition-table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.partition-table th,
|
||||
.partition-table td {
|
||||
padding: 0.75em 1em;
|
||||
text-align: left;
|
||||
font-size: 0.95em;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.partition-table th {
|
||||
background-color: #ebebeb;
|
||||
font-weight: 500;
|
||||
color: #252525;
|
||||
}
|
||||
|
||||
.partition-table tr:hover td {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
font-weight: 500;
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
|
||||
.partition-table tr:hover td {
|
||||
background-color: unset;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<mat-dialog-content class="modal-content">
|
||||
<div *ngFor="let group of groupedPartitions" class="client-section">
|
||||
<h3 class="client-title">
|
||||
{{ group.clientNames.length === 1 ? group.clientNames[0] : group.clientNames.join(', ') }}
|
||||
</h3>
|
||||
|
||||
<ng-container *ngIf="group.partitions.length > 0; else noPartitions">
|
||||
<table mat-table [dataSource]="group.partitions" class="mat-elevation-z2 partition-table">
|
||||
<!-- Columnas -->
|
||||
<ng-container matColumnDef="diskNumber">
|
||||
<th mat-header-cell *matHeaderCellDef>Disco</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.diskNumber }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="partitionNumber">
|
||||
<th mat-header-cell *matHeaderCellDef>Partición</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.partitionNumber }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="partitionCode">
|
||||
<th mat-header-cell *matHeaderCellDef>Tipo</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.partitionCode }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="size">
|
||||
<th mat-header-cell *matHeaderCellDef>Tamaño (MB)</th>
|
||||
<td mat-cell *matCellDef="let element">{{ element.size | number }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="filesystem">
|
||||
<th mat-header-cell *matHeaderCellDef>File System</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<span *ngIf="!element.isSummary; else summaryFs">{{ element.filesystem || '-' }}</span>
|
||||
<ng-template #summaryFs><em class="summary-label">Resumen</em></ng-template>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [ngClass]="{ 'summary-row': row.isSummary }"></tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #noPartitions>
|
||||
<p class="no-partitions-message">Sin particiones disponibles.</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
</mat-dialog-content>
|
|
@ -0,0 +1,42 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { PartitionTypeOrganizatorComponent } from './partition-type-organizator.component';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
|
||||
describe('PartitionTypeOrganizatorComponent', () => {
|
||||
let component: PartitionTypeOrganizatorComponent;
|
||||
let fixture: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [PartitionTypeOrganizatorComponent],
|
||||
imports: [
|
||||
MatDialogModule,
|
||||
MatTableModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: [
|
||||
{
|
||||
name: 'Client 1',
|
||||
partitions: [
|
||||
{ diskNumber: 1, partitionNumber: 1, partitionCode: 'EXT4', size: 1024, filesystem: 'ext4', memoryUsage: 50 },
|
||||
{ diskNumber: 1, partitionNumber: 2, partitionCode: 'NTFS', size: 2048, filesystem: 'ntfs', memoryUsage: 75 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PartitionTypeOrganizatorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
interface Partition {
|
||||
diskNumber: number;
|
||||
partitionNumber: number;
|
||||
partitionCode: string;
|
||||
size: number;
|
||||
filesystem: string | null;
|
||||
isSummary?: boolean;
|
||||
}
|
||||
|
||||
interface SimplifiedClient {
|
||||
name: string;
|
||||
partitions: Partition[];
|
||||
}
|
||||
|
||||
interface GroupedClientPartitions {
|
||||
clientNames: string[];
|
||||
partitions: Partition[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-partition-type-organizator',
|
||||
templateUrl: './partition-type-organizator.component.html',
|
||||
styleUrl: './partition-type-organizator.component.css'
|
||||
})
|
||||
export class PartitionTypeOrganizatorComponent implements OnInit {
|
||||
displayedColumns: string[] = ['diskNumber', 'partitionNumber', 'partitionCode', 'size', 'filesystem'];
|
||||
groupedPartitions: GroupedClientPartitions[] = [];
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public data: SimplifiedClient[]) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const simplifiedClients = this.simplifyClients(this.data);
|
||||
this.groupedPartitions = this.groupClientsByPartitions(simplifiedClients);
|
||||
}
|
||||
|
||||
private simplifyClients(clients: SimplifiedClient[]): SimplifiedClient[] {
|
||||
return clients.map(client => {
|
||||
const partitionZero = client.partitions.find(p => p.partitionNumber === 0);
|
||||
const otherPartitions = client.partitions.filter(p => p.partitionNumber !== 0);
|
||||
|
||||
const simplifiedPartitions: Partition[] = otherPartitions.map(p => ({
|
||||
diskNumber: p.diskNumber,
|
||||
partitionNumber: p.partitionNumber,
|
||||
partitionCode: p.partitionCode,
|
||||
size: p.size,
|
||||
filesystem: p.filesystem,
|
||||
isSummary: false
|
||||
}));
|
||||
|
||||
if (partitionZero) {
|
||||
simplifiedPartitions.push({
|
||||
diskNumber: partitionZero.diskNumber,
|
||||
partitionNumber: partitionZero.partitionNumber,
|
||||
partitionCode: partitionZero.partitionCode,
|
||||
size: partitionZero.size,
|
||||
filesystem: null,
|
||||
isSummary: true
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: client.name,
|
||||
partitions: simplifiedPartitions
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private groupClientsByPartitions(clients: SimplifiedClient[]): GroupedClientPartitions[] {
|
||||
const groups: GroupedClientPartitions[] = [];
|
||||
|
||||
clients.forEach(client => {
|
||||
const normalizedPartitions = this.normalizePartitions(client.partitions);
|
||||
|
||||
const existingGroup = groups.find(group =>
|
||||
JSON.stringify(this.normalizePartitions(group.partitions)) === JSON.stringify(normalizedPartitions)
|
||||
);
|
||||
|
||||
if (existingGroup) {
|
||||
existingGroup.clientNames.push(client.name);
|
||||
} else {
|
||||
groups.push({
|
||||
clientNames: [client.name],
|
||||
partitions: client.partitions
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private normalizePartitions(partitions: Partition[]): Partition[] {
|
||||
return [...partitions].sort((a, b) => a.partitionNumber - b.partitionNumber);
|
||||
}
|
||||
}
|
|
@ -69,4 +69,4 @@
|
|||
<button class="ordinary-button" (click)="close()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" (click)="saveImage()" [disabled]="loading">{{ 'saveButton' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -51,21 +51,3 @@
|
|||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.example-headers-align .mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.example-headers-align .mat-mdc-form-field+.mat-mdc-form-field {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.example-button-row {
|
||||
display: table-cell;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.example-button-row .mat-mdc-button-base {
|
||||
margin: 8px 8px 8px 0;
|
||||
}
|
|
@ -37,7 +37,7 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="{{ 'tableDescription' | translate }}">
|
||||
<ng-container matColumnDef="id">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'idColumnHeader' | translate }}</mat-header-cell>
|
||||
|
@ -50,12 +50,12 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="ip">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'nameColumnHeader' | translate }}</mat-header-cell>
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'ipLabel' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element"> {{ element.ip }} </mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="mac">
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'nameColumnHeader' | translate }}</mat-header-cell>
|
||||
<mat-header-cell *matHeaderCellDef>{{ 'macLabel' | translate }}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element"> {{ element.mac }} </mat-cell>
|
||||
</ng-container>
|
||||
|
||||
|
@ -77,4 +77,4 @@
|
|||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||
</mat-table>
|
||||
</table>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { HttpClient } from '@angular/common/http';
|
|||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
|
||||
@Component({
|
||||
selector: 'app-pxe-boot-files',
|
||||
|
@ -15,7 +16,7 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
|
||||
availableOrganizationalUnits: any[] = [];
|
||||
selectedUnitChildren: any[] = [];
|
||||
dataSource: any[] = [];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
taskForm: FormGroup;
|
||||
units: any[] = [];
|
||||
ogLiveOptions: any[] = [];
|
||||
|
@ -72,14 +73,14 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
loadChildUnits(event: any) {
|
||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${event.value.id}`).subscribe(
|
||||
response => {
|
||||
this.dataSource = response['hydra:member'];
|
||||
this.dataSource.data = response['hydra:member'];
|
||||
},
|
||||
error => console.error('Error fetching child units:', error)
|
||||
);
|
||||
}
|
||||
|
||||
applyToAll(): void {
|
||||
this.dataSource = this.dataSource.map(client => ({
|
||||
this.dataSource.data = this.dataSource.data.map(client => ({
|
||||
...client,
|
||||
ogLive: this.globalOgLive || client.ogLive
|
||||
}));
|
||||
|
@ -88,7 +89,7 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
saveOgLiveTemplates(): void {
|
||||
const groupedByTemplate: { [key: string]: string[] } = {};
|
||||
|
||||
this.dataSource.forEach(client => {
|
||||
this.dataSource.data.forEach(client => {
|
||||
if (client.ogLive) {
|
||||
if (!groupedByTemplate[client.ogLive]) {
|
||||
groupedByTemplate[client.ogLive] = [];
|
||||
|
@ -107,10 +108,10 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
|
||||
this.http.post(url, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(`Clientes guardados correctamente para la plantilla ${templateId}`);
|
||||
this.toastService.success(`Clientes guardados correctamente para la plantilla`);
|
||||
},
|
||||
error: () => {
|
||||
this.toastService.error(`Error al guardar clientes para la plantilla ${templateId}`);
|
||||
this.toastService.error(`Error al guardar clientes para la plantilla`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" (click)="openSubnetInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openSubnetInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
<button class="action-button" (click)="addImage()" joyrideStep="addImageStep"
|
||||
[text]="'addOgLiveButtonDescription' | translate">
|
||||
{{ 'addOgLiveButton' | translate }}
|
||||
|
@ -57,14 +57,14 @@
|
|||
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef === 'isDefault'">
|
||||
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
|
||||
<ng-container *ngIf="image[column.columnDef]; else cancelIcon">
|
||||
{{ 'checkCircle' | translate }}
|
||||
<mat-chip>
|
||||
<ng-container *ngIf="image.isDefault">
|
||||
{{ 'yesOption' | translate }}
|
||||
</ng-container>
|
||||
<ng-template #cancelIcon>
|
||||
{{ 'cancelIcon' | translate }}
|
||||
</ng-template>
|
||||
</mat-icon>
|
||||
<ng-container *ngIf="!image.isDefault">
|
||||
{{ 'noOption' | translate }}
|
||||
</ng-container>
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'downloadUrl'">
|
||||
|
@ -99,9 +99,6 @@
|
|||
<button mat-icon-button color="info" (click)="showOgLive($event, image)">
|
||||
<mat-icon>{{ 'viewIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button disabled color="primary" (click)="editImage(image)">
|
||||
<mat-icon>{{ 'editIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteImage(image)">
|
||||
<mat-icon>{{ 'deleteIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
|
|
|
@ -25,8 +25,8 @@ export class PXEimagesComponent implements OnInit {
|
|||
images: { downloadUrl: string; name: string; uuid: string }[] = [];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 10;
|
||||
page: number = 1;
|
||||
itemsPerPage: number = 20;
|
||||
page: number = 0;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||
selectedElements: string[] = [];
|
||||
loading: boolean = false;
|
||||
|
@ -94,12 +94,15 @@ export class PXEimagesComponent implements OnInit {
|
|||
}
|
||||
|
||||
search(): void {
|
||||
this.dataService.getImages(this.filters).subscribe(
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
|
||||
data => {
|
||||
this.dataSource.data = data;
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching og lives', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -160,19 +163,6 @@ export class PXEimagesComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
editImage(image: any): void {
|
||||
const dialogRef = this.dialog.open(CreatePXEImageComponent, {
|
||||
width: '700px',
|
||||
data: image
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteImage(image: any): void {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '400px',
|
||||
|
@ -203,22 +193,11 @@ export class PXEimagesComponent implements OnInit {
|
|||
const dialogRef = this.dialog.open(InfoImageComponent, { data: { data }, width: '700px' });
|
||||
}
|
||||
|
||||
applyFilter() {
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({
|
||||
next: (response) => {
|
||||
this.dataSource.data = response['hydra:member'];
|
||||
this.length = response['hydra:totalItems'];
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al cargar las imágenes:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent) {
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.applyFilter();
|
||||
this.length = event.length;
|
||||
this.search();
|
||||
}
|
||||
|
||||
loadAlert(): Observable<any> {
|
||||
|
@ -248,6 +227,7 @@ export class PXEimagesComponent implements OnInit {
|
|||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'titleStep',
|
||||
'viewInfoStep',
|
||||
'addImageStep',
|
||||
'searchNameStep',
|
||||
'searchDefaultImageStep',
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
<h2 mat-dialog-title>{{ isEditMode ? ('editTemplateTitle' | translate) : ('addTemplateTitle' | translate) }}</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="spacing-container">
|
||||
<form [formGroup]="templateForm" (ngSubmit)="onSave()">
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>{{ 'templateNameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" [placeholder]="'templateNamePlaceholder' | translate">
|
||||
<mat-error *ngIf="templateForm.get('name')?.hasError('required')">
|
||||
{{ 'templateNameError' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>{{ 'templateContentLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="templateContent" rows="20"
|
||||
[placeholder]="'templateContentPlaceholder' | translate"></textarea>
|
||||
<mat-error *ngIf="templateForm.get('templateContent')?.hasError('required')">
|
||||
{{ 'templateContentError' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions>
|
||||
<div class="actions-container">
|
||||
<button class="action-button" [matMenuTriggerFor]="templateMenu">
|
||||
{{ 'loadTemplateModelButton' | translate }}
|
||||
</button>
|
||||
<mat-menu #templateMenu="matMenu">
|
||||
<button mat-menu-item (click)="loadTemplateModel('ogLive')">{{ 'ogLiveModel' | translate }}</button>
|
||||
<button mat-menu-item (click)="loadTemplateModel('disco')">{{ 'diskModel' | translate }}</button>
|
||||
</mat-menu>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="ordinary-button" (click)="onCancel()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSave()" [disabled]="!templateForm.valid">
|
||||
{{ isEditMode ? ('updateButton' | translate) : ('createButton' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-actions>
|
|
@ -1,6 +1,3 @@
|
|||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #eceff1;
|
||||
|
@ -12,41 +9,17 @@ pre {
|
|||
color: #333;
|
||||
}
|
||||
|
||||
mat-dialog-actions {
|
||||
margin-top: 20px;
|
||||
|
||||
.dialog-content {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
background-color: #3f51b5;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button[type="submit"]:disabled {
|
||||
background-color: #c5cae9;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5rem;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 30px;
|
||||
font-size: 1.2rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.spacing-container {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 16px;
|
||||
.pxe-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
|
@ -56,6 +29,11 @@ h3 {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
flex-grow: 1;
|
||||
margin-right: 16px;
|
||||
|
@ -72,13 +50,11 @@ h3 {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-right: 1rem;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.action-buttons {
|
|
@ -0,0 +1,44 @@
|
|||
<h2 mat-dialog-title>{{ isEditMode ? ('editTemplateTitle' | translate) : ('addTemplateTitle' | translate) }}</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<form [formGroup]="templateForm" (ngSubmit)="onSave()" class="pxe-form">
|
||||
<mat-form-field appearance="fill" class="form-field">
|
||||
<mat-label>{{ 'templateNameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" [placeholder]="'templateNamePlaceholder' | translate">
|
||||
<mat-error *ngIf="templateForm.get('name')?.hasError('required')">
|
||||
{{ 'templateNameError' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="form-field">
|
||||
<mat-label>{{ 'templateContent' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="templateContent" rows="20"
|
||||
[placeholder]="'templateContentPlaceholder' | translate"></textarea>
|
||||
<mat-error *ngIf="templateForm.get('templateContent')?.hasError('required')">
|
||||
{{ 'templateContentError' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox formControlName="isDefault">
|
||||
{{ 'isDefaultLabel' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="action-button" [matMenuTriggerFor]="templateMenu">
|
||||
{{ 'loadTemplateModelButton' | translate }}
|
||||
</button>
|
||||
<mat-menu #templateMenu="matMenu">
|
||||
<button mat-menu-item (click)="loadTemplateModel('ogLive')">{{ 'ogLiveModel' | translate }}</button>
|
||||
<button mat-menu-item (click)="loadTemplateModel('disco')">{{ 'diskModel' | translate }}</button>
|
||||
</mat-menu>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="ordinary-button" (click)="onCancel()">{{ 'cancelButton' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSave()" [disabled]="!templateForm.valid">
|
||||
{{ isEditMode ? ('saveButton' | translate) : ('saveButton' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -20,35 +20,35 @@ export class CreatePxeTemplateComponent implements OnInit {
|
|||
|
||||
templateModels = {
|
||||
ogLive: `#!ipxe
|
||||
set timeout 0
|
||||
set timeout-style hidden
|
||||
set ISODIR __OGLIVE__
|
||||
set default 0
|
||||
set kernelargs __INFOHOST__
|
||||
:try_iso
|
||||
kernel http://__SERVERIP__/tftpboot/\${ISODIR}/ogvmlinuz \${kernelargs} || goto fallback
|
||||
initrd http://__SERVERIP__/tftpboot/\${ISODIR}/oginitrd.img
|
||||
boot
|
||||
set timeout 0
|
||||
set timeout-style hidden
|
||||
set ISODIR __OGLIVE__
|
||||
set default 0
|
||||
set kernelargs __INFOHOST__
|
||||
:try_iso
|
||||
kernel http://__SERVERIP__/tftpboot/\${ISODIR}/ogvmlinuz \${kernelargs} || goto fallback
|
||||
initrd http://__SERVERIP__/tftpboot/\${ISODIR}/oginitrd.img
|
||||
boot
|
||||
|
||||
:fallback
|
||||
set ISODIR ogLive
|
||||
kernel http://__SERVERIP__/tftpboot/\${ISODIR}/ogvmlinuz \${kernelargs}
|
||||
initrd http://__SERVERIP__/tftpboot/\${ISODIR}/oginitrd.img
|
||||
boot`,
|
||||
:fallback
|
||||
set ISODIR ogLive
|
||||
kernel http://__SERVERIP__/tftpboot/\${ISODIR}/ogvmlinuz \${kernelargs}
|
||||
initrd http://__SERVERIP__/tftpboot/\${ISODIR}/oginitrd.img
|
||||
boot`,
|
||||
|
||||
disco: `#!ipxe
|
||||
disco: `#!ipxe
|
||||
|
||||
iseq \${platform} efi && goto uefi_boot || goto bios_boot
|
||||
iseq \${platform} efi && goto uefi_boot || goto bios_boot
|
||||
|
||||
:bios_boot
|
||||
echo "Running in BIOS mode - Booting first disk"
|
||||
chain http://__SERVERIP__/tftpboot/grub.exe --config-file="title FirstHardDisk;chainloader (hd0)+1;rootnoverify (hd0);boot" || echo "Failed to boot in BIOS mode"
|
||||
exit
|
||||
:bios_boot
|
||||
echo "Running in BIOS mode - Booting first disk"
|
||||
chain http://__SERVERIP__/tftpboot/grub.exe --config-file="title FirstHardDisk;chainloader (hd0)+1;rootnoverify (hd0);boot" || echo "Failed to boot in BIOS mode"
|
||||
exit
|
||||
|
||||
:uefi_boot
|
||||
echo "Running in UEFI mode - Booting first disk"
|
||||
sanboot --no-describe --drive 0 --filename \\EFI\\grub\\Boot\\grubx64.efi || echo "Failed to boot in UEFI mode"
|
||||
exit`
|
||||
:uefi_boot
|
||||
echo "Running in UEFI mode - Booting first disk"
|
||||
sanboot --no-describe --drive 0 --filename \\EFI\\grub\\Boot\\grubx64.efi || echo "Failed to boot in UEFI mode"
|
||||
exit`
|
||||
};
|
||||
|
||||
constructor(
|
||||
|
@ -72,7 +72,8 @@ exit`
|
|||
|
||||
this.templateForm = this.fb.group({
|
||||
name: [this.data?.name || '', Validators.required],
|
||||
templateContent: [this.data?.templateContent || '', Validators.required]
|
||||
templateContent: [this.data?.templateContent || '', Validators.required],
|
||||
isDefault: [this.data?.isDefault || false]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -99,7 +100,8 @@ exit`
|
|||
const formValues = this.templateForm.value;
|
||||
const payload = {
|
||||
name: formValues.name,
|
||||
templateContent: formValues.templateContent
|
||||
templateContent: formValues.templateContent,
|
||||
isDefault: formValues.isDefault,
|
||||
};
|
||||
|
||||
this.http.post<any>(`${this.baseUrl}/pxe-templates`, payload).subscribe({
|
||||
|
@ -117,7 +119,8 @@ exit`
|
|||
const formValues = this.templateForm.value;
|
||||
const payload = {
|
||||
name: formValues.name,
|
||||
templateContent: formValues.templateContent
|
||||
templateContent: formValues.templateContent,
|
||||
isDefault: formValues.isDefault,
|
||||
};
|
||||
|
||||
this.http.patch<any>(`${this.baseUrl}/pxe-templates/${this.data.uuid}`, payload).subscribe({
|
||||
|
@ -138,42 +141,6 @@ exit`
|
|||
this.toastService.info(`Plantilla ${type} cargada.`);
|
||||
}
|
||||
|
||||
addClientToTemplate(client: any): void {
|
||||
const postData = {
|
||||
client: client['@id']
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/pxe-templates/${this.data.uuid}/sync-client`, postData).subscribe(
|
||||
() => {
|
||||
this.toastService.success('Clientes asignados correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteClient(client: any): void {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: client.name }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.http.post(`${this.baseUrl}/pxe-templates/${this.data.uuid}/delete-client`, { client: client['@id'] }).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Cliente eliminado exitosamente');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
error: error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<button mat-icon-button color="primary" (click)="initTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
|
@ -7,7 +7,7 @@
|
|||
translate }}</h2>
|
||||
</div>
|
||||
<div class="template-button-row">
|
||||
<button class="action-button" (click)="openInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
<button class="action-button" (click)="addPxeTemplate()" joyrideStep="addTemplateStep"
|
||||
text="{{ 'addTemplateButtonDescription' | translate }}">{{ 'addTemplateButton' | translate }}</button>
|
||||
</div>
|
||||
|
@ -22,10 +22,10 @@
|
|||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="searchSyncStep"
|
||||
text="{{ 'searchSyncDescription' | translate }}">
|
||||
<mat-label>{{ 'createdInOgbootLabel' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="filters['synchronized']" (selectionChange)="search()"
|
||||
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="searchIsDefaultStep"
|
||||
text="{{ 'searchIsDefaultText' | translate }}">
|
||||
<mat-label>{{ 'isDefaultLabel' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="filters['isDefault']" (selectionChange)="search()"
|
||||
placeholder="{{ 'selectOptionPlaceholder' | translate }}">
|
||||
<mat-option [value]="''">{{ 'allOption' | translate }}</mat-option>
|
||||
<mat-option [value]="true">{{ 'yesOption' | translate }}</mat-option>
|
||||
|
@ -36,16 +36,31 @@
|
|||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="{{ 'tableDescription' | translate }}">
|
||||
text="{{ 'tableDatePxeTemplateText' | translate }}">
|
||||
<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 === 'synchronized'">
|
||||
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
|
||||
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
|
||||
</mat-icon>
|
||||
<mat-chip>
|
||||
<ng-container *ngIf="image.synchronized">
|
||||
{{ 'yesOption' | translate }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!image.synchronized">
|
||||
{{ 'noOption' | translate }}
|
||||
</ng-container>
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef !== 'synchronized'">
|
||||
<ng-container *ngIf="column.columnDef === 'isDefault'">
|
||||
<mat-chip>
|
||||
<ng-container *ngIf="image.isDefault">
|
||||
{{ 'yesOption' | translate }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!image.isDefault">
|
||||
{{ 'noOption' | translate }}
|
||||
</ng-container>
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef !== 'synchronized' && column.columnDef !== 'isDefault'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
|
@ -60,7 +75,7 @@
|
|||
<button mat-icon-button color="primary" (click)="editPxeTemplate(template)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="toggleAction(template, 'delete')">
|
||||
<button mat-icon-button color="warn" [disabled]="template.isDefault" (click)="toggleAction(template, 'delete')">
|
||||
<mat-icon>{{ 'deleteIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import { CreatePxeTemplateComponent } from './create-pxeTemplate/create-pxe-template.component';
|
||||
import { CreatePxeTemplateComponent } from './manage-pxeTemplate/create-pxe-template.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { PageEvent } from '@angular/material/paginator';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { DataService } from './data.service';
|
||||
|
@ -26,8 +25,8 @@ export class PxeComponent implements OnInit{
|
|||
currentPage: number = 1;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 10;
|
||||
page: number = 1;
|
||||
itemsPerPage: number = 20;
|
||||
page: number = 0;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||
selectedElements: string[] = [];
|
||||
loading: boolean = false;
|
||||
|
@ -45,17 +44,22 @@ export class PxeComponent implements OnInit{
|
|||
{
|
||||
columnDef: 'name',
|
||||
header: 'Nombre de la plantilla',
|
||||
cell: (user: any) => `${user.name}`
|
||||
cell: (user: any) => user.name
|
||||
},
|
||||
{
|
||||
columnDef: 'synchronized',
|
||||
header: 'Sincronizado',
|
||||
cell: (user: any) => `${user.synchronized}`
|
||||
cell: (user: any) => user.synchronized
|
||||
},
|
||||
{
|
||||
columnDef: 'isDefault',
|
||||
header: 'Plantilla por defecto',
|
||||
cell: (user: any) => user.isDefault
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
header: 'Fecha de creación',
|
||||
cell: (user: any) => `${this.datePipe.transform(user.createdAt, 'dd/MM/yyyy hh:mm:ss')}`
|
||||
cell: (user: any) => this.datePipe.transform(user.createdAt, 'dd/MM/yyyy hh:mm:ss')
|
||||
}
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
|
@ -80,12 +84,15 @@ export class PxeComponent implements OnInit{
|
|||
}
|
||||
|
||||
search(): void {
|
||||
this.dataService.getPxeTemplates(this.filters).subscribe(
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
|
||||
data => {
|
||||
this.dataSource.data = data;
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching pxe templates', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -100,10 +107,9 @@ export class PxeComponent implements OnInit{
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
editPxeTemplate(template: any) {
|
||||
const dialogRef = this.dialog.open(CreatePxeTemplateComponent, {
|
||||
data: template, // Pasa los datos del template para edición
|
||||
data: template,
|
||||
width: '800px'
|
||||
});
|
||||
|
||||
|
@ -128,7 +134,6 @@ export class PxeComponent implements OnInit{
|
|||
this.search();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al eliminar la subred', error);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
|
@ -143,19 +148,7 @@ export class PxeComponent implements OnInit{
|
|||
|
||||
showTemplate(event: MouseEvent, data: any): void {
|
||||
event.stopPropagation();
|
||||
const dialogRef = this.dialog.open(ShowTemplateContentComponent, { data: { data }, width: '700px' });
|
||||
}
|
||||
|
||||
applyFilter() {
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({
|
||||
next: (response) => {
|
||||
this.dataSource.data = response['hydra:member'];
|
||||
this.length = response['hydra:totalItems'];
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al cargar las imágenes:', error);
|
||||
}
|
||||
});
|
||||
const dialogRef = this.dialog.open(ShowTemplateContentComponent, { data: { data }, width: '800px' });
|
||||
}
|
||||
|
||||
loadAlert(): Observable<any> {
|
||||
|
@ -181,20 +174,21 @@ export class PxeComponent implements OnInit{
|
|||
);
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent) {
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.applyFilter();
|
||||
this.length = event.length;
|
||||
this.search();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
initTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'serverInfoStep',
|
||||
'titleStep',
|
||||
'viewInfoStep',
|
||||
'addTemplateStep',
|
||||
'searchNameStep',
|
||||
'searchSyncStep',
|
||||
'searchIsDefaultStep',
|
||||
'tableStep',
|
||||
'actionsStep',
|
||||
'paginationStep'
|
||||
|
|
|
@ -26,8 +26,15 @@
|
|||
background-color: #f5f5f5;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
border: 1px solid #dcdcdc;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
<div class="info-container">
|
||||
<h3>{{ 'detailsTitle' | translate: { name: data.data.name } }}</h3>
|
||||
<pre class="code-block">{{ data.data.templateContent }}</pre>
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<div class="info-container">
|
||||
<h3>{{ 'detailsTitle' | translate: { name: data.data.name } }}</h3>
|
||||
<pre class="code-block">{{ data.data.templateContent }}</pre>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA } from "@angular/material/dialog";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ConfigService } from "@services/config.service";
|
||||
|
||||
|
@ -15,8 +15,13 @@ export class ShowTemplateContentComponent {
|
|||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService
|
||||
private configService: ConfigService,
|
||||
public dialogRef: MatDialogRef<ShowTemplateContentComponent>
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,25 +79,6 @@ table {
|
|||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.example-headers-align .mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.example-headers-align .mat-mdc-form-field+.mat-mdc-form-field {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.example-button-row {
|
||||
display: table-cell;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.example-button-row .mat-mdc-button-base {
|
||||
margin: 8px 8px 8px 0;
|
||||
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
|
|
|
@ -1,31 +1,33 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<button mat-icon-button color="primary" (click)="initTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="repositoryTitleStep" text="{{ 'groupsTitleStepText' | translate }}">
|
||||
<h2 joyrideStep="repositoryTitleStep" text="{{ 'repositoryTitleStepText' | translate }}">
|
||||
{{ 'repositoryTitle' | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" (click)="addImage()" joyrideStep="addStep"
|
||||
text="Utiliza este botón para añadir un nuevo repositorio.">Añadir repositorio</button>
|
||||
<button
|
||||
class="action-button" (click)="addImage()"
|
||||
joyrideStep="addStep"
|
||||
text="{{ 'addRepositoryStepText' | translate }}">{{ 'addRepository' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
|
||||
<div class="search-container">
|
||||
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>Buscar nombre de repositorio</mat-label>
|
||||
<mat-label>{{ 'search' | translate }} {{ 'repository' | translate }}</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()"
|
||||
i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>Buscar IP de repositorio</mat-label>
|
||||
<mat-label>{{ 'search' | translate }} IP de {{ 'repository' | translate }}</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" (keyup.enter)="search()"
|
||||
i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
|
@ -33,7 +35,7 @@
|
|||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableDateStep" text="{{ 'tableDateRepositoryText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let repository">
|
||||
|
@ -42,15 +44,30 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'images'">
|
||||
<button class="action-button" (click)="openShowMonoliticImagesDialog(repository)">Gestionar Imágenes</button>
|
||||
<button class="action-button" style="margin-left: 0.5vw;" [disabled]="!isGitModuleInstalled" (click)="openShowGitImagesDialog(repository)">Gestionar Imágenes Git</button>
|
||||
<button
|
||||
class="action-button"
|
||||
joyrideStep="monolithicImageStep"
|
||||
text="{{ 'monolithicImageStepText' | translate }}"
|
||||
(click)="openShowMonoliticImagesDialog(repository)">
|
||||
{{ 'monolithicImage' | translate }}
|
||||
</button>
|
||||
<button
|
||||
class="action-button"
|
||||
joyrideStep="gitImageStep"
|
||||
text="{{ 'gitImageStepText' | translate }}"
|
||||
style="margin-left: 0.5vw;"
|
||||
[disabled]="!isGitModuleInstalled"
|
||||
(click)="openShowGitImagesDialog(repository)">
|
||||
{{ 'gitImage' | translate }}
|
||||
</button>
|
||||
</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 repository" style="text-align: center;">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">{{ 'actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let repository" style="text-align: center;" joyrideStep="actionsStep"
|
||||
[text]="'actionsDescription' | translate">
|
||||
<button mat-icon-button color="primary" (click)="editRepository($event, repository)" i18n="@@editImage">
|
||||
<mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button color="warn" (click)="deleteRepository($event, repository)">
|
||||
|
@ -62,7 +79,7 @@
|
|||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<div class="paginator-container">
|
||||
<div class="paginator-container" joyrideStep="paginationStep" text="{{ 'paginationDescription' | translate }}">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="[5, 10, 20, 40, 100]"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
|
|
|
@ -23,7 +23,7 @@ export class RepositoriesComponent implements OnInit {
|
|||
private apiUrl: string;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 10;
|
||||
itemsPerPage: number = 20;
|
||||
page: number = 0;
|
||||
loading: boolean = false;
|
||||
filters: { [key: string]: string } = {};
|
||||
|
@ -37,7 +37,7 @@ export class RepositoriesComponent implements OnInit {
|
|||
{
|
||||
columnDef: 'name',
|
||||
header: 'Nombre de repositorio',
|
||||
cell: (repository: any) => `${repository.name}`
|
||||
cell: (repository: any) => repository.name
|
||||
},
|
||||
{
|
||||
columnDef: 'user',
|
||||
|
@ -47,12 +47,12 @@ export class RepositoriesComponent implements OnInit {
|
|||
{
|
||||
columnDef: 'ip',
|
||||
header: 'Ip',
|
||||
cell: (repository: any) => `${repository.ip}`
|
||||
cell: (repository: any) => repository.ip
|
||||
},
|
||||
{
|
||||
columnDef: 'images',
|
||||
header: 'Imágenes',
|
||||
cell: (repository: any) => `${repository.images}`
|
||||
header: 'Gestionar imágenes',
|
||||
cell: (repository: any) => repository.images
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
|
@ -60,7 +60,7 @@ export class RepositoriesComponent implements OnInit {
|
|||
cell: (repository: any) => `${this.datePipe.transform(repository.createdAt, 'dd/MM/yyyy hh:mm:ss')}`
|
||||
}
|
||||
];
|
||||
isGitModuleInstalled: boolean = false;
|
||||
isGitModuleInstalled: boolean = true;
|
||||
displayedColumns: string[] = ['id', 'name', 'ip', 'user', 'images', 'createdAt', 'actions'];
|
||||
|
||||
constructor(
|
||||
|
@ -166,11 +166,17 @@ export class RepositoriesComponent implements OnInit {
|
|||
this.search();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
initTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'repositoryTitleStep',
|
||||
'addStep',
|
||||
'searchStep',
|
||||
'tableDateStep',
|
||||
'monolithicImageStep',
|
||||
'gitImageStep',
|
||||
'actionsStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
|
|
|
@ -10,13 +10,10 @@
|
|||
</div>
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" (click)="openImageInfoDialog()">Ver Información</button>
|
||||
<button class="action-button" (click)="syncRepository()">Sincronizar base de datos</button>
|
||||
<button class="action-button" (click)="importImage()">
|
||||
<button class="action-button" disabled (click)="syncRepository()">Sincronizar base de datos</button>
|
||||
<button class="action-button" disabled (click)="importImage()">
|
||||
{{ 'importImageButton' | translate }}
|
||||
</button>
|
||||
<button class="action-button" (click)="convertImage()">
|
||||
{{ 'convertImageButton' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -48,10 +45,10 @@
|
|||
<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 === 'remotePc' || column.columnDef === 'isGlobal'">
|
||||
<mat-icon [color]="image.image[column.columnDef] ? 'primary' : 'warn'">
|
||||
{{ image.image[column.columnDef] ? 'check_circle' : 'cancel' }}
|
||||
</mat-icon>
|
||||
<ng-container *ngIf="column.columnDef === 'isGlobal'">
|
||||
<mat-chip>
|
||||
{{ image.isGlobal ? 'Sí' : 'No' }}
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'status'">
|
||||
<mat-chip [ngClass]="{
|
||||
|
@ -74,8 +71,10 @@
|
|||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<td mat-cell *matCellDef="let image" style="text-align: center;">
|
||||
<button mat-icon-button color="info" (click)="showImageInfo($event, image)"><mat-icon
|
||||
i18n="@@deleteElementTooltip">visibility</mat-icon></button>
|
||||
<button mat-icon-button color="primary" (click)="goToPage(image)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-icon-button color="primary" (click)="toggleAction(image, 'edit')">
|
||||
<mat-icon i18n="@@deleteElementTooltip">edit</mat-icon>
|
||||
</button>
|
||||
|
@ -86,21 +85,16 @@
|
|||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="toggleAction(image, 'get-aux')">Obtener ficheros auxiliares</button>
|
||||
<button mat-menu-item
|
||||
(click)="toggleAction(image, 'show-tags')">Ver tags</button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'"
|
||||
(click)="toggleAction(image, 'delete-permanent')">Eliminar permanentemente</button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'trash'"
|
||||
(click)="toggleAction(image, 'recover')">Recuperar imagen de la papelera</button>
|
||||
(click)="toggleAction(image, 'show-branches')">Ver ramas</button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'"
|
||||
(click)="toggleAction(image, 'transfer')">Transferir imagen</button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'"
|
||||
(click)="toggleAction(image, 'transfer-global')">Transferir imagen globalmente </button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'"
|
||||
(click)="toggleAction(image, 'backup')">Realizar backup </button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'"
|
||||
(click)="toggleAction(image, 'status')">Checkear estado imagen </button>
|
||||
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'"
|
||||
(click)="toggleAction(image, 'convert-image-to-virtual')">Convertir imagen en virtual </button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
@ -117,4 +111,4 @@
|
|||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">Cerrar</button>
|
||||
</mat-dialog-actions>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -10,11 +10,9 @@ import {Router} from "@angular/router";
|
|||
import {Observable} from "rxjs";
|
||||
import {ServerInfoDialogComponent} from "../../ogdhcp/server-info-dialog/server-info-dialog.component";
|
||||
import {ImportImageComponent} from "../import-image/import-image.component";
|
||||
import {ConvertImageComponent} from "../convert-image/convert-image.component";
|
||||
import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {ExportImageComponent} from "../../images/export-image/export-image.component";
|
||||
import {BackupImageComponent} from "../backup-image/backup-image.component";
|
||||
import {ConvertImageToVirtualComponent} from "../convert-image-to-virtual/convert-image-to-virtual.component";
|
||||
import {EditImageComponent} from "../edit-image/edit-image.component";
|
||||
|
||||
@Component({
|
||||
|
@ -22,7 +20,7 @@ import {EditImageComponent} from "../edit-image/edit-image.component";
|
|||
templateUrl: './show-git-images.component.html',
|
||||
styleUrl: './show-git-images.component.css'
|
||||
})
|
||||
export class ShowGitImagesComponent {
|
||||
export class ShowGitImagesComponent implements OnInit{
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
|
@ -41,29 +39,34 @@ baseUrl: string;
|
|||
cell: (image: any) => `${image.id}`
|
||||
},
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'Nombre de imagen',
|
||||
cell: (image: any) => `${image.image.name}`
|
||||
columnDef: 'repositoryName',
|
||||
header: 'Nombre del repositorio',
|
||||
cell: (image: any) => image.image?.name
|
||||
},
|
||||
{
|
||||
columnDef: 'version',
|
||||
header: 'Version',
|
||||
cell: (image: any) => `${image.version ? image.version : '0'}`
|
||||
columnDef: 'name',
|
||||
header: 'Nombre de imagen',
|
||||
cell: (image: any) => image.name
|
||||
},
|
||||
{
|
||||
columnDef: 'tag',
|
||||
header: 'Tag',
|
||||
cell: (image: any) => image.tag
|
||||
},
|
||||
{
|
||||
columnDef: 'isGlobal',
|
||||
header: 'Imagen global',
|
||||
cell: (image: any) => `${image.image?.isGlobal}`
|
||||
cell: (image: any) => image.image?.isGlobal
|
||||
},
|
||||
{
|
||||
columnDef: 'status',
|
||||
header: 'Estado',
|
||||
cell: (image: any) => `${image.status}`
|
||||
cell: (image: any) => image.status
|
||||
},
|
||||
{
|
||||
columnDef: 'description',
|
||||
header: 'Descripción',
|
||||
cell: (image: any) => `${image.description ? image.description : 'Sin descripción'}`
|
||||
cell: (image: any) => image.description
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
|
@ -84,11 +87,10 @@ baseUrl: string;
|
|||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/image-image-repositories`;
|
||||
this.apiUrl = `${this.baseUrl}/git-image-repositories`;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
console.error()
|
||||
if (this.data) {
|
||||
this.loadData();
|
||||
}
|
||||
|
@ -140,30 +142,7 @@ baseUrl: string;
|
|||
return this.http.get<any>(`${this.apiUrl}/server/${image.uuid}/get`, {});
|
||||
}
|
||||
|
||||
showImageInfo(event: MouseEvent, image:any) {
|
||||
event.stopPropagation();
|
||||
this.loading = true;
|
||||
this.loadImageAlert(image).subscribe(
|
||||
response => {
|
||||
this.alertMessage = response;
|
||||
|
||||
this.dialog.open(ServerInfoDialogComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
message: this.alertMessage
|
||||
}
|
||||
});
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
importImage(): void {
|
||||
console.log(this.data)
|
||||
this.dialog.open(ImportImageComponent, {
|
||||
width: '600px',
|
||||
data: {
|
||||
|
@ -177,33 +156,8 @@ baseUrl: string;
|
|||
});
|
||||
}
|
||||
|
||||
convertImage(): void {
|
||||
this.dialog.open(ConvertImageComponent, {
|
||||
width: '600px',
|
||||
data: {
|
||||
repositoryUuid: this.data.repositoryUuid,
|
||||
name: this.data.repositoryName
|
||||
}
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.loadData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleAction(image: any, action:string): void {
|
||||
switch (action) {
|
||||
case 'get-aux':
|
||||
this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/create-aux-files`, {}).subscribe({
|
||||
next: (message) => {
|
||||
this.toastService.success('Petición de creación de archivos auxiliares enviada');
|
||||
this.loadData()
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'delete-trash':
|
||||
if (!image.imageFullsum) {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
|
@ -278,17 +232,6 @@ baseUrl: string;
|
|||
}
|
||||
});
|
||||
break;
|
||||
case 'status':
|
||||
this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/status`, {}).subscribe({
|
||||
next: (response: any) => {
|
||||
this.toastService.info(response?.output);
|
||||
this.loadData()
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'transfer':
|
||||
this.http.get(`${this.baseUrl}${image.image['@id']}`).subscribe({
|
||||
next: (response) => {
|
||||
|
@ -335,17 +278,15 @@ baseUrl: string;
|
|||
}
|
||||
});
|
||||
break;
|
||||
case 'convert-image-to-virtual':
|
||||
this.http.get(`${this.baseUrl}${image.image['@id']}`).subscribe({
|
||||
case 'show-tags':
|
||||
this.http.get(`${this.baseUrl}/git-image-repositories/server/${image.uuid}/get-tags`, {}).subscribe({
|
||||
next: (response) => {
|
||||
this.dialog.open(ConvertImageToVirtualComponent, {
|
||||
width: '600px',
|
||||
this.dialog.open(ServerInfoDialogComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
image: response,
|
||||
imageImageRepository: image
|
||||
repositories: response
|
||||
}
|
||||
});
|
||||
this.router.navigate(['/commands-logs']);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
|
@ -376,11 +317,11 @@ baseUrl: string;
|
|||
}
|
||||
|
||||
loadAlert(): Observable<any> {
|
||||
return this.http.post<any>(`${this.baseUrl}/image-repositories/server/${this.data.repositoryUuid}/get-collection`, {});
|
||||
return this.http.post<any>(`${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/get-collection`, {});
|
||||
}
|
||||
|
||||
syncRepository() {
|
||||
this.http.post(`${this.baseUrl}/image-repositories/server/${this.data.repositoryUuid}/sync`, {})
|
||||
this.http.post(`${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/sync`, {})
|
||||
.subscribe(response => {
|
||||
this.toastService.success('Sincronización completada');
|
||||
this.loadData()
|
||||
|
@ -393,12 +334,12 @@ baseUrl: string;
|
|||
openImageInfoDialog() {
|
||||
this.loadAlert().subscribe(
|
||||
response => {
|
||||
this.alertMessage = response.output;
|
||||
this.alertMessage = response.repositories;
|
||||
|
||||
this.dialog.open(ServerInfoDialogComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
message: this.alertMessage
|
||||
repositories: this.alertMessage
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -408,6 +349,10 @@ baseUrl: string;
|
|||
);
|
||||
}
|
||||
|
||||
goToPage( image: any) {
|
||||
window.location.href = `http://192.168.68.20:3000/oggit/${image.image.name}`;
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<h2>Gestionar imágenes monolíticas en {{data.repositoryName}}</h2>
|
||||
<h2> {{ 'monolithicImage' | translate }} {{data.repositoryName}}</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" (click)="openImageInfoDialog()">Ver Información</button>
|
||||
|
@ -48,11 +48,24 @@
|
|||
<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 === 'remotePc' || column.columnDef === 'isGlobal'">
|
||||
<mat-icon [color]="image.image[column.columnDef] ? 'primary' : 'warn'">
|
||||
{{ image.image[column.columnDef] ? 'check_circle' : 'cancel' }}
|
||||
</mat-icon>
|
||||
<ng-container *ngIf="column.columnDef === 'name'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
{{ image.name }}
|
||||
</div>
|
||||
<div *ngIf="image.partitionInfo" style="font-size: 0.75rem; color: gray;">
|
||||
Disco:{{ image.partitionInfo?.numDisk }} | Partición:{{ image.partitionInfo?.numPartition }} |
|
||||
FileSystem:{{ image.partitionInfo?.filesystem }} | Code:{{ image.partitionInfo?.partitionCode }}
|
||||
</div>
|
||||
<div *ngIf="image.partitionInfo" style="font-size: 0.75rem; color: gray;">
|
||||
{{ image.partitionInfo?.osName }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'isGlobal'">
|
||||
<mat-chip>
|
||||
{{ image.isGlobal ? 'Sí' : 'No' }}
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'status'">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-failed': image.status === 'failed',
|
||||
|
@ -65,7 +78,7 @@
|
|||
</mat-chip>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status' && column.columnDef !== 'isGlobal'">
|
||||
*ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status' && column.columnDef !== 'isGlobal' && column.columnDef !== 'name'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
|
|
|
@ -44,7 +44,7 @@ export class ShowMonoliticImagesComponent implements OnInit {
|
|||
{
|
||||
columnDef: 'name',
|
||||
header: 'Nombre de imagen',
|
||||
cell: (image: any) => `${image.name}`
|
||||
cell: (image: any) => image.name
|
||||
},
|
||||
{
|
||||
columnDef: 'version',
|
||||
|
@ -54,17 +54,17 @@ export class ShowMonoliticImagesComponent implements OnInit {
|
|||
{
|
||||
columnDef: 'isGlobal',
|
||||
header: 'Imagen global',
|
||||
cell: (image: any) => `${image.image?.isGlobal}`
|
||||
cell: (image: any) => image.image?.isGlobal
|
||||
},
|
||||
{
|
||||
columnDef: 'status',
|
||||
header: 'Estado',
|
||||
cell: (image: any) => `${image.status}`
|
||||
cell: (image: any) => image.status
|
||||
},
|
||||
{
|
||||
columnDef: 'description',
|
||||
header: 'Descripción',
|
||||
cell: (image: any) => `${image.description ? image.description : 'Sin descripción'}`
|
||||
cell: (image: any) => image.description
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
|
@ -164,7 +164,6 @@ export class ShowMonoliticImagesComponent implements OnInit {
|
|||
}
|
||||
|
||||
importImage(): void {
|
||||
console.log(this.data)
|
||||
this.dialog.open(ImportImageComponent, {
|
||||
width: '600px',
|
||||
data: {
|
||||
|
@ -410,9 +409,7 @@ export class ShowMonoliticImagesComponent implements OnInit {
|
|||
|
||||
this.dialog.open(ServerInfoDialogComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
message: this.alertMessage
|
||||
}
|
||||
data: this.alertMessage
|
||||
});
|
||||
},
|
||||
error => {
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
.modal-content {
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.calendar-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.lists-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.imagesLists-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card.unidad-card {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.chip-failed {
|
||||
background-color: #e87979 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-success {
|
||||
background-color: #46c446 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chip-pending {
|
||||
background-color: #bebdbd !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.chip-in-progress {
|
||||
background-color: #f5a623 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-progress-flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button.cancel-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
color: red;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cancel-button mat-icon {
|
||||
color: red;
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
<div class="modal-content">
|
||||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' |
|
||||
translate }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" (click)="resetFilters(commandSearchInput, commandStatusInput)"
|
||||
joyrideStep="resetFiltersStep" text="{{ 'resetFiltersStepText' | translate }}">
|
||||
{{ 'resetFilters' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-select">
|
||||
<mat-label>{{ 'commandSelectStepText' | translate }}</mat-label>
|
||||
<mat-select (selectionChange)="onOptionCommandSelected($event.value)" #commandSearchInput>
|
||||
<mat-option *ngFor="let command of filteredCommands2" [value]="command">
|
||||
{{ translateCommand(command.name) }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<button *ngIf="commandSearchInput.value" mat-icon-button matSuffix aria-label="Clear input search"
|
||||
(click)="clearCommandFilter($event, commandSearchInput)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
<mat-form-field appearance="fill" class="search-boolean">
|
||||
<mat-label i18n="@@searchLabel">Estado</mat-label>
|
||||
<mat-select (selectionChange)="onOptionStatusSelected($event.value)" placeholder="Seleccionar opción"
|
||||
#commandStatusInput>
|
||||
<mat-option [value]="'failed'">Fallido</mat-option>
|
||||
<mat-option [value]="'pending'">Pendiente de ejecutar</mat-option>
|
||||
<mat-option [value]="'in-progress'">Ejecutando</mat-option>
|
||||
<mat-option [value]="'success'">Completado con éxito</mat-option>
|
||||
<mat-option [value]="'cancelled'">Cancelado</mat-option>
|
||||
</mat-select>
|
||||
<button *ngIf="commandStatusInput.value" mat-icon-button matSuffix aria-label="Clear input search"
|
||||
(click)="clearStatusFilter($event, commandStatusInput)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="traces" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let trace">
|
||||
|
||||
<ng-container [ngSwitch]="column.columnDef">
|
||||
<ng-container *ngSwitchCase="'status'">
|
||||
<ng-container *ngIf="trace.status === 'in-progress' && trace.progress; else statusChip">
|
||||
<div class="progress-container">
|
||||
<mat-progress-bar class="example-margin" [mode]="mode" [value]="trace.progress"
|
||||
[bufferValue]="bufferValue">
|
||||
</mat-progress-bar>
|
||||
<span>{{trace.progress}}%</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #statusChip>
|
||||
<div class="status-progress-flex">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-failed': trace.status === 'failed',
|
||||
'chip-success': trace.status === 'success',
|
||||
'chip-pending': trace.status === 'pending',
|
||||
'chip-in-progress': trace.status === 'in-progress',
|
||||
'chip-cancelled': trace.status === 'cancelled'
|
||||
}">
|
||||
{{
|
||||
trace.status === 'failed' ? 'Error' :
|
||||
trace.status === 'in-progress' ? 'En ejecución' :
|
||||
trace.status === 'success' ? 'Completado' :
|
||||
trace.status === 'pending' ? 'Pendiente' :
|
||||
trace.status === 'cancelled' ? 'Cancelado' :
|
||||
trace.status
|
||||
}}
|
||||
</mat-chip>
|
||||
<button *ngIf="trace.status === 'in-progress' && trace.command === 'deploy-image'" mat-icon-button
|
||||
(click)="cancelTrace(trace)" class="cancel-button" matTooltip="Cancelar transmisión de imagen">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'command'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span>{{ translateCommand(trace.command) }}</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ trace.jobId }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'client'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span>{{ trace.client?.name }}</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ trace.client?.ip }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngSwitchCase="'executedAt'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span style="font-size: 0.8rem;"> {{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'finishedAt'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span style="font-size: 0.8rem;"> {{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
{{ column.cell(trace) }}
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let trace" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" [disabled]="!trace.input" (click)="openInputModal(trace.input)">
|
||||
<mat-icon>
|
||||
<span class="material-symbols-outlined">
|
||||
mode_comment
|
||||
</span>
|
||||
</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" [disabled]="!trace.output" (click)="openOutputModal(trace.output)">
|
||||
<mat-icon>
|
||||
<span class="material-symbols-outlined">
|
||||
info
|
||||
</span>
|
||||
</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep" text="{{ 'paginationStepText' | translate }}">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,55 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { ClientTaskLogsComponent } from './client-task-logs.component';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { LoadingComponent } from 'src/app/shared/loading/loading.component';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('ClientTaskLogsComponent', () => {
|
||||
let component: ClientTaskLogsComponent;
|
||||
let fixture: ComponentFixture<ClientTaskLogsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockJoyrideService = jasmine.createSpyObj('JoyrideService', ['startTour']);
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ClientTaskLogsComponent, LoadingComponent],
|
||||
imports: [HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
MatIconModule,
|
||||
TranslateModule.forRoot(),
|
||||
MatFormFieldModule,
|
||||
MatPaginatorModule,
|
||||
MatSelectModule,
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: { close: jasmine.createSpy('close') } },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {} },
|
||||
{ provide: JoyrideService, useValue: mockJoyrideService },
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ClientTaskLogsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,304 @@
|
|||
import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { DeleteModalComponent } from 'src/app/shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { TranslationService } from '@services/translation.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { OutputDialogComponent } from '../output-dialog/output-dialog.component';
|
||||
import { InputDialogComponent } from '../input-dialog/input-dialog.component';
|
||||
import { ProgressBarMode } from '@angular/material/progress-bar';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { COMMAND_TYPES } from 'src/app/shared/constants/command-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-client-task-logs',
|
||||
templateUrl: './client-task-logs.component.html',
|
||||
styleUrls: ['./client-task-logs.component.css']
|
||||
})
|
||||
export class ClientTaskLogsComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
mercureUrl: string;
|
||||
traces: any[] = [];
|
||||
groupedTraces: any[] = [];
|
||||
commands: any[] = [];
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 20;
|
||||
page: number = 0;
|
||||
loading: boolean = true;
|
||||
pageSizeOptions: number[] = [10, 20, 30, 50];
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
mode: ProgressBarMode = 'buffer';
|
||||
progress = 0;
|
||||
bufferValue = 0;
|
||||
dateRange = new FormControl();
|
||||
|
||||
filteredCommands2 = Object.keys(COMMAND_TYPES).map(key => ({
|
||||
name: key,
|
||||
value: key,
|
||||
label: COMMAND_TYPES[key]
|
||||
}));
|
||||
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
header: 'ID',
|
||||
cell: (trace: any) => `${trace.id}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'command',
|
||||
header: 'Comando',
|
||||
cell: (trace: any) => trace.command
|
||||
},
|
||||
{
|
||||
columnDef: 'status',
|
||||
header: 'Estado',
|
||||
cell: (trace: any) => trace.status
|
||||
},
|
||||
{
|
||||
columnDef: 'executedAt',
|
||||
header: 'Ejecución',
|
||||
cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
},
|
||||
{
|
||||
columnDef: 'finishedAt',
|
||||
header: 'Finalización',
|
||||
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
},
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
|
||||
filters: { [key: string]: string } = {};
|
||||
filteredCommands!: Observable<any[]>;
|
||||
commandControl = new FormControl();
|
||||
|
||||
constructor(private http: HttpClient,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { client: any },
|
||||
private joyrideService: JoyrideService,
|
||||
private dialog: MatDialog,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
private translationService: TranslationService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.mercureUrl = this.configService.mercureUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTraces();
|
||||
this.loadCommands();
|
||||
this.filteredCommands = this.commandControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map(value => (typeof value === 'string' ? value : value?.name)),
|
||||
map(name => (name ? this._filterCommands(name) : this.commands.slice()))
|
||||
);
|
||||
|
||||
const eventSource = new EventSource(`${this.mercureUrl}?topic=`
|
||||
+ encodeURIComponent(`traces`));
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data && data['@id']) {
|
||||
this.updateTracesStatus(data['@id'], data.status, data.progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void {
|
||||
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
|
||||
if (traceIndex !== -1) {
|
||||
const updatedTraces = [...this.traces];
|
||||
|
||||
updatedTraces[traceIndex] = {
|
||||
...updatedTraces[traceIndex],
|
||||
status: newStatus,
|
||||
progress: progress
|
||||
};
|
||||
|
||||
this.traces = updatedTraces;
|
||||
this.cdr.detectChanges();
|
||||
|
||||
console.log(`Estado actualizado para la traza ${clientUuid}: ${newStatus}`);
|
||||
} else {
|
||||
console.warn(`Traza con UUID ${clientUuid} no encontrado en la lista.`);
|
||||
}
|
||||
}
|
||||
|
||||
private _filterCommands(name: string): any[] {
|
||||
const filterValue = name.toLowerCase();
|
||||
return this.commands.filter(command => command.name.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
onOptionCommandSelected(selectedCommand: any): void {
|
||||
this.filters['command'] = selectedCommand.id;
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
onOptionStatusSelected(selectedStatus: any): void {
|
||||
this.filters['status'] = selectedStatus;
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
openInputModal(inputData: any): void {
|
||||
this.dialog.open(InputDialogComponent, {
|
||||
width: '70vw',
|
||||
height: '60vh',
|
||||
data: { input: inputData }
|
||||
});
|
||||
}
|
||||
|
||||
openOutputModal(outputData: any): void {
|
||||
this.dialog.open(OutputDialogComponent, {
|
||||
width: '500px',
|
||||
data: { input: outputData }
|
||||
});
|
||||
}
|
||||
|
||||
cancelTrace(trace: any): void {
|
||||
this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: trace.jobId },
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.http.post(`${this.baseUrl}/traces/server/${trace.uuid}/cancel`, {}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Transmision de imagen cancelada');
|
||||
this.loadTraces();
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
console.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadTraces(): void {
|
||||
const clientId = this.data.client?.id;
|
||||
if (!clientId) return;
|
||||
|
||||
this.loading = true;
|
||||
|
||||
let params = new HttpParams()
|
||||
.set('client.id', clientId)
|
||||
.set('page', (this.page + 1).toString())
|
||||
.set('itemsPerPage', this.itemsPerPage.toString());
|
||||
|
||||
if (this.filters['command']) {
|
||||
params = params.set('command.id', this.filters['command']);
|
||||
}
|
||||
|
||||
if (this.filters['status']) {
|
||||
params = params.set('status', this.filters['status']);
|
||||
}
|
||||
|
||||
const range = this.dateRange?.value;
|
||||
if (range?.start && range?.end) {
|
||||
const fromDate = this.datePipe.transform(range.start, 'yyyy-MM-dd');
|
||||
const toDate = this.datePipe.transform(range.end, 'yyyy-MM-dd');
|
||||
|
||||
params = params.set('executedAt[after]', fromDate!);
|
||||
params = params.set('executedAt[before]', toDate!);
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}/traces`;
|
||||
|
||||
this.http.get<any>(url, { params }).subscribe(
|
||||
(data) => {
|
||||
this.traces = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching client traces', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
loadCommands() {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.baseUrl}/commands?&page=1&itemsPerPage=10000`).subscribe(
|
||||
response => {
|
||||
this.commands = response['hydra:member'];
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching commands:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
resetFilters(clientSearchCommandInput: any, clientSearchStatusInput: any) {
|
||||
this.loading = true;
|
||||
clientSearchCommandInput.value = null;
|
||||
clientSearchStatusInput.value = null;
|
||||
this.dateRange.reset();
|
||||
this.filters = {};
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
groupByCommandId(traces: any[]): any[] {
|
||||
const grouped: { [key: string]: any[] } = {};
|
||||
|
||||
traces.forEach(trace => {
|
||||
const commandId = trace.command.id;
|
||||
if (!grouped[commandId]) {
|
||||
grouped[commandId] = [];
|
||||
}
|
||||
grouped[commandId].push(trace);
|
||||
});
|
||||
|
||||
return Object.keys(grouped).map(key => ({
|
||||
commandId: key,
|
||||
traces: grouped[key]
|
||||
}));
|
||||
}
|
||||
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.length = event.length;
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
translateCommand(command: string): string {
|
||||
return this.translationService.getCommandTranslation(command);
|
||||
}
|
||||
|
||||
clearCommandFilter(event: Event, clientSearchCommandInput: any): void {
|
||||
event.stopPropagation();
|
||||
delete this.filters['command'];
|
||||
clientSearchCommandInput.value = null;
|
||||
this.loadTraces()
|
||||
}
|
||||
|
||||
clearStatusFilter(event: Event, clientSearchStatusInput: any): void {
|
||||
event.stopPropagation();
|
||||
delete this.filters['status'];
|
||||
clientSearchStatusInput.value = null;
|
||||
this.loadTraces()
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'titleStep',
|
||||
'resetFiltersStep',
|
||||
'clientSelectStep',
|
||||
'commandSelectStep',
|
||||
'tableStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<h1 mat-dialog-title>{{ 'inputDetails' | translate }}</h1>
|
||||
<div mat-dialog-content>
|
||||
<pre>{{ data.input | json }}</pre>
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'closeButton' | translate }}</button>
|
||||
</div>
|
|
@ -0,0 +1,31 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { OutputDialogComponent } from './output-dialog.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('OutputDialogComponent', () => {
|
||||
let component: OutputDialogComponent;
|
||||
let fixture: ComponentFixture<OutputDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OutputDialogComponent],
|
||||
imports: [
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: { close: jasmine.createSpy('close') } },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { input: {} } }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(OutputDialogComponent);
|
||||
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-output-dialog',
|
||||
templateUrl: './output-dialog.component.html',
|
||||
styleUrl: './output-dialog.component.css'
|
||||
})
|
||||
export class OutputDialogComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<OutputDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { input: any }
|
||||
) {}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue