develop #33
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -1,4 +1,22 @@
|
|||
# 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
|
||||
### Added
|
||||
- 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 { ModalOverlayComponent } from './shared/modal-overlay/modal-overlay.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) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
|
@ -274,7 +277,10 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
SoftwareProfilePartitionComponent,
|
||||
ClientPendingTasksComponent,
|
||||
ModalOverlayComponent,
|
||||
ScrollToTopComponent
|
||||
ScrollToTopComponent,
|
||||
CreateTagModalComponent,
|
||||
CreateBranchModalComponent,
|
||||
ClientLogsModalComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
|
|
@ -33,9 +33,9 @@ export class CommandsTaskComponent implements OnInit {
|
|||
columns = [
|
||||
{ columnDef: 'id', header: 'ID', cell: (task: any) => task.id },
|
||||
{ columnDef: 'name', header: 'Nombre de tarea', cell: (task: any) => task.name },
|
||||
{ columnDef: 'organizationalUnit', header: 'Ámbito', cell: (task: any) => task.organizationalUnit.name },
|
||||
{ columnDef: 'organizationalUnit', header: 'Ámbito', cell: (task: any) => task.scope },
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
<input matInput formControlName="executionTime" placeholder="08:00" type="time">
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Mostrar solo si no es 'none' -->
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
|
||||
<label>Días de la semana:</label>
|
||||
<div class="weekday-toggle-group">
|
||||
|
@ -37,7 +36,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selección de meses -->
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
|
||||
<label>Meses:</label>
|
||||
<div class="month-toggle-row" *ngFor="let row of monthRows">
|
||||
|
@ -52,7 +50,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rango de fechas -->
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails">
|
||||
<mat-form-field appearance="fill" class="w-half">
|
||||
<mat-label>Desde</mat-label>
|
||||
|
|
|
@ -19,6 +19,14 @@ mat-form-field {
|
|||
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 {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
|
@ -63,3 +71,148 @@ mat-form-field {
|
|||
gap: 1em;
|
||||
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-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-button value="create">Crear tarea</mat-radio-button>
|
||||
<mat-radio-button value="add">Introducir en tarea existente</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
|
||||
<!-- Selección de tarea existente -->
|
||||
<div *ngIf="taskMode === 'add'" class="select-task">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione una tarea</mat-label>
|
||||
|
@ -33,7 +31,6 @@
|
|||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de nueva tarea -->
|
||||
<form *ngIf="taskMode === 'create' && taskForm && !loading" [formGroup]="taskForm" class="task-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
|
@ -48,15 +45,17 @@
|
|||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<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="classrooms-group">Grupo de aulas</mat-option>
|
||||
<mat-option value="classroom">Aulas</mat-option>
|
||||
<mat-option value="clients-group">Grupos de clientes</mat-option>
|
||||
<mat-option value="clients">Clientes</mat-option>
|
||||
</mat-select>
|
||||
</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-select formControlName="organizationalUnit">
|
||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||
|
@ -66,6 +65,40 @@
|
|||
</mat-select>
|
||||
</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">
|
||||
¿Quieres programar la tarea al finalizar su creación?
|
||||
</mat-checkbox>
|
||||
|
|
|
@ -29,6 +29,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
existingTasks: any[] = [];
|
||||
selectedExistingTask: string | null = null;
|
||||
executionOrder: number | null = null;
|
||||
selectedClients: any[] = [];
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
|
@ -41,13 +42,32 @@ export class CreateTaskComponent implements OnInit {
|
|||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
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({
|
||||
scope: [ this.data?.scope ? this.data.scope : '', Validators.required],
|
||||
scope: [initialScope, Validators.required],
|
||||
name: ['', Validators.required],
|
||||
organizationalUnit: [ this.data?.organizationalUnit ? this.data.organizationalUnit : null, Validators.required],
|
||||
organizationalUnit: [ this.data?.organizationalUnit ? this.data.organizationalUnit : null],
|
||||
notes: [''],
|
||||
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 {
|
||||
|
@ -111,10 +131,31 @@ export class CreateTaskComponent implements OnInit {
|
|||
}
|
||||
|
||||
onScopeChange(scope: string): void {
|
||||
this.filterUnits(scope).subscribe(filteredUnits => {
|
||||
this.availableOrganizationalUnits = filteredUnits;
|
||||
if (scope === 'clients') {
|
||||
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')?.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> {
|
||||
|
@ -209,14 +250,31 @@ export class CreateTaskComponent implements OnInit {
|
|||
}
|
||||
|
||||
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 = {
|
||||
name: formData.name,
|
||||
scope: formData.scope,
|
||||
organizationalUnit: formData.organizationalUnit,
|
||||
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) {
|
||||
const taskId = this.data.task.uuid;
|
||||
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({
|
||||
|
|
|
@ -28,7 +28,7 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
columns = [
|
||||
{ columnDef: 'id', header: 'ID', cell: (schedule: any) => schedule.id },
|
||||
{ columnDef: 'recurrenceType', header: 'Recurrencia', cell: (schedule: any) => schedule.recurrenceType },
|
||||
{ columnDef: 'time', header: 'Hora de ejecución', cell: (schedule: any) => this.datePipe.transform(schedule.executionTime, 'HH:mm', '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: 'months', header: 'Meses', cell: (schedule: any) => schedule.recurrenceDetails.months },
|
||||
{ columnDef: 'enabled', header: 'Activo', cell: (schedule: any) => schedule.enabled }
|
||||
|
|
|
@ -107,18 +107,18 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
|
||||
this.arrayCommands = this.arrayCommands.map(command => {
|
||||
if (allOffOrDisconnected) {
|
||||
command.disabled = command.slug !== 'power-on';
|
||||
command.disabled = command.slug !== 'power-on' && command.slug !== 'create-image';
|
||||
} else if (allSameState) {
|
||||
if (states[0] === 'off' || states[0] === 'disconnected') {
|
||||
command.disabled = command.slug !== 'power-on';
|
||||
command.disabled = !['power-on', 'create-image'].includes(command.slug);
|
||||
} else {
|
||||
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'remove-cache-image', 'partition', 'run-script', 'software-inventory'].includes(command.slug);
|
||||
}
|
||||
} else {
|
||||
if (command.slug === 'create-image'|| command.slug === 'software-inventory') {
|
||||
if (command.slug === 'software-inventory') {
|
||||
command.disabled = multipleClients;
|
||||
} 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;
|
||||
} else {
|
||||
|
|
|
@ -184,6 +184,21 @@ mat-dialog-content {
|
|||
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 {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 50%;
|
||||
|
@ -215,6 +230,28 @@ mat-dialog-content {
|
|||
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 ===== */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
|
@ -554,4 +591,296 @@ mat-dialog-content {
|
|||
margin: 0;
|
||||
color: #666;
|
||||
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-content">
|
||||
<div class="welcome-icon">
|
||||
|
@ -17,9 +16,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenido principal -->
|
||||
<mat-dialog-content [ngClass]="{'loading': loading}">
|
||||
<!-- Spinner de carga -->
|
||||
<div class="spinner-container" *ngIf="loading">
|
||||
<div class="loading-content">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
|
@ -27,11 +24,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenido principal cuando no está cargando -->
|
||||
<div *ngIf="!loading" class="main-content">
|
||||
<!-- Resumen rápido del sistema -->
|
||||
<div class="system-overview">
|
||||
<div class="overview-card">
|
||||
<div class="overview-card clickable"
|
||||
[ngClass]="{'active': selectedSection === 'repositories'}"
|
||||
(click)="selectSection('repositories')">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>cloud</mat-icon>
|
||||
</div>
|
||||
|
@ -41,7 +38,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="overview-card clickable"
|
||||
[ngClass]="{'active': selectedSection === 'ogboot'}"
|
||||
(click)="selectSection('ogboot')">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>storage</mat-icon>
|
||||
</div>
|
||||
|
@ -52,7 +51,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="overview-card clickable"
|
||||
[ngClass]="{'active': selectedSection === 'dhcp'}"
|
||||
(click)="selectSection('dhcp')">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>router</mat-icon>
|
||||
</div>
|
||||
|
@ -62,11 +63,22 @@
|
|||
<p *ngIf="errorDhcp">Estado: <span class="status-badge offline">Error</span></p>
|
||||
</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>
|
||||
|
||||
<!-- Tabs principales -->
|
||||
<mat-tab-group (selectedTabChange)="onTabChange($event)" class="main-tabs">
|
||||
<mat-tab label="{{ 'repositoryLabel' | translate }}">
|
||||
<div class="dynamic-content">
|
||||
<div *ngIf="selectedSection === 'repositories'" class="section-content">
|
||||
<div class="repositories-container">
|
||||
<div *ngIf="repositories.length === 0" class="no-repositories">
|
||||
<mat-icon class="no-data-icon">cloud_off</mat-icon>
|
||||
|
@ -75,7 +87,6 @@
|
|||
</div>
|
||||
|
||||
<div *ngIf="repositories.length > 0" class="repositories-selector-container">
|
||||
<!-- Selector de repositorio -->
|
||||
<div class="repository-selector">
|
||||
<mat-form-field appearance="outline" class="repository-select-field">
|
||||
<mat-label>Seleccionar repositorio</mat-label>
|
||||
|
@ -88,11 +99,14 @@
|
|||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Información del repositorio seleccionado -->
|
||||
<div *ngIf="selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]" class="selected-repository-content">
|
||||
<div class="repository-item">
|
||||
<div class="repository-header">
|
||||
<h3>{{ getSelectedRepositoryName() }}</h3>
|
||||
<div class="section-header">
|
||||
<h2>{{ getSelectedRepositoryName() }}</h2>
|
||||
<button class="logs-button" (click)="scrollToLogs()">
|
||||
<mat-icon>article</mat-icon>
|
||||
<span>Ver logs</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="repository-content">
|
||||
|
@ -111,16 +125,30 @@
|
|||
</app-status-tab>
|
||||
</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>
|
||||
|
||||
<!-- Mensaje cuando no hay repositorio seleccionado -->
|
||||
<div *ngIf="!selectedRepositoryUuid" class="no-repository-selected">
|
||||
<mat-icon class="no-data-icon">storage</mat-icon>
|
||||
<h3>Selecciona un repositorio</h3>
|
||||
<p>Elige un repositorio de la lista para ver su estado detallado.</p>
|
||||
</div>
|
||||
|
||||
<!-- Error al cargar repositorio -->
|
||||
<div *ngIf="selectedRepositoryUuid && errorRepositories[selectedRepositoryUuid]" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
|
@ -134,15 +162,39 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</div>
|
||||
|
||||
<mat-tab label="OgBoot Server">
|
||||
<div *ngIf="!errorOgBoot && !loadingOgBoot" class="tab-content">
|
||||
<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 *ngIf="selectedSection === 'ogboot'" class="section-content">
|
||||
<div *ngIf="!errorOgBoot && !loadingOgBoot" class="tab-content">
|
||||
<div class="section-header">
|
||||
<h2>OgBoot Server</h2>
|
||||
<button class="logs-button" (click)="scrollToLogs()">
|
||||
<mat-icon>article</mat-icon>
|
||||
<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 *ngIf="loadingOgBoot" class="loading-container">
|
||||
<div class="loading-content">
|
||||
|
@ -161,14 +213,38 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</div>
|
||||
|
||||
<mat-tab label="DHCP Server">
|
||||
<div *ngIf="!errorDhcp && !loadingDhcp" class="tab-content">
|
||||
<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 *ngIf="selectedSection === 'dhcp'" class="section-content">
|
||||
<div *ngIf="!errorDhcp && !loadingDhcp" class="tab-content">
|
||||
<div class="section-header">
|
||||
<h2>DHCP Server</h2>
|
||||
<button class="logs-button" (click)="scrollToLogs()">
|
||||
<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 *ngIf="loadingDhcp" class="loading-container">
|
||||
<div class="loading-content">
|
||||
|
@ -187,12 +263,36 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
</mat-dialog-content>
|
||||
|
||||
<!-- Footer con acciones -->
|
||||
<mat-dialog-actions class="action-container">
|
||||
<div class="action-info">
|
||||
<p class="last-update">Última actualización: {{ lastUpdateTime }}</p>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { MatTabChangeEvent } from '@angular/material/tabs';
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-global-status',
|
||||
|
@ -31,6 +30,7 @@ export class GlobalStatusComponent implements OnInit {
|
|||
repositoryStatuses: { [key: string]: any } = {};
|
||||
lastUpdateTime: string = '';
|
||||
selectedRepositoryUuid: string = '';
|
||||
selectedSection: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore' = 'repositories';
|
||||
|
||||
ogBootApiUrl: string;
|
||||
ogBootDiskUsage: any = {};
|
||||
|
@ -44,7 +44,8 @@ export class GlobalStatusComponent implements OnInit {
|
|||
isDhcp: boolean = false;
|
||||
isRepository: boolean = false;
|
||||
|
||||
// Loading específicos para cada sección
|
||||
ogCoreApiUrl: string;
|
||||
|
||||
loadingOgBootOgLives: boolean = false;
|
||||
loadingOgBootServices: boolean = false;
|
||||
loadingOgBootDisk: boolean = false;
|
||||
|
@ -60,6 +61,7 @@ export class GlobalStatusComponent implements OnInit {
|
|||
this.baseUrl = this.configService.apiUrl;
|
||||
this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
|
||||
this.dhcpApiUrl = `${this.baseUrl}/og-dhcp/status`;
|
||||
this.ogCoreApiUrl = `${this.baseUrl}`;
|
||||
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
this.selectedRepositoryUuid = repositoryUuid;
|
||||
|
@ -324,6 +312,15 @@ export class GlobalStatusComponent implements OnInit {
|
|||
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 {
|
||||
this.loading = true;
|
||||
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>
|
||||
|
||||
<div *ngIf="!loading" class="dashboard">
|
||||
<!-- Sección de uso de recursos -->
|
||||
<div class="resources-section">
|
||||
<!-- Disk Usage Section -->
|
||||
<div class="resource-card disk-usage-container">
|
||||
<div class="resource-header">
|
||||
<div class="resource-icon">
|
||||
|
@ -32,13 +30,12 @@
|
|||
</div>
|
||||
<div class="info-item">
|
||||
<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>
|
||||
|
||||
<!-- RAM Usage Section -->
|
||||
<div class="resource-card ram-usage-container" *ngIf="isRepository">
|
||||
<div class="resource-header">
|
||||
<div class="resource-icon">
|
||||
|
@ -67,13 +64,12 @@
|
|||
</div>
|
||||
<div class="info-item">
|
||||
<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>
|
||||
|
||||
<!-- CPU Usage Section -->
|
||||
<div class="resource-card cpu-usage-container" *ngIf="isRepository">
|
||||
<div class="resource-header">
|
||||
<div class="resource-icon">
|
||||
|
@ -84,7 +80,7 @@
|
|||
<div class="resource-content">
|
||||
<div class="cpu-usage-display">
|
||||
<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>
|
||||
</div>
|
||||
|
@ -92,9 +88,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección de servicios y procesos -->
|
||||
<div class="services-section">
|
||||
<!-- Services Status Section -->
|
||||
<div class="service-card services-status" joyrideStep="servicesStatusStep" text="{{ 'servicesStatusDescription' | translate }}">
|
||||
<div class="service-header">
|
||||
<div class="service-icon">
|
||||
|
@ -116,7 +110,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processes Status Section -->
|
||||
<div class="service-card processes-status" *ngIf="isRepository">
|
||||
<div class="service-header">
|
||||
<div class="service-icon">
|
||||
|
@ -139,9 +132,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección de datos específicos -->
|
||||
<div class="data-section">
|
||||
<!-- Installed OgLives Section -->
|
||||
<div class="data-card" *ngIf="!isRepository && !isDhcp">
|
||||
<div class="data-header">
|
||||
<div class="data-icon">
|
||||
|
@ -171,7 +162,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subnets Section -->
|
||||
<div class="data-card" *ngIf="isDhcp">
|
||||
<div class="data-header">
|
||||
<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
|
||||
[isVisible]="creatingRepository"
|
||||
message="Creando repositorio...">
|
||||
|
@ -20,12 +21,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<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 class="select-container">
|
||||
<!-- Sección: Configuración de tipo de imagen -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>settings</mat-icon>
|
||||
|
@ -43,15 +43,81 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección: Configuración Git (solo para tipo git) -->
|
||||
<div class="form-section" *ngIf="imageType === 'git'">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>code</mat-icon>
|
||||
Configuración Git
|
||||
</div>
|
||||
|
||||
<div class="git-repository-section">
|
||||
<div class="repository-header">
|
||||
<div class="git-info-container" *ngIf="selectedPartition && imageType === 'git' && (hasValidGitData || loadingGitData)">
|
||||
<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'"
|
||||
class="create-repository-button"
|
||||
(click)="openCreateRepositoryModal()"
|
||||
|
@ -61,95 +127,82 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="selector">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccionar repositorio Git</mat-label>
|
||||
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositorySelected($event.value)" required>
|
||||
<mat-option [value]="null">Seleccionar repositorio git / SO</mat-option>
|
||||
<mat-option *ngFor="let repo of gitRepositories" [value]="repo">{{ repo.name }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-spinner *ngIf="loadingGitRepositories" matSuffix diameter="20"></mat-spinner>
|
||||
<mat-hint>
|
||||
<mat-icon>info</mat-icon>
|
||||
Selecciona el repositorio git para obtener las imágenes disponibles.
|
||||
<span *ngIf="gitRepositories.length === 0" class="no-repositories-hint">
|
||||
No hay repositorios disponibles. Crea uno nuevo para continuar.
|
||||
</span>
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="selector" *ngIf="!hasValidGitData">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Seleccionar repositorio Git</mat-label>
|
||||
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositorySelected($event.value)" required>
|
||||
<mat-option [value]="null">Seleccionar repositorio git / SO</mat-option>
|
||||
<mat-option *ngFor="let repo of gitRepositories" [value]="repo">{{ repo.name }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-spinner *ngIf="loadingGitRepositories" matSuffix diameter="20"></mat-spinner>
|
||||
<mat-hint>
|
||||
<mat-icon>info</mat-icon>
|
||||
Selecciona el repositorio git para obtener las imágenes disponibles.
|
||||
<span *ngIf="gitRepositories.length === 0" class="no-repositories-hint">
|
||||
No hay repositorios disponibles. Crea uno nuevo para continuar.
|
||||
</span>
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Opciones de acción Git -->
|
||||
<div class="git-action-selector">
|
||||
<div class="action-chips-container">
|
||||
<mat-chip-listbox [(ngModel)]="gitAction" required class="action-chip-listbox">
|
||||
<mat-chip-option value="create" class="action-chip create-chip firmware-chip" (click)="onGitActionSelected({value: 'create'})">
|
||||
<span>Crear imagen</span>
|
||||
</mat-chip-option>
|
||||
<mat-chip-option value="update" class="action-chip update-chip firmware-chip" (click)="onGitActionSelected({value: 'update'})">
|
||||
<span>Actualizar imagen</span>
|
||||
</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
|
||||
<button class="create-branch-button"
|
||||
*ngIf="selectedGitRepository"
|
||||
(click)="openCreateBranchModal()"
|
||||
[disabled]="loadingBranches"
|
||||
matTooltip="Crear nueva rama"
|
||||
style="display: none;">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>Crear rama</span>
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<!-- 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-title">
|
||||
<mat-icon>storage</mat-icon>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { ConfigService } from '@services/config.service';
|
|||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
|
||||
import {CreateRepositoryModalComponent} from "./create-repository-modal/create-repository-modal.component";
|
||||
import {CreateBranchModalComponent} from "../../../../repositories/show-git-images/create-branch-modal/create-branch-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-image',
|
||||
|
@ -38,12 +39,27 @@ export class CreateClientImageComponent implements OnInit{
|
|||
loadingGitRepositories: boolean = false;
|
||||
loadingGitImageRepositories: boolean = false;
|
||||
creatingRepository: boolean = false;
|
||||
gitAction: string = 'create';
|
||||
monolithicAction: string = 'create';
|
||||
existingImages: any[] = [];
|
||||
selectedExistingImage: any = null;
|
||||
loadingExistingImages: boolean = false;
|
||||
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 = [
|
||||
{
|
||||
columnDef: 'diskNumber',
|
||||
|
@ -107,9 +123,19 @@ export class CreateClientImageComponent implements OnInit{
|
|||
this.clientName = response.name;
|
||||
this.selectedRepository = response.repository;
|
||||
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
const validPartitions = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0;
|
||||
});
|
||||
|
||||
this.dataSource.data = validPartitions;
|
||||
|
||||
const firstValidPartition = validPartitions.find((partition: any) => {
|
||||
return partition.operativeSystem;
|
||||
});
|
||||
|
||||
if (firstValidPartition) {
|
||||
this.selectedPartition = firstValidPartition;
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
|
@ -141,6 +167,23 @@ export class CreateClientImageComponent implements OnInit{
|
|||
(response: any) => {
|
||||
this.gitRepositories = response['hydra:member'];
|
||||
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) => {
|
||||
console.error('Error al cargar los repositorios git:', error);
|
||||
|
@ -151,7 +194,7 @@ export class CreateClientImageComponent implements OnInit{
|
|||
|
||||
loadGitImageRepositories(gitRepository: any) {
|
||||
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(
|
||||
(response: any) => {
|
||||
this.gitImageRepositories = response['hydra:member'];
|
||||
|
@ -168,47 +211,97 @@ export class CreateClientImageComponent implements OnInit{
|
|||
this.selectedGitRepository = gitRepository;
|
||||
this.selectedExistingImage = null;
|
||||
this.existingImages = [];
|
||||
this.selectedBranch = '';
|
||||
this.branches = [];
|
||||
|
||||
if (gitRepository) {
|
||||
this.loadGitImageRepositories(gitRepository);
|
||||
this.loadBranches();
|
||||
} else {
|
||||
this.gitImageRepositories = [];
|
||||
}
|
||||
}
|
||||
|
||||
onGitActionSelected(event: any) {
|
||||
console.log('onGitActionSelected llamado con:', event);
|
||||
this.gitAction = event.value;
|
||||
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);
|
||||
loadBranches(): void {
|
||||
if (!this.selectedGitRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Antes del setTimeout');
|
||||
// Hacer scroll hacia la sección de partición después de un delay más largo
|
||||
setTimeout(() => {
|
||||
console.log('Dentro del setTimeout, llamando a scrollToPartitionSection');
|
||||
this.scrollToPartitionSection();
|
||||
}, 300);
|
||||
this.loadingBranches = true;
|
||||
const url = `${this.baseUrl}/image-repositories/server/git/${this.selectedRepository.uuid}/branches`;
|
||||
this.http.post<any>(url, { repositoryName: this.selectedGitRepository.name }).subscribe(
|
||||
data => {
|
||||
this.branches = data.branches || [];
|
||||
this.loadingBranches = false;
|
||||
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) {
|
||||
console.log('onMonolithicActionSelected llamado con:', event);
|
||||
this.monolithicAction = event.value;
|
||||
this.selectedImage = null;
|
||||
this.name = '';
|
||||
|
||||
// Si se selecciona 'update', cargar las imágenes existentes
|
||||
if (event.value === 'update') {
|
||||
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(() => {
|
||||
console.log('Dentro del setTimeout (monolithic), llamando a scrollToPartitionSection');
|
||||
this.scrollToPartitionSection();
|
||||
}, 300);
|
||||
}
|
||||
|
@ -217,8 +310,7 @@ export class CreateClientImageComponent implements OnInit{
|
|||
if (!this.selectedExistingImage) return;
|
||||
|
||||
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`;
|
||||
|
||||
this.http.get(url).subscribe(
|
||||
|
@ -238,14 +330,15 @@ export class CreateClientImageComponent implements OnInit{
|
|||
this.selectedGitRepository = null;
|
||||
this.selectedExistingImage = null;
|
||||
this.gitImageName = '';
|
||||
this.gitAction = 'create';
|
||||
this.monolithicAction = 'create';
|
||||
this.existingImages = [];
|
||||
this.gitRepositories = [];
|
||||
this.gitImageRepositories = [];
|
||||
this.selectedBranch = '';
|
||||
this.branches = [];
|
||||
|
||||
this.selectedImage = null;
|
||||
this.name = '';
|
||||
this.monolithicAction = 'create';
|
||||
}
|
||||
|
||||
resetCanonicalName() {
|
||||
|
@ -258,16 +351,6 @@ export class CreateClientImageComponent implements OnInit{
|
|||
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.monolithicAction === 'create' && !this.name) {
|
||||
|
@ -295,24 +378,46 @@ export class CreateClientImageComponent implements OnInit{
|
|||
if (result !== undefined) {
|
||||
this.loading = true;
|
||||
|
||||
let payload: any = {
|
||||
client: `/clients/${this.clientId}`,
|
||||
partition: this.selectedPartition['@id'],
|
||||
source: 'assistant',
|
||||
type: this.imageType,
|
||||
queue: result
|
||||
};
|
||||
let endpoint: string;
|
||||
let payload: any;
|
||||
|
||||
if (this.imageType === 'git') {
|
||||
payload.gitRepository = this.selectedGitRepository.name
|
||||
payload.name = this.selectedGitRepository.name;
|
||||
const gitRepoName = this.hasValidGitData ? this.gitData.repo : this.selectedGitRepository?.name;
|
||||
const originBranch = this.hasValidGitData ? this.gitData.branch : this.selectedBranch;
|
||||
|
||||
if (this.gitAction === 'create') {
|
||||
payload.action = 'create';
|
||||
if (this.branches.length === 0) {
|
||||
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 {
|
||||
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 {
|
||||
endpoint = `${this.baseUrl}/images`;
|
||||
payload = {
|
||||
client: `/clients/${this.clientId}`,
|
||||
partition: this.selectedPartition['@id'],
|
||||
type: this.imageType,
|
||||
queue: result
|
||||
};
|
||||
|
||||
if (this.monolithicAction === 'create') {
|
||||
payload.name = this.name;
|
||||
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({
|
||||
next: (response) => {
|
||||
let actionText = 'creación';
|
||||
if (this.imageType === 'git' && this.gitAction === 'update') {
|
||||
if (this.imageType === 'monolithic' && this.monolithicAction === 'update') {
|
||||
actionText = 'actualización';
|
||||
} else if (this.imageType === 'monolithic' && this.monolithicAction === 'update') {
|
||||
} else if (this.imageType === 'git' && this.branches.length > 0) {
|
||||
actionText = 'actualización';
|
||||
}
|
||||
this.toastService.success(`Petición de ${actionText} de imagen enviada`);
|
||||
|
@ -359,14 +464,28 @@ export class CreateClientImageComponent implements OnInit{
|
|||
dialogRef.afterClosed().subscribe(result => {
|
||||
this.creatingRepository = false;
|
||||
if (result) {
|
||||
|
||||
|
||||
this.newlyCreatedRepository = result;
|
||||
|
||||
this.loadGitRepositories();
|
||||
|
||||
setTimeout(() => {
|
||||
const newRepository = this.gitRepositories.find(repo => repo['@id'] === result['@id']);
|
||||
if (newRepository) {
|
||||
this.selectedGitRepository = newRepository;
|
||||
this.onGitRepositorySelected(newRepository);
|
||||
if (this.newlyCreatedRepository && !this.selectedGitRepository) {
|
||||
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;
|
||||
}
|
||||
}, 200);
|
||||
}, 1000);
|
||||
} else {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -383,8 +502,13 @@ export class CreateClientImageComponent implements OnInit{
|
|||
this.selectedImage = null;
|
||||
this.name = '';
|
||||
this.monolithicAction = 'create';
|
||||
|
||||
if (this.selectedPartition) {
|
||||
this.loadGitData(this.selectedPartition);
|
||||
}
|
||||
} else {
|
||||
this.resetGitSelections();
|
||||
this.gitData = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -414,5 +538,82 @@ export class CreateClientImageComponent implements OnInit{
|
|||
|
||||
set selectedPartition(value: any) {
|
||||
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-icon matSuffix *ngIf="loadingBranches">hourglass_empty</mat-icon>
|
||||
</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>
|
||||
|
||||
<!-- Tabla de commits (solo si imageType === 'git') -->
|
||||
<div *ngIf="imageType === 'git' && commits.length > 0" class="commits-table-container">
|
||||
<table mat-table [dataSource]="commits" class="mat-elevation-z8">
|
||||
<div *ngIf="imageType === 'git' && filteredCommits.length > 0" class="commits-table-container">
|
||||
<table mat-table [dataSource]="filteredCommits" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>Seleccionar</th>
|
||||
<td mat-cell *matCellDef="let commit">
|
||||
|
@ -162,8 +169,11 @@
|
|||
<th mat-header-cell *matHeaderCellDef>Tags</th>
|
||||
<td mat-cell *matCellDef="let commit">
|
||||
<mat-chip-list>
|
||||
<mat-chip *ngFor="let tag of commit.tags" color="primary" selected>{{ tag }}</mat-chip>
|
||||
<span *ngIf="!commit.tags || commit.tags.length === 0" style="color: #999; font-style: italic;">Sin tags</span>
|
||||
<mat-chip *ngFor="let tag of commit.tags" color="primary" selected class="custom-tag-chip"
|
||||
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>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
@ -172,7 +182,6 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Selector de método y de imagen solo si imageType === 'monolithic' -->
|
||||
<div class="monolithic-row" *ngIf="imageType === 'monolithic'">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Seleccione método de deploy</mat-label>
|
||||
|
|
|
@ -109,6 +109,9 @@ export class DeployImageComponent implements OnInit{
|
|||
selectedCommit: any = null;
|
||||
private initialGitLoad = true;
|
||||
|
||||
showOnlyTagged: boolean = false;
|
||||
filteredCommits: any[] = [];
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
|
@ -387,7 +390,7 @@ export class DeployImageComponent implements OnInit{
|
|||
}
|
||||
|
||||
if (this.imageType === 'git') {
|
||||
if (!this.selectedCommit) {
|
||||
if (!this.selectedCommit || this.filteredCommits.length === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -418,17 +421,28 @@ export class DeployImageComponent implements OnInit{
|
|||
|
||||
|
||||
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, {
|
||||
width: '800px',
|
||||
data: {
|
||||
scope: this.runScriptContext.type,
|
||||
organizationalUnit: this.runScriptContext['@id'],
|
||||
source: 'assistant'
|
||||
scope: scope,
|
||||
selectedClients: selectedClients,
|
||||
organizationalUnit: this.runScriptContext?.['@id'],
|
||||
source: 'assistant',
|
||||
runScriptContext: this.runScriptContext
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: { [x: string]: any; }) => {
|
||||
if (result) {
|
||||
if (result !== undefined) {
|
||||
const payload = {
|
||||
method: this.selectedMethod,
|
||||
diskNumber: this.selectedPartition.diskNumber,
|
||||
|
@ -519,7 +533,9 @@ export class DeployImageComponent implements OnInit{
|
|||
this.selectedBranch = '';
|
||||
this.branches = [];
|
||||
this.commits = [];
|
||||
this.filteredCommits = [];
|
||||
this.selectedCommit = null;
|
||||
this.showOnlyTagged = false;
|
||||
this.loadGitBranches();
|
||||
}
|
||||
|
||||
|
@ -545,6 +561,8 @@ export class DeployImageComponent implements OnInit{
|
|||
onGitBranchChange() {
|
||||
this.selectedCommit = null;
|
||||
this.commits = [];
|
||||
this.filteredCommits = [];
|
||||
this.showOnlyTagged = false;
|
||||
this.loadGitCommits();
|
||||
}
|
||||
|
||||
|
@ -558,6 +576,12 @@ export class DeployImageComponent implements OnInit{
|
|||
data => {
|
||||
this.commits = data.commits || [];
|
||||
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 => {
|
||||
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;
|
||||
}
|
||||
|
||||
.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 */
|
||||
::ng-deep .disk-option {
|
||||
display: flex;
|
||||
|
@ -232,7 +248,9 @@
|
|||
background: #e8f5e8;
|
||||
border: 1px solid #c8e6c9;
|
||||
border-radius: 12px;
|
||||
margin-top: 16px;
|
||||
margin-top: 0;
|
||||
flex-shrink: 0;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
|
@ -709,8 +727,454 @@
|
|||
margin: 5px 0 !important;
|
||||
}
|
||||
|
||||
/* ===== ESTADOS DE ADVERTENCIA ===== */
|
||||
/* Advertencia (90% a 99% usado) */
|
||||
/* ===== LAYOUT PRINCIPAL ===== */
|
||||
.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 {
|
||||
color: #ff9800 !important;
|
||||
}
|
||||
|
@ -723,7 +1187,6 @@
|
|||
color: #ff9800 !important;
|
||||
}
|
||||
|
||||
/* Peligro (100% o más usado) */
|
||||
.danger {
|
||||
color: #f44336 !important;
|
||||
font-weight: bold !important;
|
||||
|
@ -751,7 +1214,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* ===== INSTRUCCIONES ===== */
|
||||
.instructions-box {
|
||||
margin-top: 15px;
|
||||
background-color: #f5f5f5;
|
||||
|
@ -780,7 +1242,6 @@
|
|||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ===== RESPONSIVE ===== */
|
||||
@media (max-width: 768px) {
|
||||
.header-container {
|
||||
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 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 class="button-row">
|
||||
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
|
@ -51,7 +51,7 @@
|
|||
<div class="clients-grid">
|
||||
<div *ngFor="let client of clientData" class="client-item">
|
||||
<div class="client-card"
|
||||
(click)="client.status === 'og-live' && toggleClientSelection(client)"
|
||||
(click)="toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected}"
|
||||
[matTooltip]="getPartitionsTooltip(client)"
|
||||
matTooltipPosition="above"
|
||||
|
@ -110,17 +110,13 @@
|
|||
</div>
|
||||
</mat-option>
|
||||
</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>
|
||||
|
||||
<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">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<div class="message-text">
|
||||
|
@ -207,6 +203,16 @@
|
|||
</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">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -256,18 +262,21 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<div class="chart-container" *ngIf="selectedDisk">
|
||||
<div class="chart-container" *ngIf="selectedDisk" #chartContainer>
|
||||
<div class="chart-header">
|
||||
<h3>Distribución de Particiones</h3>
|
||||
</div>
|
||||
<ngx-charts-pie-chart
|
||||
[results]="selectedDisk.chartData"
|
||||
[doughnut]="true"
|
||||
[gradient]="true"
|
||||
[labels]="true"
|
||||
[tooltipDisabled]="false"
|
||||
[animations]="true">
|
||||
</ngx-charts-pie-chart>
|
||||
<div class="chart-wrapper">
|
||||
<ngx-charts-pie-chart
|
||||
[results]="selectedDisk.chartData"
|
||||
[doughnut]="true"
|
||||
[gradient]="true"
|
||||
[labels]="true"
|
||||
[tooltipDisabled]="false"
|
||||
[animations]="true"
|
||||
[view]="view">
|
||||
</ngx-charts-pie-chart>
|
||||
</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 { ToastrService } from 'ngx-toastr';
|
||||
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 {MatDialog} from "@angular/material/dialog";
|
||||
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 {
|
||||
uuid?: string;
|
||||
|
@ -28,7 +31,7 @@ interface Partition {
|
|||
templateUrl: './partition-assistant.component.html',
|
||||
styleUrls: ['./partition-assistant.component.css']
|
||||
})
|
||||
export class PartitionAssistantComponent implements OnInit{
|
||||
export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDestroy{
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
|
@ -47,6 +50,10 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
runScriptContext: any = null;
|
||||
showInstructions = false;
|
||||
|
||||
@ViewChild('chartContainer', { static: false }) chartContainer!: ElementRef;
|
||||
private destroy$ = new Subject<void>();
|
||||
private resizeSubject = new Subject<void>();
|
||||
|
||||
view: [number, number] = [300, 200];
|
||||
showLegend = true;
|
||||
showLabels = true;
|
||||
|
@ -55,6 +62,23 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
selectedModelClient: any = null;
|
||||
partitionCode: 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(
|
||||
private http: HttpClient,
|
||||
|
@ -77,10 +101,22 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
||||
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;
|
||||
|
||||
if (!this.selectedModelClient && this.clientData.length > 0) {
|
||||
this.selectedModelClient = this.clientData[0];
|
||||
this.clientData[0].selected = true;
|
||||
}
|
||||
|
||||
if (this.selectedModelClient) {
|
||||
this.loadPartitions(this.selectedModelClient);
|
||||
}
|
||||
|
@ -90,6 +126,55 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
this.route.queryParams.subscribe(params => {
|
||||
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 {
|
||||
|
@ -135,7 +220,6 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
initializeDisks() {
|
||||
this.disks = [];
|
||||
|
||||
// Verificar que hay datos válidos
|
||||
if (!this.data || !this.data.partitions || !Array.isArray(this.data.partitions)) {
|
||||
console.warn('No hay datos de particiones válidos');
|
||||
return;
|
||||
|
@ -161,10 +245,10 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
size: this.convertBytesToGB(partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 512 : partition.size),
|
||||
memoryUsage: partition.memoryUsage,
|
||||
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,
|
||||
format: false,
|
||||
color: '#1f1b91',
|
||||
color: this.getColorForPartition(partition.partitionNumber),
|
||||
percentage: 0,
|
||||
removed: false
|
||||
});
|
||||
|
@ -234,7 +318,14 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -267,12 +358,18 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
memoryUsage: 0,
|
||||
sizeBytes: 0,
|
||||
format: false,
|
||||
color: '#' + Math.floor(Math.random() * 16777215).toString(16),
|
||||
color: this.getNextPartitionColor(),
|
||||
percentage: 0,
|
||||
removed: false
|
||||
});
|
||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||
this.updateDiskChart(disk);
|
||||
|
||||
this.validationSubject.next();
|
||||
|
||||
setTimeout(() => {
|
||||
this.resizeChart();
|
||||
}, 100);
|
||||
} else {
|
||||
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;
|
||||
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
|
||||
this.updateDiskChart(disk);
|
||||
|
||||
this.validationSubject.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,6 +410,10 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
|
||||
|
||||
save() {
|
||||
if (this.selectedClients.length === 0 && this.clientData.length > 0) {
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
if (!this.selectedDisk) {
|
||||
this.toastService.error('No se ha seleccionado un disco.');
|
||||
return;
|
||||
|
@ -389,6 +492,12 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
|
||||
this.updateDiskChart(disk);
|
||||
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.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 {
|
||||
return partitions
|
||||
.filter(partition => !partition.removed)
|
||||
|
@ -429,19 +584,36 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
}
|
||||
|
||||
updateDiskChart(disk: any) {
|
||||
console.log('disk', disk);
|
||||
disk.chartData = this.generateChartData(disk.partitions);
|
||||
disk.used = this.calculateUsedSpace(disk.partitions);
|
||||
disk.percentage = (disk.used / disk.totalDiskSize) * 100;
|
||||
|
||||
setTimeout(() => {
|
||||
this.resizeChart();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
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, {
|
||||
width: '800px',
|
||||
data: {
|
||||
scope: this.runScriptContext.type,
|
||||
organizationalUnit: this.runScriptContext['@id'],
|
||||
source: 'assistant'
|
||||
scope: scope,
|
||||
selectedClients: selectedClients,
|
||||
organizationalUnit: this.runScriptContext?.['@id'],
|
||||
source: 'assistant',
|
||||
runScriptContext: this.runScriptContext
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -502,11 +674,16 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
onDiskSelectionChange() {
|
||||
if (this.selectedDiskNumber) {
|
||||
this.scrollToPartitionTable();
|
||||
|
||||
this.validationSubject.next();
|
||||
|
||||
setTimeout(() => {
|
||||
this.resizeChart();
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
|
||||
scrollToPartitionTable() {
|
||||
// Pequeño delay para asegurar que el contenido se haya renderizado
|
||||
setTimeout(() => {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
</div>
|
||||
<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 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
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -29,7 +29,7 @@ export class RunScriptAssistantComponent implements OnInit{
|
|||
parameters: any = {};
|
||||
selectedScript: any = null;
|
||||
selectedClients: any[] = [];
|
||||
allSelected: boolean = true;
|
||||
allSelected: boolean = false;
|
||||
commandType: string = 'existing';
|
||||
newScript: string = '';
|
||||
selection = new SelectionModel(true, []);
|
||||
|
@ -56,7 +56,11 @@ export class RunScriptAssistantComponent implements OnInit{
|
|||
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
|
||||
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()
|
||||
}
|
||||
|
@ -117,12 +121,15 @@ export class RunScriptAssistantComponent implements OnInit{
|
|||
}
|
||||
|
||||
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() {
|
||||
this.allSelected = !this.allSelected;
|
||||
this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = this.allSelected; });
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
getPartitionsTooltip(client: any): string {
|
||||
|
@ -198,21 +205,33 @@ export class RunScriptAssistantComponent implements OnInit{
|
|||
}
|
||||
|
||||
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, {
|
||||
width: '800px',
|
||||
data: {
|
||||
scope: this.runScriptContext.type,
|
||||
organizationalUnit: this.runScriptContext['@id'],
|
||||
source: 'assistant'
|
||||
scope: scope,
|
||||
selectedClients: selectedClients,
|
||||
organizationalUnit: this.runScriptContext?.['@id'],
|
||||
source: 'assistant',
|
||||
runScriptContext: this.runScriptContext
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
console.log(result);
|
||||
if (result) {
|
||||
this.http.post(`${this.baseUrl}/command-task-scripts`, {
|
||||
commandTask: result['@id'],
|
||||
commandTask: result.taskId['@id'],
|
||||
content: this.commandType === 'existing' ? this.scriptContent : this.newScript,
|
||||
order: 1,
|
||||
order: result.executionOrder,
|
||||
type: 'run-script',
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
|
|
|
@ -422,6 +422,10 @@
|
|||
<mat-icon>article</mat-icon>
|
||||
<span>Logs en tiempo real</span>
|
||||
</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'">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
|
@ -613,6 +617,10 @@
|
|||
<mat-icon>article</mat-icon>
|
||||
<span>Logs en tiempo real</span>
|
||||
</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'">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<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 { AuthService } from '@services/auth.service';
|
||||
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 {
|
||||
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 {
|
||||
event.stopPropagation();
|
||||
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">
|
||||
<mat-icon>info</mat-icon>
|
||||
</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>
|
||||
</td>
|
||||
</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 {DatePipe} from "@angular/common";
|
||||
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 {ConfigService} from "@services/config.service";
|
||||
import {Router} from "@angular/router";
|
||||
import {Observable} from "rxjs";
|
||||
import {ServerInfoDialogComponent} from "../../ogdhcp/server-info-dialog/server-info-dialog.component";
|
||||
import {ImportImageComponent} from "../import-image/import-image.component";
|
||||
import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import {ExportImageComponent} from "../../images/export-image/export-image.component";
|
||||
import {BackupImageComponent} from "../backup-image/backup-image.component";
|
||||
import {EditImageComponent} from "../edit-image/edit-image.component";
|
||||
import {CreateTagModalComponent} from "./create-tag-modal/create-tag-modal.component";
|
||||
import {CreateBranchModalComponent} from "./create-branch-modal/create-branch-modal.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-git-commits',
|
||||
|
@ -212,6 +208,12 @@ export class ShowGitCommitsComponent implements OnInit{
|
|||
this.toastService.success('Commit ID copiado al portapapeles');
|
||||
});
|
||||
break;
|
||||
case 'create-tag':
|
||||
this.openCreateTagDialog(commit);
|
||||
break;
|
||||
case 'create-branch':
|
||||
this.openCreateBranchDialog(commit);
|
||||
break;
|
||||
default:
|
||||
console.error('Acción no soportada:', action);
|
||||
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) {
|
||||
window.open(`http://localhost:3100/oggit/${this.selectedRepository}/commit/${commit.hexsha}`, '_blank');
|
||||
}
|
||||
|
|
|
@ -42,8 +42,8 @@ export class HeaderComponent implements OnInit {
|
|||
|
||||
showGlobalStatus() {
|
||||
this.dialog.open(GlobalStatusComponent, {
|
||||
width: '65vw',
|
||||
height: '80vh',
|
||||
width: '80vw',
|
||||
height: '85vh',
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ export class QueueConfirmationModalComponent {
|
|||
) {}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close(false);
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onYesClick(): void {
|
||||
|
|
Loading…
Reference in New Issue