commit
53ed53958f
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -1,4 +1,22 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
## [0.18.0] - 2025-08-04
|
||||||
|
### Added
|
||||||
|
- Se ha añadido la posibilidad de visualizar logs en tiempo real de Grafana. Tanto en los componentes como en los clientes.
|
||||||
|
- Se ha añadido la funcionaldad e integracion con OgGit.
|
||||||
|
- En el particionador, se ha añadido una integracion para comprobar los tamaños de las particiones.
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Sistema de cola de acciones.
|
||||||
|
|
||||||
|
---
|
||||||
|
## [0.17.0] - 2025-07-15
|
||||||
|
### Added
|
||||||
|
- Se ha añadido la funcionalidad para tagear commits en el apartado de imágenes git.
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Se ha corregido el particionador, para cuando un equipo es EFI, ahora aparece la primera particion completada.
|
||||||
|
|
||||||
|
|
||||||
## [0.16.0] - 2025-06-27
|
## [0.16.0] - 2025-06-27
|
||||||
### Added
|
### Added
|
||||||
- Sistema de logs en tiempo real.
|
- Sistema de logs en tiempo real.
|
||||||
|
|
|
@ -159,6 +159,9 @@ import { ClientPendingTasksComponent } from './components/task-logs/client-pendi
|
||||||
import { QueueConfirmationModalComponent } from './shared/queue-confirmation-modal/queue-confirmation-modal.component';
|
import { QueueConfirmationModalComponent } from './shared/queue-confirmation-modal/queue-confirmation-modal.component';
|
||||||
import { ModalOverlayComponent } from './shared/modal-overlay/modal-overlay.component';
|
import { ModalOverlayComponent } from './shared/modal-overlay/modal-overlay.component';
|
||||||
import { ScrollToTopComponent } from './shared/scroll-to-top/scroll-to-top.component';
|
import { ScrollToTopComponent } from './shared/scroll-to-top/scroll-to-top.component';
|
||||||
|
import { CreateTagModalComponent } from './components/repositories/show-git-images/create-tag-modal/create-tag-modal.component';
|
||||||
|
import { CreateBranchModalComponent } from './components/repositories/show-git-images/create-branch-modal/create-branch-modal.component';
|
||||||
|
import { ClientLogsModalComponent } from './components/groups/shared/client-logs-modal/client-logs-modal.component';
|
||||||
|
|
||||||
export function HttpLoaderFactory(http: HttpClient) {
|
export function HttpLoaderFactory(http: HttpClient) {
|
||||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||||
|
@ -274,7 +277,10 @@ registerLocaleData(localeEs, 'es-ES');
|
||||||
SoftwareProfilePartitionComponent,
|
SoftwareProfilePartitionComponent,
|
||||||
ClientPendingTasksComponent,
|
ClientPendingTasksComponent,
|
||||||
ModalOverlayComponent,
|
ModalOverlayComponent,
|
||||||
ScrollToTopComponent
|
ScrollToTopComponent,
|
||||||
|
CreateTagModalComponent,
|
||||||
|
CreateBranchModalComponent,
|
||||||
|
ClientLogsModalComponent
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
imports: [BrowserModule,
|
imports: [BrowserModule,
|
||||||
|
|
|
@ -33,9 +33,9 @@ export class CommandsTaskComponent implements OnInit {
|
||||||
columns = [
|
columns = [
|
||||||
{ columnDef: 'id', header: 'ID', cell: (task: any) => task.id },
|
{ columnDef: 'id', header: 'ID', cell: (task: any) => task.id },
|
||||||
{ columnDef: 'name', header: 'Nombre de tarea', cell: (task: any) => task.name },
|
{ columnDef: 'name', header: 'Nombre de tarea', cell: (task: any) => task.name },
|
||||||
{ columnDef: 'organizationalUnit', header: 'Ámbito', cell: (task: any) => task.organizationalUnit.name },
|
{ columnDef: 'organizationalUnit', header: 'Ámbito', cell: (task: any) => task.scope },
|
||||||
{ columnDef: 'management', header: 'Gestiones', cell: (task: any) => task.schedules },
|
{ columnDef: 'management', header: 'Gestiones', cell: (task: any) => task.schedules },
|
||||||
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (task: any) => this.datePipe.transform(task.nextExecution, 'dd/MM/yyyy HH:mm:ss', 'UTC') },
|
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (task: any) => this.datePipe.transform(task.nextExecution, 'dd/MM/yyyy HH:mm:ss') },
|
||||||
{ columnDef: 'createdBy', header: 'Creado por', cell: (task: any) => task.createdBy },
|
{ columnDef: 'createdBy', header: 'Creado por', cell: (task: any) => task.createdBy },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
<input matInput formControlName="executionTime" placeholder="08:00" type="time">
|
<input matInput formControlName="executionTime" placeholder="08:00" type="time">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<!-- Mostrar solo si no es 'none' -->
|
|
||||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
|
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
|
||||||
<label>Días de la semana:</label>
|
<label>Días de la semana:</label>
|
||||||
<div class="weekday-toggle-group">
|
<div class="weekday-toggle-group">
|
||||||
|
@ -37,7 +36,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selección de meses -->
|
|
||||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
|
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
|
||||||
<label>Meses:</label>
|
<label>Meses:</label>
|
||||||
<div class="month-toggle-row" *ngFor="let row of monthRows">
|
<div class="month-toggle-row" *ngFor="let row of monthRows">
|
||||||
|
@ -52,7 +50,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rango de fechas -->
|
|
||||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails">
|
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails">
|
||||||
<mat-form-field appearance="fill" class="w-half">
|
<mat-form-field appearance="fill" class="w-half">
|
||||||
<mat-label>Desde</mat-label>
|
<mat-label>Desde</mat-label>
|
||||||
|
|
|
@ -19,6 +19,14 @@ mat-form-field {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mat-form-field.mat-form-field-disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field.mat-form-field-disabled .mat-form-field-label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
@ -63,3 +71,148 @@ mat-form-field {
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Estilos para la selección de clientes */
|
||||||
|
.clients-selection {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clients-selection h4 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-selected-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border: 1px solid #2196f3;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #1976d2;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-selected-info mat-icon {
|
||||||
|
color: #2196f3;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-clients {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clients-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-count {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clients-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-card {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-card.pre-selected {
|
||||||
|
border-color: #4caf50;
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-card.pre-selected:hover {
|
||||||
|
border-color: #45a049;
|
||||||
|
background-color: #d4edda;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-card mat-card-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-details {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-ip {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-status {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-og-live {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-offline {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-unknown {
|
||||||
|
background-color: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-icon {
|
||||||
|
color: #1976d2;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.clients-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clients-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,13 +5,11 @@
|
||||||
<mat-dialog-content class="dialog-content">
|
<mat-dialog-content class="dialog-content">
|
||||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||||
|
|
||||||
<!-- Toggle entre crear o añadir -->
|
|
||||||
<mat-radio-group *ngIf="data?.source === 'assistant'" [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode">
|
<mat-radio-group *ngIf="data?.source === 'assistant'" [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode">
|
||||||
<mat-radio-button value="create">Crear tarea</mat-radio-button>
|
<mat-radio-button value="create">Crear tarea</mat-radio-button>
|
||||||
<mat-radio-button value="add">Introducir en tarea existente</mat-radio-button>
|
<mat-radio-button value="add">Introducir en tarea existente</mat-radio-button>
|
||||||
</mat-radio-group>
|
</mat-radio-group>
|
||||||
|
|
||||||
<!-- Selección de tarea existente -->
|
|
||||||
<div *ngIf="taskMode === 'add'" class="select-task">
|
<div *ngIf="taskMode === 'add'" class="select-task">
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>Seleccione una tarea</mat-label>
|
<mat-label>Seleccione una tarea</mat-label>
|
||||||
|
@ -33,7 +31,6 @@
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Formulario de nueva tarea -->
|
|
||||||
<form *ngIf="taskMode === 'create' && taskForm && !loading" [formGroup]="taskForm" class="task-form">
|
<form *ngIf="taskMode === 'create' && taskForm && !loading" [formGroup]="taskForm" class="task-form">
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||||
|
@ -48,15 +45,17 @@
|
||||||
|
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="full-width">
|
||||||
<mat-label>Ámbito</mat-label>
|
<mat-label>Ámbito</mat-label>
|
||||||
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)">
|
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)"
|
||||||
|
[disabled]="data?.clients && data.clients.length >= 1">
|
||||||
<mat-option value="organizational-unit">Unidad Organizativa</mat-option>
|
<mat-option value="organizational-unit">Unidad Organizativa</mat-option>
|
||||||
<mat-option value="classrooms-group">Grupo de aulas</mat-option>
|
<mat-option value="classrooms-group">Grupo de aulas</mat-option>
|
||||||
<mat-option value="classroom">Aulas</mat-option>
|
<mat-option value="classroom">Aulas</mat-option>
|
||||||
<mat-option value="clients-group">Grupos de clientes</mat-option>
|
<mat-option value="clients-group">Grupos de clientes</mat-option>
|
||||||
|
<mat-option value="clients">Clientes</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field *ngIf="taskForm.get('scope')?.value !== 'clients'" appearance="fill" class="full-width">
|
||||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||||
<mat-select formControlName="organizationalUnit">
|
<mat-select formControlName="organizationalUnit">
|
||||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||||
|
@ -66,6 +65,40 @@
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
<div *ngIf="taskForm.get('scope')?.value === 'clients'" class="clients-selection">
|
||||||
|
<h4>Clientes seleccionados</h4>
|
||||||
|
|
||||||
|
<div *ngIf="data?.selectedClients && data.selectedClients.length > 0" class="pre-selected-info">
|
||||||
|
<mat-icon>info</mat-icon>
|
||||||
|
<span>Los clientes han sido pre-seleccionados desde el componente de despliegue de imágenes.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clients-list">
|
||||||
|
<div class="clients-grid">
|
||||||
|
<mat-card
|
||||||
|
*ngFor="let client of clients"
|
||||||
|
class="client-card"
|
||||||
|
[class.pre-selected]="isClientPreSelected(client)"
|
||||||
|
>
|
||||||
|
<mat-card-content>
|
||||||
|
<div class="client-info">
|
||||||
|
<div class="client-name">{{ client.name || client.hostname }}</div>
|
||||||
|
<div class="client-details">
|
||||||
|
<span class="client-ip">{{ client.ip }}</span>
|
||||||
|
<span class="client-status" [class]="'status-' + client.status">
|
||||||
|
{{ client.status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mat-icon class="selected-icon">
|
||||||
|
check_circle
|
||||||
|
</mat-icon>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<mat-checkbox *ngIf="!editing" formControlName="scheduleAfterCreate">
|
<mat-checkbox *ngIf="!editing" formControlName="scheduleAfterCreate">
|
||||||
¿Quieres programar la tarea al finalizar su creación?
|
¿Quieres programar la tarea al finalizar su creación?
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
|
|
|
@ -29,6 +29,7 @@ export class CreateTaskComponent implements OnInit {
|
||||||
existingTasks: any[] = [];
|
existingTasks: any[] = [];
|
||||||
selectedExistingTask: string | null = null;
|
selectedExistingTask: string | null = null;
|
||||||
executionOrder: number | null = null;
|
executionOrder: number | null = null;
|
||||||
|
selectedClients: any[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
|
@ -41,13 +42,32 @@ export class CreateTaskComponent implements OnInit {
|
||||||
) {
|
) {
|
||||||
this.baseUrl = this.configService.apiUrl;
|
this.baseUrl = this.configService.apiUrl;
|
||||||
this.apiUrl = `${this.baseUrl}/command-tasks`;
|
this.apiUrl = `${this.baseUrl}/command-tasks`;
|
||||||
|
|
||||||
|
let initialScope = '';
|
||||||
|
if (this.data?.selectedClients && this.data.selectedClients.length > 0) {
|
||||||
|
initialScope = 'clients';
|
||||||
|
} else if (this.data?.scope) {
|
||||||
|
initialScope = this.data.scope;
|
||||||
|
} else if (this.data?.runScriptContext) {
|
||||||
|
initialScope = this.data.runScriptContext.type || '';
|
||||||
|
}
|
||||||
|
|
||||||
this.taskForm = this.fb.group({
|
this.taskForm = this.fb.group({
|
||||||
scope: [ this.data?.scope ? this.data.scope : '', Validators.required],
|
scope: [initialScope, Validators.required],
|
||||||
name: ['', Validators.required],
|
name: ['', Validators.required],
|
||||||
organizationalUnit: [ this.data?.organizationalUnit ? this.data.organizationalUnit : null, Validators.required],
|
organizationalUnit: [ this.data?.organizationalUnit ? this.data.organizationalUnit : null],
|
||||||
notes: [''],
|
notes: [''],
|
||||||
scheduleAfterCreate: [false]
|
scheduleAfterCreate: [false]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.data?.selectedClients && Array.isArray(this.data.selectedClients)) {
|
||||||
|
this.selectedClients = [...this.data.selectedClients];
|
||||||
|
this.clients = [...this.data.selectedClients];
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.onScopeChange(initialScope);
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -111,10 +131,31 @@ export class CreateTaskComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
onScopeChange(scope: string): void {
|
onScopeChange(scope: string): void {
|
||||||
this.filterUnits(scope).subscribe(filteredUnits => {
|
if (scope === 'clients') {
|
||||||
this.availableOrganizationalUnits = filteredUnits;
|
if (this.data?.selectedClients && this.data.selectedClients.length > 0) {
|
||||||
|
this.selectedClients = [...this.data.selectedClients];
|
||||||
|
this.clients = [...this.data.selectedClients];
|
||||||
|
} else {
|
||||||
|
this.toastr.error('No hay clientes pre-seleccionados para este ámbito');
|
||||||
|
this.taskForm.get('scope')?.setValue('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.taskForm.get('organizationalUnit')?.setValue('');
|
this.taskForm.get('organizationalUnit')?.setValue('');
|
||||||
});
|
this.taskForm.get('organizationalUnit')?.clearValidators();
|
||||||
|
} else {
|
||||||
|
this.filterUnits(scope).subscribe(filteredUnits => {
|
||||||
|
this.availableOrganizationalUnits = filteredUnits;
|
||||||
|
if (!this.data?.organizationalUnit) {
|
||||||
|
this.taskForm.get('organizationalUnit')?.setValue('');
|
||||||
|
}
|
||||||
|
this.taskForm.get('organizationalUnit')?.setValidators(Validators.required);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.taskForm.get('organizationalUnit')?.updateValueAndValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
isClientPreSelected(client: any): boolean {
|
||||||
|
return this.data?.selectedClients && this.data.selectedClients.some((c: any) => c.uuid === client.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
startUnitsFilter(): Promise<void> {
|
startUnitsFilter(): Promise<void> {
|
||||||
|
@ -209,14 +250,31 @@ export class CreateTaskComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = this.taskForm.value;
|
const formData = this.taskForm.value;
|
||||||
|
const scope = formData.scope;
|
||||||
|
|
||||||
|
if (scope === 'clients' && this.selectedClients.length === 0) {
|
||||||
|
this.toastr.error('Debe seleccionar al menos un cliente');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope !== 'clients' && !formData.organizationalUnit) {
|
||||||
|
this.toastr.error('Debe seleccionar una unidad organizativa');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
scope: formData.scope,
|
scope: formData.scope,
|
||||||
organizationalUnit: formData.organizationalUnit,
|
|
||||||
notes: formData.notes || '',
|
notes: formData.notes || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (scope === 'clients') {
|
||||||
|
payload.clients = this.selectedClients.map(client => client.uuid);
|
||||||
|
payload.organizationalUnit = null;
|
||||||
|
} else {
|
||||||
|
payload.organizationalUnit = formData.organizationalUnit;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
const taskId = this.data.task.uuid;
|
const taskId = this.data.task.uuid;
|
||||||
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({
|
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({
|
||||||
|
|
|
@ -28,7 +28,7 @@ export class ShowTaskScheduleComponent implements OnInit{
|
||||||
columns = [
|
columns = [
|
||||||
{ columnDef: 'id', header: 'ID', cell: (schedule: any) => schedule.id },
|
{ columnDef: 'id', header: 'ID', cell: (schedule: any) => schedule.id },
|
||||||
{ columnDef: 'recurrenceType', header: 'Recurrencia', cell: (schedule: any) => schedule.recurrenceType },
|
{ columnDef: 'recurrenceType', header: 'Recurrencia', cell: (schedule: any) => schedule.recurrenceType },
|
||||||
{ columnDef: 'time', header: 'Hora de ejecución', cell: (schedule: any) => this.datePipe.transform(schedule.executionTime, 'HH:mm', 'UTC') },
|
{ columnDef: 'time', header: 'Hora de ejecución', cell: (schedule: any) => this.datePipe.transform(schedule.executionTime, 'HH:mm') },
|
||||||
{ columnDef: 'daysOfWeek', header: 'Dias de la semana', cell: (schedule: any) => schedule.recurrenceDetails.daysOfWeek },
|
{ columnDef: 'daysOfWeek', header: 'Dias de la semana', cell: (schedule: any) => schedule.recurrenceDetails.daysOfWeek },
|
||||||
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => schedule.recurrenceDetails.months },
|
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => schedule.recurrenceDetails.months },
|
||||||
{ columnDef: 'enabled', header: 'Activo', cell: (schedule: any) => schedule.enabled }
|
{ columnDef: 'enabled', header: 'Activo', cell: (schedule: any) => schedule.enabled }
|
||||||
|
|
|
@ -107,18 +107,18 @@ export class ExecuteCommandComponent implements OnInit {
|
||||||
|
|
||||||
this.arrayCommands = this.arrayCommands.map(command => {
|
this.arrayCommands = this.arrayCommands.map(command => {
|
||||||
if (allOffOrDisconnected) {
|
if (allOffOrDisconnected) {
|
||||||
command.disabled = command.slug !== 'power-on';
|
command.disabled = command.slug !== 'power-on' && command.slug !== 'create-image';
|
||||||
} else if (allSameState) {
|
} else if (allSameState) {
|
||||||
if (states[0] === 'off' || states[0] === 'disconnected') {
|
if (states[0] === 'off' || states[0] === 'disconnected') {
|
||||||
command.disabled = command.slug !== 'power-on';
|
command.disabled = !['power-on', 'create-image'].includes(command.slug);
|
||||||
} else {
|
} else {
|
||||||
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'remove-cache-image', 'partition', 'run-script', 'software-inventory'].includes(command.slug);
|
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'remove-cache-image', 'partition', 'run-script', 'software-inventory'].includes(command.slug);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (command.slug === 'create-image'|| command.slug === 'software-inventory') {
|
if (command.slug === 'software-inventory') {
|
||||||
command.disabled = multipleClients;
|
command.disabled = multipleClients;
|
||||||
} else if (
|
} else if (
|
||||||
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'remove-cache-image', 'run-script'].includes(command.slug)
|
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'remove-cache-image', 'run-script', 'create-image'].includes(command.slug)
|
||||||
) {
|
) {
|
||||||
command.disabled = false;
|
command.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -184,6 +184,21 @@ mat-dialog-content {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overview-card.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card.clickable:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card.clickable:active {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
.overview-icon {
|
.overview-icon {
|
||||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
@ -215,6 +230,28 @@ mat-dialog-content {
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.click-hint {
|
||||||
|
margin-top: 0.5rem !important;
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
color: #667eea !important;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-icon {
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
width: 0.875rem !important;
|
||||||
|
height: 0.875rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card.clickable:hover .click-hint {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== BADGES DE ESTADO ===== */
|
/* ===== BADGES DE ESTADO ===== */
|
||||||
.status-badge {
|
.status-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -554,4 +591,296 @@ mat-dialog-content {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== SECCIÓN DE LOGS ===== */
|
||||||
|
.logs-section {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header h3 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header h3::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ogboot-logs-iframe {
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dhcp-logs-iframe {
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive para logs */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.logs-section {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-container {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ogboot-logs-iframe {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 150px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== TARJETAS ACTIVAS ===== */
|
||||||
|
.overview-card.clickable.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card.clickable.active .overview-icon {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card.clickable.active .status-badge {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card.clickable.active .click-hint {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card.clickable.active h3 {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card.clickable.active p {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== CONTENIDO DINÁMICO ===== */
|
||||||
|
.dynamic-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: fadeInUp 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 24px;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||||
|
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button mat-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive para el header de sección */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button {
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button mat-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 24px;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||||
|
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button mat-icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive para el header de sección */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button {
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-button mat-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- Header con bienvenida -->
|
|
||||||
<div class="welcome-header">
|
<div class="welcome-header">
|
||||||
<div class="welcome-content">
|
<div class="welcome-content">
|
||||||
<div class="welcome-icon">
|
<div class="welcome-icon">
|
||||||
|
@ -17,9 +16,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contenido principal -->
|
|
||||||
<mat-dialog-content [ngClass]="{'loading': loading}">
|
<mat-dialog-content [ngClass]="{'loading': loading}">
|
||||||
<!-- Spinner de carga -->
|
|
||||||
<div class="spinner-container" *ngIf="loading">
|
<div class="spinner-container" *ngIf="loading">
|
||||||
<div class="loading-content">
|
<div class="loading-content">
|
||||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||||
|
@ -27,11 +24,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contenido principal cuando no está cargando -->
|
|
||||||
<div *ngIf="!loading" class="main-content">
|
<div *ngIf="!loading" class="main-content">
|
||||||
<!-- Resumen rápido del sistema -->
|
|
||||||
<div class="system-overview">
|
<div class="system-overview">
|
||||||
<div class="overview-card">
|
<div class="overview-card clickable"
|
||||||
|
[ngClass]="{'active': selectedSection === 'repositories'}"
|
||||||
|
(click)="selectSection('repositories')">
|
||||||
<div class="overview-icon">
|
<div class="overview-icon">
|
||||||
<mat-icon>cloud</mat-icon>
|
<mat-icon>cloud</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,7 +38,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overview-card">
|
<div class="overview-card clickable"
|
||||||
|
[ngClass]="{'active': selectedSection === 'ogboot'}"
|
||||||
|
(click)="selectSection('ogboot')">
|
||||||
<div class="overview-icon">
|
<div class="overview-icon">
|
||||||
<mat-icon>storage</mat-icon>
|
<mat-icon>storage</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
|
@ -52,7 +51,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overview-card">
|
<div class="overview-card clickable"
|
||||||
|
[ngClass]="{'active': selectedSection === 'dhcp'}"
|
||||||
|
(click)="selectSection('dhcp')">
|
||||||
<div class="overview-icon">
|
<div class="overview-icon">
|
||||||
<mat-icon>router</mat-icon>
|
<mat-icon>router</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,11 +63,22 @@
|
||||||
<p *ngIf="errorDhcp">Estado: <span class="status-badge offline">Error</span></p>
|
<p *ngIf="errorDhcp">Estado: <span class="status-badge offline">Error</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="overview-card clickable"
|
||||||
|
[ngClass]="{'active': selectedSection === 'ogcore'}"
|
||||||
|
(click)="selectSection('ogcore')">
|
||||||
|
<div class="overview-icon">
|
||||||
|
<mat-icon>dns</mat-icon>
|
||||||
|
</div>
|
||||||
|
<div class="overview-content">
|
||||||
|
<h3>OgCore Server</h3>
|
||||||
|
<p>Logs del servidor</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs principales -->
|
<div class="dynamic-content">
|
||||||
<mat-tab-group (selectedTabChange)="onTabChange($event)" class="main-tabs">
|
<div *ngIf="selectedSection === 'repositories'" class="section-content">
|
||||||
<mat-tab label="{{ 'repositoryLabel' | translate }}">
|
|
||||||
<div class="repositories-container">
|
<div class="repositories-container">
|
||||||
<div *ngIf="repositories.length === 0" class="no-repositories">
|
<div *ngIf="repositories.length === 0" class="no-repositories">
|
||||||
<mat-icon class="no-data-icon">cloud_off</mat-icon>
|
<mat-icon class="no-data-icon">cloud_off</mat-icon>
|
||||||
|
@ -75,7 +87,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="repositories.length > 0" class="repositories-selector-container">
|
<div *ngIf="repositories.length > 0" class="repositories-selector-container">
|
||||||
<!-- Selector de repositorio -->
|
|
||||||
<div class="repository-selector">
|
<div class="repository-selector">
|
||||||
<mat-form-field appearance="outline" class="repository-select-field">
|
<mat-form-field appearance="outline" class="repository-select-field">
|
||||||
<mat-label>Seleccionar repositorio</mat-label>
|
<mat-label>Seleccionar repositorio</mat-label>
|
||||||
|
@ -88,11 +99,14 @@
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Información del repositorio seleccionado -->
|
|
||||||
<div *ngIf="selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]" class="selected-repository-content">
|
<div *ngIf="selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]" class="selected-repository-content">
|
||||||
<div class="repository-item">
|
<div class="repository-item">
|
||||||
<div class="repository-header">
|
<div class="section-header">
|
||||||
<h3>{{ getSelectedRepositoryName() }}</h3>
|
<h2>{{ getSelectedRepositoryName() }}</h2>
|
||||||
|
<button class="logs-button" (click)="scrollToLogs()">
|
||||||
|
<mat-icon>article</mat-icon>
|
||||||
|
<span>Ver logs</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="repository-content">
|
<div class="repository-content">
|
||||||
|
@ -111,16 +125,30 @@
|
||||||
</app-status-tab>
|
</app-status-tab>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="repository-logs-section" id="repository-logs-section">
|
||||||
|
<div class="logs-header">
|
||||||
|
<h3>Datos del repositorio: {{ getSelectedRepositoryName() }}</h3>
|
||||||
|
<p>Información específica del repositorio seleccionado</p>
|
||||||
|
</div>
|
||||||
|
<div class="logs-container">
|
||||||
|
<iframe
|
||||||
|
src="https://localhost:3030/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=ogrepository2&theme=dark&panelId=1&__feature.dashboardSceneSolo"
|
||||||
|
width="100%"
|
||||||
|
height="600"
|
||||||
|
frameborder="0"
|
||||||
|
class="repository-logs-iframe">
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mensaje cuando no hay repositorio seleccionado -->
|
|
||||||
<div *ngIf="!selectedRepositoryUuid" class="no-repository-selected">
|
<div *ngIf="!selectedRepositoryUuid" class="no-repository-selected">
|
||||||
<mat-icon class="no-data-icon">storage</mat-icon>
|
<mat-icon class="no-data-icon">storage</mat-icon>
|
||||||
<h3>Selecciona un repositorio</h3>
|
<h3>Selecciona un repositorio</h3>
|
||||||
<p>Elige un repositorio de la lista para ver su estado detallado.</p>
|
<p>Elige un repositorio de la lista para ver su estado detallado.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error al cargar repositorio -->
|
|
||||||
<div *ngIf="selectedRepositoryUuid && errorRepositories[selectedRepositoryUuid]" class="error-container">
|
<div *ngIf="selectedRepositoryUuid && errorRepositories[selectedRepositoryUuid]" class="error-container">
|
||||||
<div class="error-card">
|
<div class="error-card">
|
||||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||||
|
@ -134,15 +162,39 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-tab>
|
</div>
|
||||||
|
|
||||||
<mat-tab label="OgBoot Server">
|
<div *ngIf="selectedSection === 'ogboot'" class="section-content">
|
||||||
<div *ngIf="!errorOgBoot && !loadingOgBoot" class="tab-content">
|
<div *ngIf="!errorOgBoot && !loadingOgBoot" class="tab-content">
|
||||||
<app-status-tab [loading]="loadingOgBoot" [diskUsage]="ogBootDiskUsage" [servicesStatus]="ogBootServicesStatus"
|
<div class="section-header">
|
||||||
[installedOgLives]="installedOgLives" [diskUsageChartData]="ogBootDiskUsageChartData" [view]="view"
|
<h2>OgBoot Server</h2>
|
||||||
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="isDhcp"
|
<button class="logs-button" (click)="scrollToLogs()">
|
||||||
[isRepository]="false">
|
<mat-icon>article</mat-icon>
|
||||||
</app-status-tab>
|
<span>Ver logs</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-status-tab [loading]="loadingOgBoot" [diskUsage]="ogBootDiskUsage" [servicesStatus]="ogBootServicesStatus"
|
||||||
|
[installedOgLives]="installedOgLives" [diskUsageChartData]="ogBootDiskUsageChartData" [view]="view"
|
||||||
|
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="isDhcp"
|
||||||
|
[isRepository]="false">
|
||||||
|
</app-status-tab>
|
||||||
|
|
||||||
|
<div class="logs-section" id="ogboot-logs-section">
|
||||||
|
<div class="logs-header">
|
||||||
|
<h3>Logs de OgBoot</h3>
|
||||||
|
<p>Logs en tiempo real del servidor OgBoot</p>
|
||||||
|
</div>
|
||||||
|
<div class="logs-container">
|
||||||
|
<iframe
|
||||||
|
src="https://localhost:3030/d-solo/ogboot-logs/ogboot-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo"
|
||||||
|
width="100%"
|
||||||
|
height="800"
|
||||||
|
frameborder="0"
|
||||||
|
class="ogboot-logs-iframe">
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="loadingOgBoot" class="loading-container">
|
<div *ngIf="loadingOgBoot" class="loading-container">
|
||||||
<div class="loading-content">
|
<div class="loading-content">
|
||||||
|
@ -161,14 +213,38 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-tab>
|
</div>
|
||||||
|
|
||||||
<mat-tab label="DHCP Server">
|
<div *ngIf="selectedSection === 'dhcp'" class="section-content">
|
||||||
<div *ngIf="!errorDhcp && !loadingDhcp" class="tab-content">
|
<div *ngIf="!errorDhcp && !loadingDhcp" class="tab-content">
|
||||||
<app-status-tab [loading]="loadingDhcp" [diskUsage]="dhcpDiskUsage" [servicesStatus]="dhcpServicesStatus"
|
<div class="section-header">
|
||||||
[subnets]="subnets" [diskUsageChartData]="dhcpDiskUsageChartData" [view]="view" [colorScheme]="colorScheme"
|
<h2>DHCP Server</h2>
|
||||||
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="true" [isRepository]="false">
|
<button class="logs-button" (click)="scrollToLogs()">
|
||||||
</app-status-tab>
|
<mat-icon>article</mat-icon>
|
||||||
|
<span>Ver logs</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-status-tab [loading]="loadingDhcp" [diskUsage]="dhcpDiskUsage" [servicesStatus]="dhcpServicesStatus"
|
||||||
|
[subnets]="subnets" [diskUsageChartData]="dhcpDiskUsageChartData" [view]="view" [colorScheme]="colorScheme"
|
||||||
|
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="true" [isRepository]="false">
|
||||||
|
</app-status-tab>
|
||||||
|
|
||||||
|
<div class="logs-section" id="dhcp-logs-section">
|
||||||
|
<div class="logs-header">
|
||||||
|
<h3>Logs de DHCP</h3>
|
||||||
|
<p>Logs en tiempo real del servidor DHCP</p>
|
||||||
|
</div>
|
||||||
|
<div class="logs-container">
|
||||||
|
<iframe
|
||||||
|
src="https://localhost:3030/d-solo/ogdhcp-logs/ogdhcp-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo"
|
||||||
|
width="100%"
|
||||||
|
height="800"
|
||||||
|
frameborder="0"
|
||||||
|
class="dhcp-logs-iframe">
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="loadingDhcp" class="loading-container">
|
<div *ngIf="loadingDhcp" class="loading-container">
|
||||||
<div class="loading-content">
|
<div class="loading-content">
|
||||||
|
@ -187,12 +263,36 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-tab>
|
</div>
|
||||||
</mat-tab-group>
|
|
||||||
|
<div *ngIf="selectedSection === 'ogcore'" class="section-content">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>OgCore Server</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="logs-section" id="ogcore-logs-section">
|
||||||
|
<div class="logs-header">
|
||||||
|
<h3>Logs de OgCore</h3>
|
||||||
|
<p>Logs en tiempo real del servidor OgCore</p>
|
||||||
|
</div>
|
||||||
|
<div class="logs-container">
|
||||||
|
<iframe
|
||||||
|
src="https://localhost:3030/d-solo/ogcore-logs/ogcore-logs?orgId=1&tab=transformations&theme=dark&panelId=1&__feature.dashboardSceneSolo&now-5m&to=now&timezone=browser"
|
||||||
|
width="100%"
|
||||||
|
height="800"
|
||||||
|
frameborder="0"
|
||||||
|
class="ogcore-logs-iframe">
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
|
|
||||||
<!-- Footer con acciones -->
|
|
||||||
<mat-dialog-actions class="action-container">
|
<mat-dialog-actions class="action-container">
|
||||||
<div class="action-info">
|
<div class="action-info">
|
||||||
<p class="last-update">Última actualización: {{ lastUpdateTime }}</p>
|
<p class="last-update">Última actualización: {{ lastUpdateTime }}</p>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { ConfigService } from '@services/config.service';
|
import { ConfigService } from '@services/config.service';
|
||||||
import { MatTabChangeEvent } from '@angular/material/tabs';
|
import { ToastrService } from "ngx-toastr";
|
||||||
import {ToastrService} from "ngx-toastr";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-global-status',
|
selector: 'app-global-status',
|
||||||
|
@ -31,6 +30,7 @@ export class GlobalStatusComponent implements OnInit {
|
||||||
repositoryStatuses: { [key: string]: any } = {};
|
repositoryStatuses: { [key: string]: any } = {};
|
||||||
lastUpdateTime: string = '';
|
lastUpdateTime: string = '';
|
||||||
selectedRepositoryUuid: string = '';
|
selectedRepositoryUuid: string = '';
|
||||||
|
selectedSection: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore' = 'repositories';
|
||||||
|
|
||||||
ogBootApiUrl: string;
|
ogBootApiUrl: string;
|
||||||
ogBootDiskUsage: any = {};
|
ogBootDiskUsage: any = {};
|
||||||
|
@ -44,7 +44,8 @@ export class GlobalStatusComponent implements OnInit {
|
||||||
isDhcp: boolean = false;
|
isDhcp: boolean = false;
|
||||||
isRepository: boolean = false;
|
isRepository: boolean = false;
|
||||||
|
|
||||||
// Loading específicos para cada sección
|
ogCoreApiUrl: string;
|
||||||
|
|
||||||
loadingOgBootOgLives: boolean = false;
|
loadingOgBootOgLives: boolean = false;
|
||||||
loadingOgBootServices: boolean = false;
|
loadingOgBootServices: boolean = false;
|
||||||
loadingOgBootDisk: boolean = false;
|
loadingOgBootDisk: boolean = false;
|
||||||
|
@ -60,6 +61,7 @@ export class GlobalStatusComponent implements OnInit {
|
||||||
this.baseUrl = this.configService.apiUrl;
|
this.baseUrl = this.configService.apiUrl;
|
||||||
this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
|
this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
|
||||||
this.dhcpApiUrl = `${this.baseUrl}/og-dhcp/status`;
|
this.dhcpApiUrl = `${this.baseUrl}/og-dhcp/status`;
|
||||||
|
this.ogCoreApiUrl = `${this.baseUrl}`;
|
||||||
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
|
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
|
||||||
|
|
||||||
this.ogBootDiskUsageChartData = [];
|
this.ogBootDiskUsageChartData = [];
|
||||||
|
@ -290,23 +292,9 @@ export class GlobalStatusComponent implements OnInit {
|
||||||
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorDhcp', false);
|
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorDhcp', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTabChange(event: MatTabChangeEvent): void {
|
|
||||||
switch (event.index) {
|
|
||||||
case 0:
|
|
||||||
if (this.repositories.length === 0) {
|
|
||||||
this.loadRepositories(false);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
this.loadOgBootStatus();
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
this.loadDhcpStatus();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onRepositoryChange(repositoryUuid: string): void {
|
onRepositoryChange(repositoryUuid: string): void {
|
||||||
this.selectedRepositoryUuid = repositoryUuid;
|
this.selectedRepositoryUuid = repositoryUuid;
|
||||||
|
@ -324,6 +312,15 @@ export class GlobalStatusComponent implements OnInit {
|
||||||
return selectedRepo ? selectedRepo.name : '';
|
return selectedRepo ? selectedRepo.name : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRepositoryIframeUrl(): string {
|
||||||
|
const repositoryName = this.getSelectedRepositoryName();
|
||||||
|
if (repositoryName) {
|
||||||
|
const encodedName = encodeURIComponent(repositoryName);
|
||||||
|
return `https://localhost:3030/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
refreshAll(): void {
|
refreshAll(): void {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.updateLastUpdateTime();
|
this.updateLastUpdateTime();
|
||||||
|
@ -364,4 +361,71 @@ export class GlobalStatusComponent implements OnInit {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
selectSection(section: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore'): void {
|
||||||
|
this.selectedSection = section;
|
||||||
|
|
||||||
|
switch (section) {
|
||||||
|
case 'repositories':
|
||||||
|
break;
|
||||||
|
case 'ogboot':
|
||||||
|
if (!this.ogBootDiskUsage) {
|
||||||
|
this.loadOgBootStatus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'dhcp':
|
||||||
|
if (!this.dhcpDiskUsage) {
|
||||||
|
this.loadDhcpStatus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ogcore':
|
||||||
|
// No se necesita cargar nada, solo mostrar los logs
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToLogs(): void {
|
||||||
|
let logsSectionId: string;
|
||||||
|
switch (this.selectedSection) {
|
||||||
|
case 'repositories':
|
||||||
|
logsSectionId = 'repository-logs-section';
|
||||||
|
break;
|
||||||
|
case 'ogboot':
|
||||||
|
logsSectionId = 'ogboot-logs-section';
|
||||||
|
break;
|
||||||
|
case 'dhcp':
|
||||||
|
logsSectionId = 'dhcp-logs-section';
|
||||||
|
break;
|
||||||
|
case 'ogcore':
|
||||||
|
logsSectionId = 'ogcore-logs-section';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkAndScroll = () => {
|
||||||
|
const logsSection = document.getElementById(logsSectionId);
|
||||||
|
if (logsSection) {
|
||||||
|
logsSection.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (checkAndScroll()) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(interval);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<app-loading [isLoading]="loading"></app-loading>
|
<app-loading [isLoading]="loading"></app-loading>
|
||||||
|
|
||||||
<div *ngIf="!loading" class="dashboard">
|
<div *ngIf="!loading" class="dashboard">
|
||||||
<!-- Sección de uso de recursos -->
|
|
||||||
<div class="resources-section">
|
<div class="resources-section">
|
||||||
<!-- Disk Usage Section -->
|
|
||||||
<div class="resource-card disk-usage-container">
|
<div class="resource-card disk-usage-container">
|
||||||
<div class="resource-header">
|
<div class="resource-header">
|
||||||
<div class="resource-icon">
|
<div class="resource-icon">
|
||||||
|
@ -32,13 +30,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">{{ 'usedPercentageLabel' | translate }}:</span>
|
<span class="info-label">{{ 'usedPercentageLabel' | translate }}:</span>
|
||||||
<span class="info-value usage-percentage">{{ isRepository ? diskUsage.used_percentage : diskUsage.percentage }}%</span>
|
<span class="info-value usage-percentage">{{ isRepository ? diskUsage.used_percentage : diskUsage.percentage }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RAM Usage Section -->
|
|
||||||
<div class="resource-card ram-usage-container" *ngIf="isRepository">
|
<div class="resource-card ram-usage-container" *ngIf="isRepository">
|
||||||
<div class="resource-header">
|
<div class="resource-header">
|
||||||
<div class="resource-icon">
|
<div class="resource-icon">
|
||||||
|
@ -67,13 +64,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">{{ 'usedPercentageLabel' | translate }}:</span>
|
<span class="info-label">{{ 'usedPercentageLabel' | translate }}:</span>
|
||||||
<span class="info-value usage-percentage">{{ ramUsage.used_percentage }}%</span>
|
<span class="info-value usage-percentage">{{ ramUsage.used_percentage }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CPU Usage Section -->
|
|
||||||
<div class="resource-card cpu-usage-container" *ngIf="isRepository">
|
<div class="resource-card cpu-usage-container" *ngIf="isRepository">
|
||||||
<div class="resource-header">
|
<div class="resource-header">
|
||||||
<div class="resource-icon">
|
<div class="resource-icon">
|
||||||
|
@ -84,7 +80,7 @@
|
||||||
<div class="resource-content">
|
<div class="resource-content">
|
||||||
<div class="cpu-usage-display">
|
<div class="cpu-usage-display">
|
||||||
<div class="cpu-circle">
|
<div class="cpu-circle">
|
||||||
<div class="cpu-percentage">{{ cpuUsage.used_percentage }}%</div>
|
<div class="cpu-percentage">{{ cpuUsage.used_percentage }}</div>
|
||||||
<div class="cpu-label">Uso actual</div>
|
<div class="cpu-label">Uso actual</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -92,9 +88,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sección de servicios y procesos -->
|
|
||||||
<div class="services-section">
|
<div class="services-section">
|
||||||
<!-- Services Status Section -->
|
|
||||||
<div class="service-card services-status" joyrideStep="servicesStatusStep" text="{{ 'servicesStatusDescription' | translate }}">
|
<div class="service-card services-status" joyrideStep="servicesStatusStep" text="{{ 'servicesStatusDescription' | translate }}">
|
||||||
<div class="service-header">
|
<div class="service-header">
|
||||||
<div class="service-icon">
|
<div class="service-icon">
|
||||||
|
@ -116,7 +110,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Processes Status Section -->
|
|
||||||
<div class="service-card processes-status" *ngIf="isRepository">
|
<div class="service-card processes-status" *ngIf="isRepository">
|
||||||
<div class="service-header">
|
<div class="service-header">
|
||||||
<div class="service-icon">
|
<div class="service-icon">
|
||||||
|
@ -139,9 +132,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sección de datos específicos -->
|
|
||||||
<div class="data-section">
|
<div class="data-section">
|
||||||
<!-- Installed OgLives Section -->
|
|
||||||
<div class="data-card" *ngIf="!isRepository && !isDhcp">
|
<div class="data-card" *ngIf="!isRepository && !isDhcp">
|
||||||
<div class="data-header">
|
<div class="data-header">
|
||||||
<div class="data-icon">
|
<div class="data-icon">
|
||||||
|
@ -171,7 +162,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Subnets Section -->
|
|
||||||
<div class="data-card" *ngIf="isDhcp">
|
<div class="data-card" *ngIf="isDhcp">
|
||||||
<div class="data-header">
|
<div class="data-header">
|
||||||
<div class="data-icon">
|
<div class="data-icon">
|
||||||
|
|
|
@ -675,3 +675,340 @@ mat-form-field {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.scroll-to-top-button {
|
||||||
|
right: 16px;
|
||||||
|
bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para el selector de ramas */
|
||||||
|
.branch-selector-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-branch-button {
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-branch-button:hover:not(:disabled) {
|
||||||
|
background: #45a049;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-branch-button:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-branch-button mat-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para la sección de commits */
|
||||||
|
.commits-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-string {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commits-table-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-id {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-message {
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-stats {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-tags mat-chip {
|
||||||
|
font-size: 10px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tags {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons button {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons button:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginator-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive para ramas y commits */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.branch-selector-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-branch-button {
|
||||||
|
height: auto;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commits-section {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-message {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
color: #495057;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
animation: fadeInUp 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-grow: 1;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-loading-spinner {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-content {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 12px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-item-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #495057;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.git-data-display {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #495057;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para la advertencia de repositorio no encontrado */
|
||||||
|
.repository-warning {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: #f39c12;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-message {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive para el cuadro informativo de Git */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.git-info-card {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-loading-spinner {
|
||||||
|
margin-left: 0;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-data-display {
|
||||||
|
font-size: 11px;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-item {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-info-value {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
<!-- Overlay de carga para creación de repositorio -->
|
<app-loading [isLoading]="loading"></app-loading>
|
||||||
|
|
||||||
<app-modal-overlay
|
<app-modal-overlay
|
||||||
[isVisible]="creatingRepository"
|
[isVisible]="creatingRepository"
|
||||||
message="Creando repositorio...">
|
message="Creando repositorio...">
|
||||||
|
@ -20,12 +21,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button class="action-button" id="execute-button" [disabled]="!selectedPartition" (click)="save()">Ejecutar</button>
|
<button class="action-button" id="execute-button" [disabled]="!selectedPartition || loading" (click)="save()">Ejecutar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="select-container">
|
<div class="select-container">
|
||||||
<!-- Sección: Configuración de tipo de imagen -->
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="form-section-title">
|
<div class="form-section-title">
|
||||||
<mat-icon>settings</mat-icon>
|
<mat-icon>settings</mat-icon>
|
||||||
|
@ -43,15 +43,81 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sección: Configuración Git (solo para tipo git) -->
|
|
||||||
<div class="form-section" *ngIf="imageType === 'git'">
|
<div class="form-section" *ngIf="imageType === 'git'">
|
||||||
<div class="form-section-title">
|
<div class="form-section-title">
|
||||||
<mat-icon>code</mat-icon>
|
<mat-icon>code</mat-icon>
|
||||||
Configuración Git
|
Configuración Git
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="git-repository-section">
|
<div class="git-info-container" *ngIf="selectedPartition && imageType === 'git' && (hasValidGitData || loadingGitData)">
|
||||||
<div class="repository-header">
|
<div class="git-info-card">
|
||||||
|
<div class="git-info-header">
|
||||||
|
<mat-icon class="git-info-icon">info</mat-icon>
|
||||||
|
<span class="git-info-title">La imagen en caché a actualizar es:</span>
|
||||||
|
<mat-spinner *ngIf="loadingGitData" diameter="20" class="git-loading-spinner"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
<div class="" *ngIf="!loadingGitData && hasValidGitData">
|
||||||
|
<div class="git-info-grid">
|
||||||
|
<div class="git-info-item">
|
||||||
|
<mat-form-field appearance="outline" class="git-form-field">
|
||||||
|
<mat-label>
|
||||||
|
<mat-icon class="git-item-icon">folder</mat-icon>
|
||||||
|
Repositorio
|
||||||
|
</mat-label>
|
||||||
|
<input matInput [value]="gitData.repo" readonly>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="git-info-item">
|
||||||
|
<mat-form-field appearance="outline" class="git-form-field">
|
||||||
|
<mat-label>
|
||||||
|
<mat-icon class="git-item-icon">account_tree</mat-icon>
|
||||||
|
Rama origen
|
||||||
|
</mat-label>
|
||||||
|
<input matInput [value]="gitData.branch" readonly>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="git-info-item">
|
||||||
|
<mat-form-field appearance="outline" class="git-form-field">
|
||||||
|
<mat-label>
|
||||||
|
<mat-icon class="git-item-icon">call_split</mat-icon>
|
||||||
|
Rama destino
|
||||||
|
</mat-label>
|
||||||
|
<input matInput
|
||||||
|
[(ngModel)]="destinationBranch"
|
||||||
|
[readonly]="!isDestinationBranchEditable"
|
||||||
|
placeholder="Nombre de la rama destino"
|
||||||
|
[matTooltip]="isDestinationBranchEditable ? 'Guardar cambios' : 'Editar nombre de la rama destino'">
|
||||||
|
<button mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
[matTooltip]="isDestinationBranchEditable ? 'Guardar' : 'Editar rama destino'"
|
||||||
|
(click)="toggleDestinationBranchEdit()">
|
||||||
|
<mat-icon>{{ isDestinationBranchEditable ? 'save' : 'edit' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="repository-warning" *ngIf="repositoryNotFound">
|
||||||
|
<mat-icon class="warning-icon">warning</mat-icon>
|
||||||
|
<div class="warning-content">
|
||||||
|
<div class="warning-title">Repositorio no encontrado</div>
|
||||||
|
<div class="warning-message">
|
||||||
|
El repositorio "{{ gitData.repo }}" no existe en el listado de repositorios disponibles.
|
||||||
|
Es posible que necesites crearlo primero.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="git-info-loading" *ngIf="loadingGitData">
|
||||||
|
<span>Cargando información de Git...</span>
|
||||||
|
</div>
|
||||||
|
<div class="git-info-empty" *ngIf="!loadingGitData && !hasValidGitData">
|
||||||
|
<span>No se encontró información de Git para esta partición</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="repository-header" *ngIf="!hasValidGitData">
|
||||||
<button *ngIf="imageType === 'git'"
|
<button *ngIf="imageType === 'git'"
|
||||||
class="create-repository-button"
|
class="create-repository-button"
|
||||||
(click)="openCreateRepositoryModal()"
|
(click)="openCreateRepositoryModal()"
|
||||||
|
@ -61,95 +127,82 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="selector">
|
<div class="selector" *ngIf="!hasValidGitData">
|
||||||
<mat-form-field appearance="fill" class="full-width">
|
<mat-form-field appearance="fill" class="half-width">
|
||||||
<mat-label>Seleccionar repositorio Git</mat-label>
|
<mat-label>Seleccionar repositorio Git</mat-label>
|
||||||
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositorySelected($event.value)" required>
|
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositorySelected($event.value)" required>
|
||||||
<mat-option [value]="null">Seleccionar repositorio git / SO</mat-option>
|
<mat-option [value]="null">Seleccionar repositorio git / SO</mat-option>
|
||||||
<mat-option *ngFor="let repo of gitRepositories" [value]="repo">{{ repo.name }}</mat-option>
|
<mat-option *ngFor="let repo of gitRepositories" [value]="repo">{{ repo.name }}</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
<mat-spinner *ngIf="loadingGitRepositories" matSuffix diameter="20"></mat-spinner>
|
<mat-spinner *ngIf="loadingGitRepositories" matSuffix diameter="20"></mat-spinner>
|
||||||
<mat-hint>
|
<mat-hint>
|
||||||
<mat-icon>info</mat-icon>
|
<mat-icon>info</mat-icon>
|
||||||
Selecciona el repositorio git para obtener las imágenes disponibles.
|
Selecciona el repositorio git para obtener las imágenes disponibles.
|
||||||
<span *ngIf="gitRepositories.length === 0" class="no-repositories-hint">
|
<span *ngIf="gitRepositories.length === 0" class="no-repositories-hint">
|
||||||
No hay repositorios disponibles. Crea uno nuevo para continuar.
|
No hay repositorios disponibles. Crea uno nuevo para continuar.
|
||||||
</span>
|
</span>
|
||||||
</mat-hint>
|
</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Opciones de acción Git -->
|
<button class="create-branch-button"
|
||||||
<div class="git-action-selector">
|
*ngIf="selectedGitRepository"
|
||||||
<div class="action-chips-container">
|
(click)="openCreateBranchModal()"
|
||||||
<mat-chip-listbox [(ngModel)]="gitAction" required class="action-chip-listbox">
|
[disabled]="loadingBranches"
|
||||||
<mat-chip-option value="create" class="action-chip create-chip firmware-chip" (click)="onGitActionSelected({value: 'create'})">
|
matTooltip="Crear nueva rama"
|
||||||
<span>Crear imagen</span>
|
style="display: none;">
|
||||||
</mat-chip-option>
|
<mat-icon>add</mat-icon>
|
||||||
<mat-chip-option value="update" class="action-chip update-chip firmware-chip" (click)="onGitActionSelected({value: 'update'})">
|
<span>Crear rama</span>
|
||||||
<span>Actualizar imagen</span>
|
</button>
|
||||||
</mat-chip-option>
|
|
||||||
</mat-chip-listbox>
|
|
||||||
</div>
|
|
||||||
<div class="action-hint">
|
|
||||||
<mat-icon>info</mat-icon>
|
|
||||||
<span *ngIf="gitAction === 'create'">Crea una nueva imagen con el nombre especificado</span>
|
|
||||||
<span *ngIf="gitAction === 'update'">Actualiza una imagen existente seleccionada</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sección: Configuración general -->
|
|
||||||
<div class="form-section" *ngIf="imageType !== 'git'">
|
|
||||||
<div class="form-section-title">
|
|
||||||
<mat-icon>image</mat-icon>
|
|
||||||
Configuración de imagen
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Opciones de acción para imágenes monolíticas -->
|
|
||||||
<div class="action-chips-container">
|
|
||||||
<mat-chip-listbox [(ngModel)]="monolithicAction" required class="action-chip-listbox">
|
|
||||||
<mat-chip-option value="create" class="action-chip create-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'create'})">
|
|
||||||
<span>Crear imagen</span>
|
|
||||||
</mat-chip-option>
|
|
||||||
<mat-chip-option value="update" class="action-chip update-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'update'})">
|
|
||||||
<span>Actualizar imagen</span>
|
|
||||||
</mat-chip-option>
|
|
||||||
</mat-chip-listbox>
|
|
||||||
</div>
|
|
||||||
<div class="action-hint">
|
|
||||||
<mat-icon>info</mat-icon>
|
|
||||||
<span *ngIf="monolithicAction === 'create'">Crea una nueva imagen con el nombre especificado</span>
|
|
||||||
<span *ngIf="monolithicAction === 'update'">Actualiza una imagen existente seleccionada</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="selector" *ngIf="monolithicAction === 'create'">
|
|
||||||
<mat-form-field appearance="fill" class="half-width">
|
|
||||||
<mat-label>Nombre canónico</mat-label>
|
|
||||||
<input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
|
|
||||||
<mat-hint>Introduce el nombre para la nueva imagen que se creará.</mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="selector" *ngIf="monolithicAction === 'update'">
|
|
||||||
<mat-form-field appearance="fill" class="half-width">
|
|
||||||
<mat-label>Seleccione imagen</mat-label>
|
|
||||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required>
|
|
||||||
<mat-option [value]="null">Seleccionar imagen para actualizar</mat-option>
|
|
||||||
<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"
|
|
||||||
(click)="selectedImage = null; resetCanonicalName()">
|
|
||||||
<mat-icon>close</mat-icon>
|
|
||||||
</button>
|
|
||||||
<mat-hint>Selecciona la imagen existente que quieres actualizar.</mat-hint>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sección: Selección de partición -->
|
<div class="form-section" *ngIf="imageType === 'monolithic'">
|
||||||
|
<div class="form-section-title">
|
||||||
|
<mat-icon>image</mat-icon>
|
||||||
|
Configuración de imagen monolítica
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-chips-container">
|
||||||
|
<mat-chip-listbox [(ngModel)]="monolithicAction" required class="action-chip-listbox">
|
||||||
|
<mat-chip-option value="create" class="action-chip create-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'create'})">
|
||||||
|
<span>Crear imagen</span>
|
||||||
|
</mat-chip-option>
|
||||||
|
<mat-chip-option value="update" class="action-chip update-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'update'})">
|
||||||
|
<span>Actualizar imagen</span>
|
||||||
|
</mat-chip-option>
|
||||||
|
</mat-chip-listbox>
|
||||||
|
</div>
|
||||||
|
<div class="action-hint">
|
||||||
|
<mat-icon>info</mat-icon>
|
||||||
|
<span *ngIf="monolithicAction === 'create'">Crea una nueva imagen con el nombre especificado</span>
|
||||||
|
<span *ngIf="monolithicAction === 'update'">Actualiza una imagen existente seleccionada</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selector" *ngIf="monolithicAction === 'create'">
|
||||||
|
<mat-form-field appearance="fill" class="half-width">
|
||||||
|
<mat-label>Nombre canónico</mat-label>
|
||||||
|
<input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
|
||||||
|
<mat-hint>Introduce el nombre para la nueva imagen que se creará.</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selector" *ngIf="monolithicAction === 'update'">
|
||||||
|
<mat-form-field appearance="fill" class="half-width">
|
||||||
|
<mat-label>Seleccione imagen</mat-label>
|
||||||
|
<mat-select [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required>
|
||||||
|
<mat-option [value]="null">Seleccionar imagen para actualizar</mat-option>
|
||||||
|
<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"
|
||||||
|
(click)="selectedImage = null; resetCanonicalName()">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-hint>Selecciona la imagen existente que quieres actualizar.</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-section" #partitionSection id="partition-selection">
|
<div class="form-section" #partitionSection id="partition-selection">
|
||||||
<div class="form-section-title">
|
<div class="form-section-title">
|
||||||
<mat-icon>storage</mat-icon>
|
<mat-icon>storage</mat-icon>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { ConfigService } from '@services/config.service';
|
||||||
import {MatDialog} from "@angular/material/dialog";
|
import {MatDialog} from "@angular/material/dialog";
|
||||||
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
|
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
|
||||||
import {CreateRepositoryModalComponent} from "./create-repository-modal/create-repository-modal.component";
|
import {CreateRepositoryModalComponent} from "./create-repository-modal/create-repository-modal.component";
|
||||||
|
import {CreateBranchModalComponent} from "../../../../repositories/show-git-images/create-branch-modal/create-branch-modal.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create-image',
|
selector: 'app-create-image',
|
||||||
|
@ -38,12 +39,27 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
loadingGitRepositories: boolean = false;
|
loadingGitRepositories: boolean = false;
|
||||||
loadingGitImageRepositories: boolean = false;
|
loadingGitImageRepositories: boolean = false;
|
||||||
creatingRepository: boolean = false;
|
creatingRepository: boolean = false;
|
||||||
gitAction: string = 'create';
|
|
||||||
monolithicAction: string = 'create';
|
monolithicAction: string = 'create';
|
||||||
existingImages: any[] = [];
|
existingImages: any[] = [];
|
||||||
selectedExistingImage: any = null;
|
selectedExistingImage: any = null;
|
||||||
loadingExistingImages: boolean = false;
|
loadingExistingImages: boolean = false;
|
||||||
dataSource = new MatTableDataSource<any>();
|
dataSource = new MatTableDataSource<any>();
|
||||||
|
|
||||||
|
branches: string[] = [];
|
||||||
|
selectedBranch: string = '';
|
||||||
|
loadingBranches: boolean = false;
|
||||||
|
|
||||||
|
gitData: any = null;
|
||||||
|
loadingGitData: boolean = false;
|
||||||
|
destinationBranch: string = '';
|
||||||
|
isDestinationBranchEditable: boolean = false;
|
||||||
|
repositoryNotFound: boolean = false;
|
||||||
|
newlyCreatedRepository: any = null;
|
||||||
|
|
||||||
|
get hasValidGitData(): boolean {
|
||||||
|
return this.gitData && this.gitData.repo && this.gitData.branch;
|
||||||
|
}
|
||||||
|
|
||||||
columns = [
|
columns = [
|
||||||
{
|
{
|
||||||
columnDef: 'diskNumber',
|
columnDef: 'diskNumber',
|
||||||
|
@ -107,9 +123,19 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
this.clientName = response.name;
|
this.clientName = response.name;
|
||||||
this.selectedRepository = response.repository;
|
this.selectedRepository = response.repository;
|
||||||
|
|
||||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
const validPartitions = response.partitions.filter((partition: any) => {
|
||||||
return partition.partitionNumber !== 0;
|
return partition.partitionNumber !== 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.dataSource.data = validPartitions;
|
||||||
|
|
||||||
|
const firstValidPartition = validPartitions.find((partition: any) => {
|
||||||
|
return partition.operativeSystem;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (firstValidPartition) {
|
||||||
|
this.selectedPartition = firstValidPartition;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
@ -141,6 +167,23 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
(response: any) => {
|
(response: any) => {
|
||||||
this.gitRepositories = response['hydra:member'];
|
this.gitRepositories = response['hydra:member'];
|
||||||
this.loadingGitRepositories = false;
|
this.loadingGitRepositories = false;
|
||||||
|
|
||||||
|
if (this.newlyCreatedRepository) {
|
||||||
|
|
||||||
|
const newRepo = this.gitRepositories.find(repo =>
|
||||||
|
repo.name === this.newlyCreatedRepository.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newRepo) {
|
||||||
|
this.selectedGitRepository = newRepo;
|
||||||
|
this.onGitRepositorySelected(newRepo);
|
||||||
|
this.toastService.success(`Repositorio "${newRepo.name}" preseleccionado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.newlyCreatedRepository = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkRepositoryExists();
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error('Error al cargar los repositorios git:', error);
|
console.error('Error al cargar los repositorios git:', error);
|
||||||
|
@ -151,7 +194,7 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
|
|
||||||
loadGitImageRepositories(gitRepository: any) {
|
loadGitImageRepositories(gitRepository: any) {
|
||||||
this.loadingGitImageRepositories = true;
|
this.loadingGitImageRepositories = true;
|
||||||
const url = `${this.baseUrl}/git-image-repositories?gitRepository.id=${gitRepository.id}&page=1&itemsPerPage=100`;
|
const url = `${this.baseUrl}/git-image-repositories?gitRepository.uuid=${gitRepository.uuid}&page=1&itemsPerPage=100`;
|
||||||
this.http.get(url).subscribe(
|
this.http.get(url).subscribe(
|
||||||
(response: any) => {
|
(response: any) => {
|
||||||
this.gitImageRepositories = response['hydra:member'];
|
this.gitImageRepositories = response['hydra:member'];
|
||||||
|
@ -168,47 +211,97 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
this.selectedGitRepository = gitRepository;
|
this.selectedGitRepository = gitRepository;
|
||||||
this.selectedExistingImage = null;
|
this.selectedExistingImage = null;
|
||||||
this.existingImages = [];
|
this.existingImages = [];
|
||||||
|
this.selectedBranch = '';
|
||||||
|
this.branches = [];
|
||||||
|
|
||||||
if (gitRepository) {
|
if (gitRepository) {
|
||||||
this.loadGitImageRepositories(gitRepository);
|
this.loadGitImageRepositories(gitRepository);
|
||||||
|
this.loadBranches();
|
||||||
} else {
|
} else {
|
||||||
this.gitImageRepositories = [];
|
this.gitImageRepositories = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onGitActionSelected(event: any) {
|
loadBranches(): void {
|
||||||
console.log('onGitActionSelected llamado con:', event);
|
if (!this.selectedGitRepository) {
|
||||||
this.gitAction = event.value;
|
return;
|
||||||
this.selectedExistingImage = null;
|
|
||||||
this.gitImageName = '';
|
|
||||||
|
|
||||||
// Si se selecciona 'update' y ya hay un repositorio Git seleccionado, cargar los repositorios de imágenes
|
|
||||||
if (event.value === 'update' && this.selectedGitRepository) {
|
|
||||||
this.loadGitImageRepositories(this.selectedGitRepository);
|
|
||||||
}
|
}
|
||||||
|
this.loadingBranches = true;
|
||||||
console.log('Antes del setTimeout');
|
const url = `${this.baseUrl}/image-repositories/server/git/${this.selectedRepository.uuid}/branches`;
|
||||||
// Hacer scroll hacia la sección de partición después de un delay más largo
|
this.http.post<any>(url, { repositoryName: this.selectedGitRepository.name }).subscribe(
|
||||||
setTimeout(() => {
|
data => {
|
||||||
console.log('Dentro del setTimeout, llamando a scrollToPartitionSection');
|
this.branches = data.branches || [];
|
||||||
this.scrollToPartitionSection();
|
this.loadingBranches = false;
|
||||||
}, 300);
|
if (this.branches.length > 0) {
|
||||||
|
this.selectedBranch = this.branches[0];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.error('Error fetching branches', error);
|
||||||
|
this.toastService.error('Error al cargar las ramas del repositorio');
|
||||||
|
this.loadingBranches = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBranchChange(): void {
|
||||||
|
// La rama ha sido seleccionada, no necesitamos hacer nada más
|
||||||
|
}
|
||||||
|
|
||||||
|
openCreateBranchModal(): void {
|
||||||
|
if (this.hasValidGitData) {
|
||||||
|
const dialogRef = this.dialog.open(CreateBranchModalComponent, {
|
||||||
|
width: '500px',
|
||||||
|
data: {
|
||||||
|
commit: this.gitData.branch,
|
||||||
|
repositoryName: this.gitData.repo,
|
||||||
|
repositoryUuid: this.selectedRepository.uuid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
this.toastService.success('Rama creada correctamente');
|
||||||
|
this.destinationBranch = result.branchName || result;
|
||||||
|
this.loadBranches();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selectedGitRepository) {
|
||||||
|
this.toastService.error('Debe seleccionar un repositorio primero');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(CreateBranchModalComponent, {
|
||||||
|
width: '500px',
|
||||||
|
data: {
|
||||||
|
commit: this.selectedBranch || 'master',
|
||||||
|
repositoryName: this.selectedGitRepository.name,
|
||||||
|
repositoryUuid: this.selectedRepository.uuid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
this.toastService.success('Rama creada correctamente');
|
||||||
|
this.destinationBranch = result.branchName || result;
|
||||||
|
this.loadBranches();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMonolithicActionSelected(event: any) {
|
onMonolithicActionSelected(event: any) {
|
||||||
console.log('onMonolithicActionSelected llamado con:', event);
|
|
||||||
this.monolithicAction = event.value;
|
this.monolithicAction = event.value;
|
||||||
this.selectedImage = null;
|
this.selectedImage = null;
|
||||||
this.name = '';
|
this.name = '';
|
||||||
|
|
||||||
// Si se selecciona 'update', cargar las imágenes existentes
|
|
||||||
if (event.value === 'update') {
|
if (event.value === 'update') {
|
||||||
this.loadImages();
|
this.loadImages();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Antes del setTimeout (monolithic)');
|
|
||||||
// Hacer scroll hacia la sección de partición después de un delay más largo
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log('Dentro del setTimeout (monolithic), llamando a scrollToPartitionSection');
|
|
||||||
this.scrollToPartitionSection();
|
this.scrollToPartitionSection();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
@ -217,8 +310,7 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
if (!this.selectedExistingImage) return;
|
if (!this.selectedExistingImage) return;
|
||||||
|
|
||||||
this.loadingExistingImages = true;
|
this.loadingExistingImages = true;
|
||||||
// Aquí deberías hacer el GET al endpoint externo
|
|
||||||
// Por ahora uso un endpoint de ejemplo, ajusta según tu API
|
|
||||||
const url = `${this.baseUrl}/images?gitImageRepository.id=${this.selectedExistingImage.id}&page=1&itemsPerPage=100`;
|
const url = `${this.baseUrl}/images?gitImageRepository.id=${this.selectedExistingImage.id}&page=1&itemsPerPage=100`;
|
||||||
|
|
||||||
this.http.get(url).subscribe(
|
this.http.get(url).subscribe(
|
||||||
|
@ -238,14 +330,15 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
this.selectedGitRepository = null;
|
this.selectedGitRepository = null;
|
||||||
this.selectedExistingImage = null;
|
this.selectedExistingImage = null;
|
||||||
this.gitImageName = '';
|
this.gitImageName = '';
|
||||||
this.gitAction = 'create';
|
this.monolithicAction = 'create';
|
||||||
this.existingImages = [];
|
this.existingImages = [];
|
||||||
this.gitRepositories = [];
|
this.gitRepositories = [];
|
||||||
this.gitImageRepositories = [];
|
this.gitImageRepositories = [];
|
||||||
|
this.selectedBranch = '';
|
||||||
|
this.branches = [];
|
||||||
|
|
||||||
this.selectedImage = null;
|
this.selectedImage = null;
|
||||||
this.name = '';
|
this.name = '';
|
||||||
this.monolithicAction = 'create';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetCanonicalName() {
|
resetCanonicalName() {
|
||||||
|
@ -258,16 +351,6 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.imageType === 'git') {
|
|
||||||
if (!this.selectedGitRepository) {
|
|
||||||
this.toastService.error('Debes seleccionar un repositorio Git');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.gitAction === 'update' && !this.selectedExistingImage) {
|
|
||||||
this.toastService.error('Debes seleccionar un repositorio de imágenes Git');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.imageType === 'monolithic') {
|
if (this.imageType === 'monolithic') {
|
||||||
if (this.monolithicAction === 'create' && !this.name) {
|
if (this.monolithicAction === 'create' && !this.name) {
|
||||||
|
@ -295,24 +378,46 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
let payload: any = {
|
let endpoint: string;
|
||||||
client: `/clients/${this.clientId}`,
|
let payload: any;
|
||||||
partition: this.selectedPartition['@id'],
|
|
||||||
source: 'assistant',
|
|
||||||
type: this.imageType,
|
|
||||||
queue: result
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.imageType === 'git') {
|
if (this.imageType === 'git') {
|
||||||
payload.gitRepository = this.selectedGitRepository.name
|
const gitRepoName = this.hasValidGitData ? this.gitData.repo : this.selectedGitRepository?.name;
|
||||||
payload.name = this.selectedGitRepository.name;
|
const originBranch = this.hasValidGitData ? this.gitData.branch : this.selectedBranch;
|
||||||
|
|
||||||
if (this.gitAction === 'create') {
|
if (this.branches.length === 0) {
|
||||||
payload.action = 'create';
|
endpoint = `${this.baseUrl}/images`;
|
||||||
|
payload = {
|
||||||
|
client: `/clients/${this.clientId}`,
|
||||||
|
partition: this.selectedPartition['@id'],
|
||||||
|
type: this.imageType,
|
||||||
|
gitRepository: gitRepoName,
|
||||||
|
name: gitRepoName,
|
||||||
|
originalBranch: originBranch,
|
||||||
|
destinationBranch: this.destinationBranch,
|
||||||
|
action: 'create',
|
||||||
|
queue: result
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
payload.action = 'update';
|
endpoint = `${this.baseUrl}/git-repositories/update-image`;
|
||||||
|
payload = {
|
||||||
|
client: `/clients/${this.clientId}`,
|
||||||
|
partition: this.selectedPartition['@id'],
|
||||||
|
gitRepository: gitRepoName,
|
||||||
|
originalBranch: originBranch,
|
||||||
|
destinationBranch: this.destinationBranch,
|
||||||
|
queue: result
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
endpoint = `${this.baseUrl}/images`;
|
||||||
|
payload = {
|
||||||
|
client: `/clients/${this.clientId}`,
|
||||||
|
partition: this.selectedPartition['@id'],
|
||||||
|
type: this.imageType,
|
||||||
|
queue: result
|
||||||
|
};
|
||||||
|
|
||||||
if (this.monolithicAction === 'create') {
|
if (this.monolithicAction === 'create') {
|
||||||
payload.name = this.name;
|
payload.name = this.name;
|
||||||
payload.action = 'create';
|
payload.action = 'create';
|
||||||
|
@ -322,13 +427,13 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.http.post(`${this.baseUrl}/images`, payload)
|
this.http.post(endpoint, payload)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
let actionText = 'creación';
|
let actionText = 'creación';
|
||||||
if (this.imageType === 'git' && this.gitAction === 'update') {
|
if (this.imageType === 'monolithic' && this.monolithicAction === 'update') {
|
||||||
actionText = 'actualización';
|
actionText = 'actualización';
|
||||||
} else if (this.imageType === 'monolithic' && this.monolithicAction === 'update') {
|
} else if (this.imageType === 'git' && this.branches.length > 0) {
|
||||||
actionText = 'actualización';
|
actionText = 'actualización';
|
||||||
}
|
}
|
||||||
this.toastService.success(`Petición de ${actionText} de imagen enviada`);
|
this.toastService.success(`Petición de ${actionText} de imagen enviada`);
|
||||||
|
@ -359,14 +464,28 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
dialogRef.afterClosed().subscribe(result => {
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
this.creatingRepository = false;
|
this.creatingRepository = false;
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|
||||||
|
|
||||||
|
this.newlyCreatedRepository = result;
|
||||||
|
|
||||||
this.loadGitRepositories();
|
this.loadGitRepositories();
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const newRepository = this.gitRepositories.find(repo => repo['@id'] === result['@id']);
|
if (this.newlyCreatedRepository && !this.selectedGitRepository) {
|
||||||
if (newRepository) {
|
const newRepo = this.gitRepositories.find(repo =>
|
||||||
this.selectedGitRepository = newRepository;
|
repo.name === this.newlyCreatedRepository.name
|
||||||
this.onGitRepositorySelected(newRepository);
|
);
|
||||||
|
|
||||||
|
if (newRepo) {
|
||||||
|
this.selectedGitRepository = newRepo;
|
||||||
|
this.onGitRepositorySelected(newRepo);
|
||||||
|
this.toastService.success(`Repositorio "${newRepo.name}" preseleccionado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.newlyCreatedRepository = null;
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 1000);
|
||||||
|
} else {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -383,8 +502,13 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
this.selectedImage = null;
|
this.selectedImage = null;
|
||||||
this.name = '';
|
this.name = '';
|
||||||
this.monolithicAction = 'create';
|
this.monolithicAction = 'create';
|
||||||
|
|
||||||
|
if (this.selectedPartition) {
|
||||||
|
this.loadGitData(this.selectedPartition);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.resetGitSelections();
|
this.resetGitSelections();
|
||||||
|
this.gitData = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,5 +538,82 @@ export class CreateClientImageComponent implements OnInit{
|
||||||
|
|
||||||
set selectedPartition(value: any) {
|
set selectedPartition(value: any) {
|
||||||
this._selectedPartition = value;
|
this._selectedPartition = value;
|
||||||
|
|
||||||
|
if (value && this.imageType === 'git') {
|
||||||
|
this.loadGitData(value);
|
||||||
|
} else {
|
||||||
|
this.gitData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadGitData(partition: any): void {
|
||||||
|
this.loadingGitData = true;
|
||||||
|
this.gitData = null;
|
||||||
|
this.repositoryNotFound = false;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
partition: partition['@id'],
|
||||||
|
client: `/clients/${this.clientId}`
|
||||||
|
};
|
||||||
|
|
||||||
|
this.http.post(`${this.baseUrl}/git-repositories/get-git-data`, payload).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
this.gitData = response;
|
||||||
|
if (this.gitData && this.gitData.branch) {
|
||||||
|
this.destinationBranch = this.gitData.branch;
|
||||||
|
}
|
||||||
|
this.loadingGitData = false;
|
||||||
|
|
||||||
|
this.checkRepositoryExists();
|
||||||
|
|
||||||
|
if (this.hasValidGitData) {
|
||||||
|
this.loadBranchesForGitData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error al cargar datos de Git:', error);
|
||||||
|
this.toastService.error('Error al cargar información de Git');
|
||||||
|
this.loadingGitData = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkRepositoryExists(): void {
|
||||||
|
if (this.gitData && this.gitData.repo && this.gitRepositories.length > 0) {
|
||||||
|
const repoExists = this.gitRepositories.some((repo: any) =>
|
||||||
|
repo.name === this.gitData.repo
|
||||||
|
);
|
||||||
|
this.repositoryNotFound = !repoExists;
|
||||||
|
} else {
|
||||||
|
this.repositoryNotFound = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBranchesForGitData(): void {
|
||||||
|
if (!this.gitData || !this.gitData.repo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingBranches = true;
|
||||||
|
const url = `${this.baseUrl}/image-repositories/server/git/${this.selectedRepository.uuid}/branches`;
|
||||||
|
this.http.post<any>(url, { repositoryName: this.gitData.repo }).subscribe(
|
||||||
|
data => {
|
||||||
|
this.branches = data.branches || [];
|
||||||
|
this.loadingBranches = false;
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.error('Error fetching branches for Git data', error);
|
||||||
|
this.loadingBranches = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDestinationBranchEdit(): void {
|
||||||
|
this.isDestinationBranchEditable = !this.isDestinationBranchEditable;
|
||||||
|
|
||||||
|
if (!this.isDestinationBranchEditable) {
|
||||||
|
// Opcional: Aquí se pueden agregar validaciones adicionales
|
||||||
|
console.log('Rama destino guardada:', this.destinationBranch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -519,4 +519,73 @@ table {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.commits-table-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-chip.mat-standard-chip.mat-chip-selected.mat-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||||
|
color: white !important;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
margin: 2px !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3) !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-chip.mat-standard-chip.mat-chip-selected.mat-primary:hover {
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-chip.mat-standard-chip.mat-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||||
|
color: white !important;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
margin: 2px !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3) !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-chip.mat-standard-chip.mat-primary:hover {
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .custom-tag-chip.mat-chip.mat-standard-chip.mat-chip-selected.mat-primary {
|
||||||
|
background: rgba(102, 126, 234, 0.1) !important;
|
||||||
|
color: #667eea !important;
|
||||||
|
border: 1px solid rgba(102, 126, 234, 0.3) !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
font-size: 10px !important;
|
||||||
|
padding: 2px 6px !important;
|
||||||
|
margin: 1px !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .custom-tag-chip.mat-chip.mat-standard-chip.mat-chip-selected.mat-primary:hover {
|
||||||
|
background: rgba(102, 126, 234, 0.15) !important;
|
||||||
|
border-color: rgba(102, 126, 234, 0.5) !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tags-text {
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px dashed #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -129,11 +129,18 @@
|
||||||
</mat-select>
|
</mat-select>
|
||||||
<mat-icon matSuffix *ngIf="loadingBranches">hourglass_empty</mat-icon>
|
<mat-icon matSuffix *ngIf="loadingBranches">hourglass_empty</mat-icon>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="fill" style="width: 200px;">
|
||||||
|
<mat-label>Tag</mat-label>
|
||||||
|
<mat-select [(ngModel)]="showOnlyTagged" (selectionChange)="onTagFilterChange()" [disabled]="!commits.length">
|
||||||
|
<mat-option [value]="false">No</mat-option>
|
||||||
|
<mat-option [value]="true">Sí</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabla de commits (solo si imageType === 'git') -->
|
<div *ngIf="imageType === 'git' && filteredCommits.length > 0" class="commits-table-container">
|
||||||
<div *ngIf="imageType === 'git' && commits.length > 0" class="commits-table-container">
|
<table mat-table [dataSource]="filteredCommits" class="mat-elevation-z8">
|
||||||
<table mat-table [dataSource]="commits" class="mat-elevation-z8">
|
|
||||||
<ng-container matColumnDef="select">
|
<ng-container matColumnDef="select">
|
||||||
<th mat-header-cell *matHeaderCellDef>Seleccionar</th>
|
<th mat-header-cell *matHeaderCellDef>Seleccionar</th>
|
||||||
<td mat-cell *matCellDef="let commit">
|
<td mat-cell *matCellDef="let commit">
|
||||||
|
@ -162,8 +169,11 @@
|
||||||
<th mat-header-cell *matHeaderCellDef>Tags</th>
|
<th mat-header-cell *matHeaderCellDef>Tags</th>
|
||||||
<td mat-cell *matCellDef="let commit">
|
<td mat-cell *matCellDef="let commit">
|
||||||
<mat-chip-list>
|
<mat-chip-list>
|
||||||
<mat-chip *ngFor="let tag of commit.tags" color="primary" selected>{{ tag }}</mat-chip>
|
<mat-chip *ngFor="let tag of commit.tags" color="primary" selected class="custom-tag-chip"
|
||||||
<span *ngIf="!commit.tags || commit.tags.length === 0" style="color: #999; font-style: italic;">Sin tags</span>
|
style="background: rgba(102, 126, 234, 0.1) !important; color: #667eea !important; border: 1px solid rgba(102, 126, 234, 0.3) !important; border-radius: 8px !important; font-weight: 500 !important; font-size: 10px !important; padding: 2px 6px !important; margin: 1px !important; box-shadow: none !important;">
|
||||||
|
{{ tag }}
|
||||||
|
</mat-chip>
|
||||||
|
<span *ngIf="!commit.tags || commit.tags.length === 0" class="no-tags-text">Sin tags</span>
|
||||||
</mat-chip-list>
|
</mat-chip-list>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -172,7 +182,6 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selector de método y de imagen solo si imageType === 'monolithic' -->
|
|
||||||
<div class="monolithic-row" *ngIf="imageType === 'monolithic'">
|
<div class="monolithic-row" *ngIf="imageType === 'monolithic'">
|
||||||
<mat-form-field appearance="fill" class="half-width">
|
<mat-form-field appearance="fill" class="half-width">
|
||||||
<mat-label>Seleccione método de deploy</mat-label>
|
<mat-label>Seleccione método de deploy</mat-label>
|
||||||
|
|
|
@ -109,6 +109,9 @@ export class DeployImageComponent implements OnInit{
|
||||||
selectedCommit: any = null;
|
selectedCommit: any = null;
|
||||||
private initialGitLoad = true;
|
private initialGitLoad = true;
|
||||||
|
|
||||||
|
showOnlyTagged: boolean = false;
|
||||||
|
filteredCommits: any[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private toastService: ToastrService,
|
private toastService: ToastrService,
|
||||||
|
@ -387,7 +390,7 @@ export class DeployImageComponent implements OnInit{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.imageType === 'git') {
|
if (this.imageType === 'git') {
|
||||||
if (!this.selectedCommit) {
|
if (!this.selectedCommit || this.filteredCommits.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -418,17 +421,28 @@ export class DeployImageComponent implements OnInit{
|
||||||
|
|
||||||
|
|
||||||
openScheduleModal(): void {
|
openScheduleModal(): void {
|
||||||
|
let scope = this.runScriptContext.type;
|
||||||
|
let selectedClients = null;
|
||||||
|
|
||||||
|
|
||||||
|
if ((!this.runScriptContext || this.runScriptContext.type === 'client' || this.selectedClients.length === 1) && this.selectedClients && this.selectedClients.length > 0) {
|
||||||
|
scope = 'clients';
|
||||||
|
selectedClients = this.selectedClients;
|
||||||
|
}
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(CreateTaskComponent, {
|
const dialogRef = this.dialog.open(CreateTaskComponent, {
|
||||||
width: '800px',
|
width: '800px',
|
||||||
data: {
|
data: {
|
||||||
scope: this.runScriptContext.type,
|
scope: scope,
|
||||||
organizationalUnit: this.runScriptContext['@id'],
|
selectedClients: selectedClients,
|
||||||
source: 'assistant'
|
organizationalUnit: this.runScriptContext?.['@id'],
|
||||||
|
source: 'assistant',
|
||||||
|
runScriptContext: this.runScriptContext
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((result: { [x: string]: any; }) => {
|
dialogRef.afterClosed().subscribe((result: { [x: string]: any; }) => {
|
||||||
if (result) {
|
if (result !== undefined) {
|
||||||
const payload = {
|
const payload = {
|
||||||
method: this.selectedMethod,
|
method: this.selectedMethod,
|
||||||
diskNumber: this.selectedPartition.diskNumber,
|
diskNumber: this.selectedPartition.diskNumber,
|
||||||
|
@ -519,7 +533,9 @@ export class DeployImageComponent implements OnInit{
|
||||||
this.selectedBranch = '';
|
this.selectedBranch = '';
|
||||||
this.branches = [];
|
this.branches = [];
|
||||||
this.commits = [];
|
this.commits = [];
|
||||||
|
this.filteredCommits = [];
|
||||||
this.selectedCommit = null;
|
this.selectedCommit = null;
|
||||||
|
this.showOnlyTagged = false;
|
||||||
this.loadGitBranches();
|
this.loadGitBranches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -545,6 +561,8 @@ export class DeployImageComponent implements OnInit{
|
||||||
onGitBranchChange() {
|
onGitBranchChange() {
|
||||||
this.selectedCommit = null;
|
this.selectedCommit = null;
|
||||||
this.commits = [];
|
this.commits = [];
|
||||||
|
this.filteredCommits = [];
|
||||||
|
this.showOnlyTagged = false;
|
||||||
this.loadGitCommits();
|
this.loadGitCommits();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -558,6 +576,12 @@ export class DeployImageComponent implements OnInit{
|
||||||
data => {
|
data => {
|
||||||
this.commits = data.commits || [];
|
this.commits = data.commits || [];
|
||||||
this.loadingCommits = false;
|
this.loadingCommits = false;
|
||||||
|
this.filterCommits();
|
||||||
|
if (this.filteredCommits.length > 0) {
|
||||||
|
this.selectedCommit = this.filteredCommits.reduce((a, b) =>
|
||||||
|
new Date(a.committed_date * 1000) > new Date(b.committed_date * 1000) ? a : b
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
this.toastService.error('Error al cargar los commits');
|
this.toastService.error('Error al cargar los commits');
|
||||||
|
@ -565,4 +589,23 @@ export class DeployImageComponent implements OnInit{
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filterCommits() {
|
||||||
|
if (this.showOnlyTagged) {
|
||||||
|
this.filteredCommits = this.commits.filter(commit =>
|
||||||
|
commit.tags && commit.tags.length > 0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.filteredCommits = this.commits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTagFilterChange() {
|
||||||
|
this.filterCommits();
|
||||||
|
if (this.filteredCommits.length > 0 && !this.filteredCommits.includes(this.selectedCommit)) {
|
||||||
|
this.selectedCommit = this.filteredCommits.reduce((a, b) =>
|
||||||
|
new Date(a.committed_date * 1000) > new Date(b.committed_date * 1000) ? a : b
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,6 +175,22 @@
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-disk-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #2e7d32;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
/* Opciones del select */
|
/* Opciones del select */
|
||||||
::ng-deep .disk-option {
|
::ng-deep .disk-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -232,7 +248,9 @@
|
||||||
background: #e8f5e8;
|
background: #e8f5e8;
|
||||||
border: 1px solid #c8e6c9;
|
border: 1px solid #c8e6c9;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin-top: 16px;
|
margin-top: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-icon {
|
.info-icon {
|
||||||
|
@ -709,8 +727,454 @@
|
||||||
margin: 5px 0 !important;
|
margin: 5px 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== ESTADOS DE ADVERTENCIA ===== */
|
/* ===== LAYOUT PRINCIPAL ===== */
|
||||||
/* Advertencia (90% a 99% usado) */
|
.partition-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== BARRA DE PROGRESO DE PARTICIONES ===== */
|
||||||
|
.partition-progress-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disk-info-summary {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disk-info-summary span {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-size {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.used-size {
|
||||||
|
background: #fff3e0;
|
||||||
|
color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.free-size {
|
||||||
|
background: #e8f5e8;
|
||||||
|
color: #388e3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-progress-bar {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-segments {
|
||||||
|
display: flex;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-segment {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-segment:hover {
|
||||||
|
filter: brightness(1.2);
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-segment:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-segment.removed {
|
||||||
|
opacity: 0.3;
|
||||||
|
background: #ccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-segment.free-space {
|
||||||
|
background: linear-gradient(45deg, #e8f5e8, #c8e6c9);
|
||||||
|
border-left: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment-label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-number {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-percentage {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.free-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.free-percentage {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== LEYENDA DE PARTICIONES ===== */
|
||||||
|
.partition-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item:hover {
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item.removed {
|
||||||
|
opacity: 0.5;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item:hover .legend-color {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-color.free-color {
|
||||||
|
background: linear-gradient(45deg, #e8f5e8, #c8e6c9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-text {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-size {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== TABLA MAT-TABLE ===== */
|
||||||
|
.table-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
padding: 20px 24px 16px 24px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-mat-table {
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-mat-table .mat-header-cell {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 16px 12px;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-mat-table .mat-cell {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #f1f3f4;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-mat-table .mat-row:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Campos compactos para la tabla */
|
||||||
|
.compact-form-field {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-form-field .mat-form-field-wrapper {
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-form-field .mat-form-field-infix {
|
||||||
|
padding: 2px 0;
|
||||||
|
min-height: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-form-field .mat-form-field-outline {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-form-field .mat-form-field-outline-start,
|
||||||
|
.compact-form-field .mat-form-field-outline-end {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-form-field .mat-form-field-outline-gap {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos para inputs y selects compactos */
|
||||||
|
.compact-form-field input,
|
||||||
|
.compact-form-field .mat-select {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-form-field .mat-select-value {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reducir el padding de las celdas de la tabla */
|
||||||
|
.partition-mat-table .mat-cell {
|
||||||
|
padding: 6px 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-mat-table .mat-header-cell {
|
||||||
|
padding: 10px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hacer los inputs más pequeños */
|
||||||
|
.compact-form-field .mat-form-field-infix {
|
||||||
|
padding: 2px 0;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajustar el tamaño de los selects */
|
||||||
|
.compact-form-field .mat-select-trigger {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajustar el tamaño de los inputs numéricos */
|
||||||
|
.compact-form-field input[type="number"] {
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reducir el espacio del wrapper del form field */
|
||||||
|
.compact-form-field .mat-form-field-wrapper {
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajustar el espacio del outline */
|
||||||
|
.compact-form-field .mat-form-field-outline {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-form-field .mat-form-field-outline-start,
|
||||||
|
.compact-form-field .mat-form-field-outline-end,
|
||||||
|
.compact-form-field .mat-form-field-outline-gap {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox en la tabla */
|
||||||
|
.partition-mat-table .mat-checkbox {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botón de eliminar */
|
||||||
|
.partition-mat-table .mat-icon-button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-mat-table .mat-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== RESPONSIVE ===== */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.partition-layout {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disk-info-summary {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-legend {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-mat-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-form-field {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.progress-segments {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment-label {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-number {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-percentage {
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-mat-table .mat-header-cell,
|
||||||
|
.partition-mat-table .mat-cell {
|
||||||
|
padding: 8px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-segment {
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
color: #ff9800 !important;
|
color: #ff9800 !important;
|
||||||
}
|
}
|
||||||
|
@ -723,7 +1187,6 @@
|
||||||
color: #ff9800 !important;
|
color: #ff9800 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Peligro (100% o más usado) */
|
|
||||||
.danger {
|
.danger {
|
||||||
color: #f44336 !important;
|
color: #f44336 !important;
|
||||||
font-weight: bold !important;
|
font-weight: bold !important;
|
||||||
|
@ -751,7 +1214,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== INSTRUCCIONES ===== */
|
|
||||||
.instructions-box {
|
.instructions-box {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
@ -780,7 +1242,6 @@
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== RESPONSIVE ===== */
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.header-container {
|
.header-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -844,6 +1305,73 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.partition-validation-indicator {
|
||||||
|
margin: 16px 0;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-status.loading {
|
||||||
|
color: #1976d2;
|
||||||
|
background: #e3f2fd;
|
||||||
|
border-color: #bbdefb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-status.success {
|
||||||
|
color: #2e7d32;
|
||||||
|
background: #e8f5e8;
|
||||||
|
border-color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-status.error {
|
||||||
|
color: #d32f2f;
|
||||||
|
background: #ffebee;
|
||||||
|
border-color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-icon.loading {
|
||||||
|
color: #1976d2;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-icon.success {
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-icon.error {
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button class="action-button" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="save()">Ejecutar</button>
|
<button class="action-button" [disabled]="!selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0 || partitionValidationStatus === 'error'" (click)="save()">Ejecutar</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button class="action-button" color="accent" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="openScheduleModal()">
|
<button class="action-button" color="accent" [disabled]="!selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="openScheduleModal()">
|
||||||
Opciones de programación
|
Opciones de programación
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
<div class="clients-grid">
|
<div class="clients-grid">
|
||||||
<div *ngFor="let client of clientData" class="client-item">
|
<div *ngFor="let client of clientData" class="client-item">
|
||||||
<div class="client-card"
|
<div class="client-card"
|
||||||
(click)="client.status === 'og-live' && toggleClientSelection(client)"
|
(click)="toggleClientSelection(client)"
|
||||||
[ngClass]="{'selected-client': client.selected}"
|
[ngClass]="{'selected-client': client.selected}"
|
||||||
[matTooltip]="getPartitionsTooltip(client)"
|
[matTooltip]="getPartitionsTooltip(client)"
|
||||||
matTooltipPosition="above"
|
matTooltipPosition="above"
|
||||||
|
@ -110,17 +110,13 @@
|
||||||
</div>
|
</div>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
<mat-hint>Selecciona el disco que deseas particionar</mat-hint>
|
<mat-hint *ngIf="!selectedDisk">Selecciona el disco que deseas particionar</mat-hint>
|
||||||
|
<mat-hint *ngIf="selectedDisk" class="selected-disk-hint">
|
||||||
|
<mat-icon class="hint-icon">check_circle</mat-icon>
|
||||||
|
Disco {{ selectedDisk.diskNumber }} seleccionado - {{ (selectedDisk.totalDiskSize / 1024).toFixed(2) }} GB
|
||||||
|
</mat-hint>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<div class="selection-info" *ngIf="selectedDisk">
|
|
||||||
<mat-icon class="info-icon">info</mat-icon>
|
|
||||||
<div class="info-text">
|
|
||||||
<span class="info-title">Disco seleccionado: {{ selectedDisk.diskNumber }}</span>
|
|
||||||
<span class="info-subtitle">Tamaño total: {{ (selectedDisk.totalDiskSize / 1024).toFixed(2) }} GB</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="no-disks-message" *ngIf="!disks || disks.length === 0">
|
<div class="no-disks-message" *ngIf="!disks || disks.length === 0">
|
||||||
<mat-icon class="warning-icon">warning</mat-icon>
|
<mat-icon class="warning-icon">warning</mat-icon>
|
||||||
<div class="message-text">
|
<div class="message-text">
|
||||||
|
@ -207,6 +203,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Indicador de validación de particiones -->
|
||||||
|
<div class="partition-validation-indicator" *ngIf="selectedDisk && selectedDisk.partitions.length > 0">
|
||||||
|
<div class="validation-status" [ngClass]="partitionValidationStatus">
|
||||||
|
<mat-icon *ngIf="partitionValidationStatus === 'loading'" class="validation-icon loading">hourglass_empty</mat-icon>
|
||||||
|
<mat-icon *ngIf="partitionValidationStatus === 'success'" class="validation-icon success">check_circle</mat-icon>
|
||||||
|
<mat-icon *ngIf="partitionValidationStatus === 'error'" class="validation-icon error">error</mat-icon>
|
||||||
|
<span class="validation-message" *ngIf="partitionValidationMessage">{{ partitionValidationMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="partition-table" id="partition-table">
|
<table class="partition-table" id="partition-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -256,18 +262,21 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart-container" *ngIf="selectedDisk">
|
<div class="chart-container" *ngIf="selectedDisk" #chartContainer>
|
||||||
<div class="chart-header">
|
<div class="chart-header">
|
||||||
<h3>Distribución de Particiones</h3>
|
<h3>Distribución de Particiones</h3>
|
||||||
</div>
|
</div>
|
||||||
<ngx-charts-pie-chart
|
<div class="chart-wrapper">
|
||||||
[results]="selectedDisk.chartData"
|
<ngx-charts-pie-chart
|
||||||
[doughnut]="true"
|
[results]="selectedDisk.chartData"
|
||||||
[gradient]="true"
|
[doughnut]="true"
|
||||||
[labels]="true"
|
[gradient]="true"
|
||||||
[tooltipDisabled]="false"
|
[labels]="true"
|
||||||
[animations]="true">
|
[tooltipDisabled]="false"
|
||||||
</ngx-charts-pie-chart>
|
[animations]="true"
|
||||||
|
[view]="view">
|
||||||
|
</ngx-charts-pie-chart>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/core';
|
import {Component, EventEmitter, Inject, Input, OnInit, Output, AfterViewInit, OnDestroy, ViewChild, ElementRef, HostListener} from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import {ActivatedRoute, Router} from "@angular/router";
|
import {ActivatedRoute, Router} from "@angular/router";
|
||||||
|
@ -8,6 +8,9 @@ import { ConfigService } from '@services/config.service';
|
||||||
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
|
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
|
||||||
import {MatDialog} from "@angular/material/dialog";
|
import {MatDialog} from "@angular/material/dialog";
|
||||||
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
|
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil, debounceTime } from 'rxjs/operators';
|
||||||
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
|
|
||||||
interface Partition {
|
interface Partition {
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
|
@ -28,7 +31,7 @@ interface Partition {
|
||||||
templateUrl: './partition-assistant.component.html',
|
templateUrl: './partition-assistant.component.html',
|
||||||
styleUrls: ['./partition-assistant.component.css']
|
styleUrls: ['./partition-assistant.component.css']
|
||||||
})
|
})
|
||||||
export class PartitionAssistantComponent implements OnInit{
|
export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDestroy{
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
private apiUrl: string;
|
private apiUrl: string;
|
||||||
@Output() dataChange = new EventEmitter<any>();
|
@Output() dataChange = new EventEmitter<any>();
|
||||||
|
@ -47,6 +50,10 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
runScriptContext: any = null;
|
runScriptContext: any = null;
|
||||||
showInstructions = false;
|
showInstructions = false;
|
||||||
|
|
||||||
|
@ViewChild('chartContainer', { static: false }) chartContainer!: ElementRef;
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private resizeSubject = new Subject<void>();
|
||||||
|
|
||||||
view: [number, number] = [300, 200];
|
view: [number, number] = [300, 200];
|
||||||
showLegend = true;
|
showLegend = true;
|
||||||
showLabels = true;
|
showLabels = true;
|
||||||
|
@ -55,6 +62,23 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
selectedModelClient: any = null;
|
selectedModelClient: any = null;
|
||||||
partitionCode: string = '';
|
partitionCode: string = '';
|
||||||
generatedInstructions: string = '';
|
generatedInstructions: string = '';
|
||||||
|
|
||||||
|
// Propiedades para validación de particiones
|
||||||
|
partitionValidationStatus: 'idle' | 'loading' | 'success' | 'error' = 'idle';
|
||||||
|
partitionValidationMessage: string = '';
|
||||||
|
private validationDebounceTime = 500; // ms
|
||||||
|
private validationSubject = new Subject<void>();
|
||||||
|
|
||||||
|
// Columnas para mat-table
|
||||||
|
displayedColumns: string[] = ['partitionNumber', 'partitionCode', 'filesystem', 'size', 'percentage', 'format', 'actions'];
|
||||||
|
|
||||||
|
// Paleta de colores para las particiones
|
||||||
|
private partitionColors = [
|
||||||
|
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
||||||
|
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
|
||||||
|
'#F8C471', '#82E0AA', '#F1948A', '#85C1E9', '#D7BDE2',
|
||||||
|
'#A8E6CF', '#FFD3B6', '#FFAAA5', '#DCEDC8', '#FFE0B2'
|
||||||
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
|
@ -77,10 +101,22 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
||||||
this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; });
|
this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; });
|
||||||
|
|
||||||
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
|
this.selectedClients = this.clientData.filter(
|
||||||
|
(client: { selected: boolean; status: string }) => client.selected
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.selectedClients.length === 0 && this.clientData.length > 0) {
|
||||||
|
this.selectedClients = [this.clientData[0]];
|
||||||
|
this.clientData[0].selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.selectedModelClient = this.clientData.find((client: { selected: boolean; status: string}) => client.selected) || null;
|
this.selectedModelClient = this.clientData.find((client: { selected: boolean; status: string}) => client.selected) || null;
|
||||||
|
|
||||||
|
if (!this.selectedModelClient && this.clientData.length > 0) {
|
||||||
|
this.selectedModelClient = this.clientData[0];
|
||||||
|
this.clientData[0].selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.selectedModelClient) {
|
if (this.selectedModelClient) {
|
||||||
this.loadPartitions(this.selectedModelClient);
|
this.loadPartitions(this.selectedModelClient);
|
||||||
}
|
}
|
||||||
|
@ -90,6 +126,55 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
this.route.queryParams.subscribe(params => {
|
this.route.queryParams.subscribe(params => {
|
||||||
this.runScriptContext = params['runScriptContext'] ? JSON.parse(params['runScriptContext']) : null;
|
this.runScriptContext = params['runScriptContext'] ? JSON.parse(params['runScriptContext']) : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.resizeSubject.pipe(
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
debounceTime(100)
|
||||||
|
).subscribe(() => {
|
||||||
|
this.resizeChart();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.validationSubject.pipe(
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
debounceTime(this.validationDebounceTime)
|
||||||
|
).subscribe(() => {
|
||||||
|
this.validatePartitionSizes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.resizeChart();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize')
|
||||||
|
onResize(): void {
|
||||||
|
this.resizeSubject.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
private resizeChart(): void {
|
||||||
|
if (this.chartContainer && this.chartContainer.nativeElement) {
|
||||||
|
const container = this.chartContainer.nativeElement;
|
||||||
|
const width = container.offsetWidth;
|
||||||
|
|
||||||
|
if (width > 0) {
|
||||||
|
const height = Math.max(250, Math.min(450, width * 0.7));
|
||||||
|
|
||||||
|
if (Math.abs(this.view[0] - width) > 10 || Math.abs(this.view[1] - height) > 10) {
|
||||||
|
this.view = [width, height];
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.view = [...this.view];
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectedDisk():any {
|
get selectedDisk():any {
|
||||||
|
@ -135,7 +220,6 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
initializeDisks() {
|
initializeDisks() {
|
||||||
this.disks = [];
|
this.disks = [];
|
||||||
|
|
||||||
// Verificar que hay datos válidos
|
|
||||||
if (!this.data || !this.data.partitions || !Array.isArray(this.data.partitions)) {
|
if (!this.data || !this.data.partitions || !Array.isArray(this.data.partitions)) {
|
||||||
console.warn('No hay datos de particiones válidos');
|
console.warn('No hay datos de particiones válidos');
|
||||||
return;
|
return;
|
||||||
|
@ -161,10 +245,10 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
size: this.convertBytesToGB(partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 512 : partition.size),
|
size: this.convertBytesToGB(partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 512 : partition.size),
|
||||||
memoryUsage: partition.memoryUsage,
|
memoryUsage: partition.memoryUsage,
|
||||||
partitionCode: partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 'EFI' : partition.partitionCode,
|
partitionCode: partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 'EFI' : partition.partitionCode,
|
||||||
filesystem: partition.filesystem,
|
filesystem: partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 'FAT32' : partition.filesystem,
|
||||||
sizeBytes: partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 512 : partition.size,
|
sizeBytes: partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 512 : partition.size,
|
||||||
format: false,
|
format: false,
|
||||||
color: '#1f1b91',
|
color: this.getColorForPartition(partition.partitionNumber),
|
||||||
percentage: 0,
|
percentage: 0,
|
||||||
removed: false
|
removed: false
|
||||||
});
|
});
|
||||||
|
@ -234,7 +318,14 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedClients() {
|
updateSelectedClients() {
|
||||||
this.selectedClients = this.clientData.filter((client: { selected: any; }) => client.selected);
|
this.selectedClients = this.clientData.filter(
|
||||||
|
(client: { selected: boolean; status: string }) => client.selected
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.selectedClients.length === 0 && this.clientData.length > 0) {
|
||||||
|
this.selectedClients = [this.clientData[0]];
|
||||||
|
this.clientData[0].selected = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPartitionsTooltip(client: any): string {
|
getPartitionsTooltip(client: any): string {
|
||||||
|
@ -267,12 +358,18 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
memoryUsage: 0,
|
memoryUsage: 0,
|
||||||
sizeBytes: 0,
|
sizeBytes: 0,
|
||||||
format: false,
|
format: false,
|
||||||
color: '#' + Math.floor(Math.random() * 16777215).toString(16),
|
color: this.getNextPartitionColor(),
|
||||||
percentage: 0,
|
percentage: 0,
|
||||||
removed: false
|
removed: false
|
||||||
});
|
});
|
||||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||||
this.updateDiskChart(disk);
|
this.updateDiskChart(disk);
|
||||||
|
|
||||||
|
this.validationSubject.next();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.resizeChart();
|
||||||
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
this.toastService.error('No hay suficiente espacio libre en el disco para crear una nueva partición.');
|
this.toastService.error('No hay suficiente espacio libre en el disco para crear una nueva partición.');
|
||||||
}
|
}
|
||||||
|
@ -297,6 +394,8 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
partition.percentage = (size / disk.totalDiskSize) * 100;
|
partition.percentage = (size / disk.totalDiskSize) * 100;
|
||||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||||
this.updateDiskChart(disk);
|
this.updateDiskChart(disk);
|
||||||
|
|
||||||
|
this.validationSubject.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -311,6 +410,10 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
|
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
|
if (this.selectedClients.length === 0 && this.clientData.length > 0) {
|
||||||
|
this.updateSelectedClients();
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.selectedDisk) {
|
if (!this.selectedDisk) {
|
||||||
this.toastService.error('No se ha seleccionado un disco.');
|
this.toastService.error('No se ha seleccionado un disco.');
|
||||||
return;
|
return;
|
||||||
|
@ -389,6 +492,12 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
|
|
||||||
this.updateDiskChart(disk);
|
this.updateDiskChart(disk);
|
||||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||||
|
|
||||||
|
this.validationSubject.next();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.resizeChart();
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -401,9 +510,55 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
}
|
}
|
||||||
this.updateDiskChart(disk);
|
this.updateDiskChart(disk);
|
||||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||||
|
|
||||||
|
this.validationSubject.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validatePartitionSizes(): void {
|
||||||
|
if (!this.selectedModelClient || !this.selectedDisk) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.partitionValidationStatus = 'loading';
|
||||||
|
this.partitionValidationMessage = '';
|
||||||
|
|
||||||
|
const partitions = this.selectedDisk.partitions
|
||||||
|
.filter((partition: Partition) => !partition.removed)
|
||||||
|
.map((partition: Partition) => ({
|
||||||
|
diskNumber: this.selectedDisk.diskNumber,
|
||||||
|
partitionNumber: partition.partitionNumber,
|
||||||
|
size: partition.size,
|
||||||
|
partitionCode: partition.partitionCode,
|
||||||
|
filesystem: partition.filesystem
|
||||||
|
}));
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
partitions: partitions
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `${this.baseUrl}${this.selectedModelClient.uuid}/check-partition-sizes`;
|
||||||
|
|
||||||
|
this.http.post(url, payload).subscribe(
|
||||||
|
(response: any) => {
|
||||||
|
if (response.res === 1) {
|
||||||
|
this.partitionValidationStatus = 'success';
|
||||||
|
this.partitionValidationMessage = 'Las particiones cumplen con los requisitos del disco.';
|
||||||
|
} else if (response.res === 2) {
|
||||||
|
this.partitionValidationStatus = 'error';
|
||||||
|
this.partitionValidationMessage = response.der || 'Las particiones no cumplen con los requisitos del disco.';
|
||||||
|
} else {
|
||||||
|
this.partitionValidationStatus = 'error';
|
||||||
|
this.partitionValidationMessage = 'Respuesta inesperada del servidor.';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.partitionValidationStatus = 'error';
|
||||||
|
this.partitionValidationMessage = error.error?.message || 'Error al validar las particiones.';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
calculateUsedSpace(partitions: Partition[]): number {
|
calculateUsedSpace(partitions: Partition[]): number {
|
||||||
return partitions
|
return partitions
|
||||||
.filter(partition => !partition.removed)
|
.filter(partition => !partition.removed)
|
||||||
|
@ -429,19 +584,36 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDiskChart(disk: any) {
|
updateDiskChart(disk: any) {
|
||||||
console.log('disk', disk);
|
|
||||||
disk.chartData = this.generateChartData(disk.partitions);
|
disk.chartData = this.generateChartData(disk.partitions);
|
||||||
disk.used = this.calculateUsedSpace(disk.partitions);
|
disk.used = this.calculateUsedSpace(disk.partitions);
|
||||||
disk.percentage = (disk.used / disk.totalDiskSize) * 100;
|
disk.percentage = (disk.used / disk.totalDiskSize) * 100;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.resizeChart();
|
||||||
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
openScheduleModal(): void {
|
openScheduleModal(): void {
|
||||||
|
let scope = this.runScriptContext?.type || 'clients';
|
||||||
|
let selectedClients = null;
|
||||||
|
|
||||||
|
if (this.selectedClients.length === 0 && this.clientData.length > 0) {
|
||||||
|
this.updateSelectedClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedClients && this.selectedClients.length > 0) {
|
||||||
|
scope = 'clients';
|
||||||
|
selectedClients = this.selectedClients;
|
||||||
|
}
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(CreateTaskComponent, {
|
const dialogRef = this.dialog.open(CreateTaskComponent, {
|
||||||
width: '800px',
|
width: '800px',
|
||||||
data: {
|
data: {
|
||||||
scope: this.runScriptContext.type,
|
scope: scope,
|
||||||
organizationalUnit: this.runScriptContext['@id'],
|
selectedClients: selectedClients,
|
||||||
source: 'assistant'
|
organizationalUnit: this.runScriptContext?.['@id'],
|
||||||
|
source: 'assistant',
|
||||||
|
runScriptContext: this.runScriptContext
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -502,11 +674,16 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
onDiskSelectionChange() {
|
onDiskSelectionChange() {
|
||||||
if (this.selectedDiskNumber) {
|
if (this.selectedDiskNumber) {
|
||||||
this.scrollToPartitionTable();
|
this.scrollToPartitionTable();
|
||||||
|
|
||||||
|
this.validationSubject.next();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.resizeChart();
|
||||||
|
}, 150);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToPartitionTable() {
|
scrollToPartitionTable() {
|
||||||
// Pequeño delay para asegurar que el contenido se haya renderizado
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const diskInfo = document.getElementById('disk-info');
|
const diskInfo = document.getElementById('disk-info');
|
||||||
|
|
||||||
|
@ -536,4 +713,40 @@ export class PartitionAssistantComponent implements OnInit{
|
||||||
console.error('No se encontró el botón execute-button');
|
console.error('No se encontró el botón execute-button');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPartitionsDataSource(): MatTableDataSource<Partition> {
|
||||||
|
return new MatTableDataSource<Partition>(this.selectedDisk?.partitions || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPartitionIndex(partition: Partition): number {
|
||||||
|
return this.selectedDisk?.partitions.findIndex((p: Partition) => p.uuid === partition.uuid) || -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFreeSpacePercentage(): number {
|
||||||
|
if (!this.selectedDisk) return 0;
|
||||||
|
return this.selectedDisk.totalDiskSize > 0 ?
|
||||||
|
((this.selectedDisk.totalDiskSize - this.selectedDisk.used) / this.selectedDisk.totalDiskSize) * 100 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private getNextPartitionColor(): string {
|
||||||
|
if (!this.selectedDisk) return this.partitionColors[0];
|
||||||
|
|
||||||
|
const usedColors = this.selectedDisk.partitions
|
||||||
|
.filter((p: Partition) => !p.removed)
|
||||||
|
.map((p: Partition) => p.color);
|
||||||
|
|
||||||
|
for (const color of this.partitionColors) {
|
||||||
|
if (!usedColors.includes(color)) {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.partitionColors[Math.floor(Math.random() * this.partitionColors.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private getColorForPartition(partitionNumber: number): string {
|
||||||
|
return this.partitionColors[(partitionNumber - 1) % this.partitionColors.length];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript) || loading" (click)="save()">Ejecutar</button>
|
<button class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript) || (commandType === 'new' && !newScript.trim()) || loading" (click)="save()">Ejecutar</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<button color="accent" class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript) || loading" (click)="openScheduleModal()">
|
<button color="accent" class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript) || (commandType === 'new' && !newScript.trim()) || loading" (click)="openScheduleModal()">
|
||||||
Opciones de programación
|
Opciones de programación
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -29,7 +29,7 @@ export class RunScriptAssistantComponent implements OnInit{
|
||||||
parameters: any = {};
|
parameters: any = {};
|
||||||
selectedScript: any = null;
|
selectedScript: any = null;
|
||||||
selectedClients: any[] = [];
|
selectedClients: any[] = [];
|
||||||
allSelected: boolean = true;
|
allSelected: boolean = false;
|
||||||
commandType: string = 'existing';
|
commandType: string = 'existing';
|
||||||
newScript: string = '';
|
newScript: string = '';
|
||||||
selection = new SelectionModel(true, []);
|
selection = new SelectionModel(true, []);
|
||||||
|
@ -56,7 +56,11 @@ export class RunScriptAssistantComponent implements OnInit{
|
||||||
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
||||||
this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; });
|
this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; });
|
||||||
|
|
||||||
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
|
this.selectedClients = this.clientData.filter(
|
||||||
|
(client: { selected: boolean; status: string }) => client.selected
|
||||||
|
);
|
||||||
|
|
||||||
|
this.allSelected = this.clientData.length > 0 && this.clientData.every((client: { selected: boolean }) => client.selected);
|
||||||
|
|
||||||
this.loadScripts()
|
this.loadScripts()
|
||||||
}
|
}
|
||||||
|
@ -117,12 +121,15 @@ export class RunScriptAssistantComponent implements OnInit{
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedClients() {
|
updateSelectedClients() {
|
||||||
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
|
this.selectedClients = this.clientData.filter(
|
||||||
|
(client: { selected: boolean; status: string }) => client.selected
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSelectAll() {
|
toggleSelectAll() {
|
||||||
this.allSelected = !this.allSelected;
|
this.allSelected = !this.allSelected;
|
||||||
this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = this.allSelected; });
|
this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = this.allSelected; });
|
||||||
|
this.updateSelectedClients();
|
||||||
}
|
}
|
||||||
|
|
||||||
getPartitionsTooltip(client: any): string {
|
getPartitionsTooltip(client: any): string {
|
||||||
|
@ -198,21 +205,33 @@ export class RunScriptAssistantComponent implements OnInit{
|
||||||
}
|
}
|
||||||
|
|
||||||
openScheduleModal(): void {
|
openScheduleModal(): void {
|
||||||
|
let scope = this.runScriptContext.type;
|
||||||
|
let selectedClients = null;
|
||||||
|
|
||||||
|
|
||||||
|
if ((!this.runScriptContext || this.runScriptContext.type === 'client' || this.selectedClients.length === 1) && this.selectedClients && this.selectedClients.length > 0) {
|
||||||
|
scope = 'clients';
|
||||||
|
selectedClients = this.selectedClients;
|
||||||
|
}
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(CreateTaskComponent, {
|
const dialogRef = this.dialog.open(CreateTaskComponent, {
|
||||||
width: '800px',
|
width: '800px',
|
||||||
data: {
|
data: {
|
||||||
scope: this.runScriptContext.type,
|
scope: scope,
|
||||||
organizationalUnit: this.runScriptContext['@id'],
|
selectedClients: selectedClients,
|
||||||
source: 'assistant'
|
organizationalUnit: this.runScriptContext?.['@id'],
|
||||||
|
source: 'assistant',
|
||||||
|
runScriptContext: this.runScriptContext
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(result => {
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
console.log(result);
|
||||||
if (result) {
|
if (result) {
|
||||||
this.http.post(`${this.baseUrl}/command-task-scripts`, {
|
this.http.post(`${this.baseUrl}/command-task-scripts`, {
|
||||||
commandTask: result['@id'],
|
commandTask: result.taskId['@id'],
|
||||||
content: this.commandType === 'existing' ? this.scriptContent : this.newScript,
|
content: this.commandType === 'existing' ? this.scriptContent : this.newScript,
|
||||||
order: 1,
|
order: result.executionOrder,
|
||||||
type: 'run-script',
|
type: 'run-script',
|
||||||
}).subscribe({
|
}).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
|
|
@ -422,6 +422,10 @@
|
||||||
<mat-icon>article</mat-icon>
|
<mat-icon>article</mat-icon>
|
||||||
<span>Logs en tiempo real</span>
|
<span>Logs en tiempo real</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-menu-item (click)="openClientLogsModal($event, client)">
|
||||||
|
<mat-icon>analytics</mat-icon>
|
||||||
|
<span>Logs de cliente</span>
|
||||||
|
</button>
|
||||||
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span>{{ 'delete' | translate }}</span>
|
<span>{{ 'delete' | translate }}</span>
|
||||||
|
@ -613,6 +617,10 @@
|
||||||
<mat-icon>article</mat-icon>
|
<mat-icon>article</mat-icon>
|
||||||
<span>Logs en tiempo real</span>
|
<span>Logs en tiempo real</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button mat-menu-item (click)="openClientLogsModal($event, client)">
|
||||||
|
<mat-icon>analytics</mat-icon>
|
||||||
|
<span>Logs de cliente</span>
|
||||||
|
</button>
|
||||||
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||||
<mat-icon>delete</mat-icon>
|
<mat-icon>delete</mat-icon>
|
||||||
<span>{{ 'delete' | translate }}</span>
|
<span>{{ 'delete' | translate }}</span>
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { ClientTaskLogsComponent } from '../task-logs/client-task-logs/client-ta
|
||||||
import {ChangeParentComponent} from "./shared/change-parent/change-parent.component";
|
import {ChangeParentComponent} from "./shared/change-parent/change-parent.component";
|
||||||
import { AuthService } from '@services/auth.service';
|
import { AuthService } from '@services/auth.service';
|
||||||
import { ClientPendingTasksComponent } from '../task-logs/client-pending-tasks/client-pending-tasks.component';
|
import { ClientPendingTasksComponent } from '../task-logs/client-pending-tasks/client-pending-tasks.component';
|
||||||
|
import { ClientLogsModalComponent } from './shared/client-logs-modal/client-logs-modal.component';
|
||||||
|
|
||||||
enum NodeType {
|
enum NodeType {
|
||||||
OrganizationalUnit = 'organizational-unit',
|
OrganizationalUnit = 'organizational-unit',
|
||||||
|
@ -995,6 +996,27 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openClientLogsModal(event: MouseEvent, client: Client): void {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!client.mac) {
|
||||||
|
this.toastr.error('No se puede acceder a los logs: MAC del cliente no disponible', 'Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(ClientLogsModalComponent, {
|
||||||
|
width: '1400px',
|
||||||
|
height: '90vh',
|
||||||
|
data: { client },
|
||||||
|
disableClose: false,
|
||||||
|
hasBackdrop: true,
|
||||||
|
backdropClass: 'non-clickable-backdrop',
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((result) => {
|
||||||
|
// El modal se cerró
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
openOUPendingTasks(event: MouseEvent, node: any): void {
|
openOUPendingTasks(event: MouseEvent, node: any): void {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
.client-logs-modal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grafana-button {
|
||||||
|
color: #666;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grafana-button:hover {
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-info {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-info p {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-info strong {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iframe-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-iframe {
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions button {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.logs-iframe {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 600px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.logs-iframe {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 400px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 12px 24px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
<div class="client-logs-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 mat-dialog-title>
|
||||||
|
<mat-icon>article</mat-icon>
|
||||||
|
Logs de cliente - {{ data.client.name }}
|
||||||
|
</h2>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button mat-icon-button (click)="openGrafanaInNewTab()" matTooltip="Abrir en Grafana" class="grafana-button">
|
||||||
|
<mat-icon>open_in_new</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="client-info">
|
||||||
|
<p><strong>Cliente:</strong> {{ data.client.name }}</p>
|
||||||
|
<p><strong>IP:</strong> {{ data.client.ip }}</p>
|
||||||
|
<p><strong>MAC:</strong> {{ data.client.mac }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="iframe-container">
|
||||||
|
<iframe
|
||||||
|
[src]="iframeUrl | safe"
|
||||||
|
width="1200"
|
||||||
|
height="800"
|
||||||
|
frameborder="0"
|
||||||
|
class="logs-iframe">
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="action-button" (click)="closeModal()">
|
||||||
|
Cerrar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-client-logs-modal',
|
||||||
|
templateUrl: './client-logs-modal.component.html',
|
||||||
|
styleUrls: ['./client-logs-modal.component.css']
|
||||||
|
})
|
||||||
|
export class ClientLogsModalComponent {
|
||||||
|
iframeUrl: string;
|
||||||
|
grafanaUrl: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dialogRef: MatDialogRef<ClientLogsModalComponent>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { client: any }
|
||||||
|
) {
|
||||||
|
const mac = this.data.client.mac || '';
|
||||||
|
this.iframeUrl = `https://localhost:3030/d-solo/opengnsys-clients/filebeat-clients?orgId=1&timezone=browser&var-hostname=$__all&var-mac=${mac}&refresh=5s&panelId=1&__feature.dashboardSceneSolo`;
|
||||||
|
this.grafanaUrl = `https://localhost:3030/d/opengnsys-clients/filebeat-clients?orgId=1&from=now-5m&to=now&timezone=browser&var-hostname=$__all&var-mac=${mac}&refresh=5s&viewPanel=panel-1`;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
openGrafanaInNewTab(): void {
|
||||||
|
window.open(this.grafanaUrl, '_blank');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
.dialog-content {
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-info {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-info p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 0 24px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ordinary-button {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ordinary-button:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover:not(:disabled) {
|
||||||
|
background-color: #303f9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<app-loading [isLoading]="loading"></app-loading>
|
||||||
|
|
||||||
|
<h2 mat-dialog-title>Crear Rama desde Commit</h2>
|
||||||
|
|
||||||
|
<mat-dialog-content class="dialog-content">
|
||||||
|
<div class="commit-info">
|
||||||
|
<p><strong>Commit ID:</strong> {{ data.commit?.hexsha }}</p>
|
||||||
|
<p><strong>Mensaje:</strong> {{ data.commit?.message }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form [formGroup]="branchForm" (ngSubmit)="createBranch()" class="branch-form">
|
||||||
|
<mat-form-field appearance="fill" class="form-field">
|
||||||
|
<mat-label>Nombre de la rama</mat-label>
|
||||||
|
<input matInput formControlName="name" placeholder="ej: feature-nueva-funcionalidad" required>
|
||||||
|
<mat-error *ngIf="branchForm.get('name')?.hasError('required')">
|
||||||
|
El nombre de la rama es obligatorio
|
||||||
|
</mat-error>
|
||||||
|
<mat-error *ngIf="branchForm.get('name')?.hasError('pattern')">
|
||||||
|
El nombre de la rama solo puede contener letras, números, puntos, guiones y guiones bajos
|
||||||
|
</mat-error>
|
||||||
|
<mat-hint>Ejemplo: feature-nueva-funcionalidad, hotfix-bug-123, release-v2.0</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<div mat-dialog-actions class="action-container">
|
||||||
|
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||||
|
<button class="submit-button" (click)="createBranch()" [disabled]="branchForm.invalid || loading">
|
||||||
|
Crear Rama
|
||||||
|
</button>
|
||||||
|
</div>
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
import { ConfigService } from '@services/config.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-create-branch-modal',
|
||||||
|
templateUrl: './create-branch-modal.component.html',
|
||||||
|
styleUrl: './create-branch-modal.component.css'
|
||||||
|
})
|
||||||
|
export class CreateBranchModalComponent {
|
||||||
|
branchForm: FormGroup;
|
||||||
|
loading: boolean = false;
|
||||||
|
baseUrl: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private http: HttpClient,
|
||||||
|
public dialogRef: MatDialogRef<CreateBranchModalComponent>,
|
||||||
|
private toastService: ToastrService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { commit: any, repositoryName: string, repositoryUuid: string }
|
||||||
|
) {
|
||||||
|
this.baseUrl = this.configService.apiUrl;
|
||||||
|
this.branchForm = this.fb.group({
|
||||||
|
name: ['', [Validators.required, Validators.pattern(/^[a-zA-Z0-9._-]+$/)]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createBranch(): void {
|
||||||
|
if (this.branchForm.valid) {
|
||||||
|
this.loading = true;
|
||||||
|
const payload = {
|
||||||
|
commit: this.data.commit?.hexsha || 'master',
|
||||||
|
name: this.branchForm.value.name,
|
||||||
|
repository: this.data.repositoryName
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/create-branch`;
|
||||||
|
|
||||||
|
this.http.post(url, payload).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
this.toastService.success('Rama creada correctamente');
|
||||||
|
this.dialogRef.close(response);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.toastService.error(error.error?.message || 'Error al crear la rama');
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.toastService.error('Por favor, complete todos los campos requeridos');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
.dialog-content {
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 500px;
|
||||||
|
padding: 0 24px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2[mat-dialog-title] {
|
||||||
|
padding: 24px 24px 0 24px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-info {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid #3f51b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-info p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commit-info strong {
|
||||||
|
color: #3f51b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 0 24px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ordinary-button {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ordinary-button:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
background-color: #3f51b5;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover:not(:disabled) {
|
||||||
|
background-color: #303f9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
<app-loading [isLoading]="loading"></app-loading>
|
||||||
|
|
||||||
|
<h2 mat-dialog-title>Crear Tag para Commit</h2>
|
||||||
|
|
||||||
|
<mat-dialog-content class="dialog-content">
|
||||||
|
<div class="commit-info">
|
||||||
|
<p><strong>Commit ID:</strong> {{ data.commit.hexsha }}</p>
|
||||||
|
<p><strong>Mensaje:</strong> {{ data.commit.message }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form [formGroup]="tagForm" (ngSubmit)="createTag()" class="tag-form">
|
||||||
|
<mat-form-field appearance="fill" class="form-field">
|
||||||
|
<mat-label>Nombre del tag</mat-label>
|
||||||
|
<input matInput formControlName="name" placeholder="ej: v1.0.0" required>
|
||||||
|
<mat-error *ngIf="tagForm.get('name')?.hasError('required')">
|
||||||
|
El nombre del tag es obligatorio
|
||||||
|
</mat-error>
|
||||||
|
<mat-error *ngIf="tagForm.get('name')?.hasError('pattern')">
|
||||||
|
El nombre del tag solo puede contener letras, números, puntos, guiones y guiones bajos
|
||||||
|
</mat-error>
|
||||||
|
<mat-hint>Ejemplo: v1.0.0, release-2024-01, hotfix-bug-123</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="fill" class="form-field">
|
||||||
|
<mat-label>Mensaje del tag</mat-label>
|
||||||
|
<textarea matInput formControlName="message" placeholder="Descripción del tag" required rows="3"></textarea>
|
||||||
|
<mat-error *ngIf="tagForm.get('message')?.hasError('required')">
|
||||||
|
El mensaje del tag es obligatorio
|
||||||
|
</mat-error>
|
||||||
|
<mat-hint>Descripción del tag (ej: "Release estable de la versión 1.0")</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<div mat-dialog-actions class="action-container">
|
||||||
|
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||||
|
<button class="submit-button" (click)="createTag()" [disabled]="tagForm.invalid || loading">
|
||||||
|
Crear Tag
|
||||||
|
</button>
|
||||||
|
</div>
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { CreateTagModalComponent } from './create-tag-modal.component';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
import { ToastrModule } from 'ngx-toastr';
|
||||||
|
import { ConfigService } from '@services/config.service';
|
||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { LoadingComponent } from '../../../../shared/loading/loading.component';
|
||||||
|
|
||||||
|
describe('CreateTagModalComponent', () => {
|
||||||
|
let component: CreateTagModalComponent;
|
||||||
|
let fixture: ComponentFixture<CreateTagModalComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const mockToastrService = {
|
||||||
|
success: jasmine.createSpy('success'),
|
||||||
|
error: jasmine.createSpy('error'),
|
||||||
|
warning: jasmine.createSpy('warning'),
|
||||||
|
info: jasmine.createSpy('info')
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockConfigService = {
|
||||||
|
apiUrl: 'http://mock-api-url'
|
||||||
|
};
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [CreateTagModalComponent, LoadingComponent],
|
||||||
|
imports: [
|
||||||
|
MatDialogModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatButtonModule,
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
ToastrModule.forRoot()
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
FormBuilder,
|
||||||
|
HttpClient,
|
||||||
|
provideHttpClient(),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
{ provide: ToastrService, useValue: mockToastrService },
|
||||||
|
{ provide: ConfigService, useValue: mockConfigService },
|
||||||
|
{
|
||||||
|
provide: MatDialogRef,
|
||||||
|
useValue: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MAT_DIALOG_DATA,
|
||||||
|
useValue: {
|
||||||
|
commit: {
|
||||||
|
hexsha: 'test-commit-id',
|
||||||
|
message: 'Test commit message'
|
||||||
|
},
|
||||||
|
repositoryName: 'test-repo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CreateTagModalComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
import { ConfigService } from '@services/config.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-create-tag-modal',
|
||||||
|
templateUrl: './create-tag-modal.component.html',
|
||||||
|
styleUrl: './create-tag-modal.component.css'
|
||||||
|
})
|
||||||
|
export class CreateTagModalComponent {
|
||||||
|
tagForm: FormGroup;
|
||||||
|
loading: boolean = false;
|
||||||
|
baseUrl: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private http: HttpClient,
|
||||||
|
public dialogRef: MatDialogRef<CreateTagModalComponent>,
|
||||||
|
private toastService: ToastrService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: { commit: any, repositoryName: string, repositoryUuid: string }
|
||||||
|
) {
|
||||||
|
this.baseUrl = this.configService.apiUrl;
|
||||||
|
this.tagForm = this.fb.group({
|
||||||
|
name: ['', [Validators.required, Validators.pattern(/^[a-zA-Z0-9._-]+$/)]],
|
||||||
|
message: ['', Validators.required]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createTag(): void {
|
||||||
|
if (this.tagForm.valid) {
|
||||||
|
this.loading = true;
|
||||||
|
const payload = {
|
||||||
|
commit: this.data.commit.hexsha,
|
||||||
|
name: this.tagForm.value.name,
|
||||||
|
message: this.tagForm.value.message,
|
||||||
|
repository: this.data.repositoryName
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/create-tag`;
|
||||||
|
|
||||||
|
this.http.post(url, payload).subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
this.toastService.success('Tag creado correctamente');
|
||||||
|
this.dialogRef.close(response);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.toastService.error(error.error?.message || 'Error al crear el tag');
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.toastService.error('Por favor, complete todos los campos requeridos');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -95,6 +95,14 @@
|
||||||
<button mat-icon-button color="primary" (click)="toggleAction(commit, 'view-details')" matTooltip="Ver detalles">
|
<button mat-icon-button color="primary" (click)="toggleAction(commit, 'view-details')" matTooltip="Ver detalles">
|
||||||
<mat-icon>info</mat-icon>
|
<mat-icon>info</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button mat-icon-button color="accent" (click)="toggleAction(commit, 'create-tag')" matTooltip="Crear tag">
|
||||||
|
<mat-icon>local_offer</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button mat-icon-button color="warn" (click)="toggleAction(commit, 'create-branch')" matTooltip="Crear rama">
|
||||||
|
<mat-icon>account_tree</mat-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {Component, Inject, Input, isDevMode, OnInit} from '@angular/core';
|
import {Component, Inject, isDevMode, OnInit} from '@angular/core';
|
||||||
import {MatTableDataSource} from "@angular/material/table";
|
import {MatTableDataSource} from "@angular/material/table";
|
||||||
import {DatePipe} from "@angular/common";
|
import {DatePipe} from "@angular/common";
|
||||||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||||
|
@ -7,13 +7,9 @@ import {ToastrService} from "ngx-toastr";
|
||||||
import {JoyrideService} from "ngx-joyride";
|
import {JoyrideService} from "ngx-joyride";
|
||||||
import {ConfigService} from "@services/config.service";
|
import {ConfigService} from "@services/config.service";
|
||||||
import {Router} from "@angular/router";
|
import {Router} from "@angular/router";
|
||||||
import {Observable} from "rxjs";
|
|
||||||
import {ServerInfoDialogComponent} from "../../ogdhcp/server-info-dialog/server-info-dialog.component";
|
import {ServerInfoDialogComponent} from "../../ogdhcp/server-info-dialog/server-info-dialog.component";
|
||||||
import {ImportImageComponent} from "../import-image/import-image.component";
|
import {CreateTagModalComponent} from "./create-tag-modal/create-tag-modal.component";
|
||||||
import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component";
|
import {CreateBranchModalComponent} from "./create-branch-modal/create-branch-modal.component";
|
||||||
import {ExportImageComponent} from "../../images/export-image/export-image.component";
|
|
||||||
import {BackupImageComponent} from "../backup-image/backup-image.component";
|
|
||||||
import {EditImageComponent} from "../edit-image/edit-image.component";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-show-git-commits',
|
selector: 'app-show-git-commits',
|
||||||
|
@ -212,6 +208,12 @@ export class ShowGitCommitsComponent implements OnInit{
|
||||||
this.toastService.success('Commit ID copiado al portapapeles');
|
this.toastService.success('Commit ID copiado al portapapeles');
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'create-tag':
|
||||||
|
this.openCreateTagDialog(commit);
|
||||||
|
break;
|
||||||
|
case 'create-branch':
|
||||||
|
this.openCreateBranchDialog(commit);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.error('Acción no soportada:', action);
|
console.error('Acción no soportada:', action);
|
||||||
break;
|
break;
|
||||||
|
@ -253,6 +255,43 @@ export class ShowGitCommitsComponent implements OnInit{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openCreateTagDialog(commit: any) {
|
||||||
|
const dialogRef = this.dialog.open(CreateTagModalComponent, {
|
||||||
|
width: '500px',
|
||||||
|
data: {
|
||||||
|
commit: commit,
|
||||||
|
repositoryName: this.selectedRepository,
|
||||||
|
repositoryUuid: this.data.repositoryUuid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
// Recargar los datos para mostrar el nuevo tag
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openCreateBranchDialog(commit: any) {
|
||||||
|
const dialogRef = this.dialog.open(CreateBranchModalComponent, {
|
||||||
|
width: '500px',
|
||||||
|
data: {
|
||||||
|
commit: commit,
|
||||||
|
repositoryName: this.selectedRepository,
|
||||||
|
repositoryUuid: this.data.repositoryUuid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe(result => {
|
||||||
|
if (result) {
|
||||||
|
// Recargar los datos para mostrar la nueva rama
|
||||||
|
this.loadBranches();
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
goToPage(commit: any) {
|
goToPage(commit: any) {
|
||||||
window.open(`http://localhost:3100/oggit/${this.selectedRepository}/commit/${commit.hexsha}`, '_blank');
|
window.open(`http://localhost:3100/oggit/${this.selectedRepository}/commit/${commit.hexsha}`, '_blank');
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,8 +42,8 @@ export class HeaderComponent implements OnInit {
|
||||||
|
|
||||||
showGlobalStatus() {
|
showGlobalStatus() {
|
||||||
this.dialog.open(GlobalStatusComponent, {
|
this.dialog.open(GlobalStatusComponent, {
|
||||||
width: '65vw',
|
width: '80vw',
|
||||||
height: '80vh',
|
height: '85vh',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ export class QueueConfirmationModalComponent {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
onNoClick(): void {
|
onNoClick(): void {
|
||||||
this.dialogRef.close(false);
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
onYesClick(): void {
|
onYesClick(): void {
|
||||||
|
|
Loading…
Reference in New Issue