diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index db56824..5318784 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -1,4 +1,20 @@ -import { NgModule, CUSTOM_ELEMENTS_SCHEMA, LOCALE_ID, APP_INITIALIZER } from '@angular/core'; +import { NgModule, CUSTOM_ELEMENTS_SCHEMA, LOCALE_ID, APP_INITIALIZER, Injectable } from '@angular/core'; +import { MatPaginatorIntl } from '@angular/material/paginator'; + +@Injectable() +class SpanishPaginatorIntl extends MatPaginatorIntl { + override itemsPerPageLabel = 'Elementos por página:'; + override nextPageLabel = 'Siguiente'; + override previousPageLabel = 'Anterior'; + override firstPageLabel = 'Primera página'; + override lastPageLabel = 'Última página'; + override getRangeLabel = (page: number, pageSize: number, length: number) => { + if (length === 0 || pageSize === 0) return `0 de ${length}`; + const startIndex = page * pageSize; + const endIndex = Math.min(startIndex + pageSize, length); + return `${startIndex + 1} – ${endIndex} de ${length}`; + }; +} import { ConfigService } from './services/config.service'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; @@ -106,6 +122,7 @@ import { RepositoriesComponent } from './components/repositories/repositories.co import { ManageRepositoryComponent } from './components/repositories/manage-repository/manage-repository.component'; import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component'; import { DeployImageComponent } from './components/groups/components/client-main-view/deploy-image/deploy-image.component'; +import { DeployMethodConfigDialogComponent } from './components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component'; import { MainRepositoryViewComponent } from './components/repositories/main-repository-view/main-repository-view.component'; import { ExecuteCommandOuComponent } from './components/groups/shared/execute-command-ou/execute-command-ou.component'; import { JoyrideModule } from 'ngx-joyride'; @@ -250,6 +267,7 @@ registerLocaleData(localeEs, 'es-ES'); ExecuteCommandComponent, ExecuteCommandOuComponent, DeployImageComponent, + DeployMethodConfigDialogComponent, MainRepositoryViewComponent, EnvVarsComponent, MenusComponent, @@ -361,6 +379,7 @@ registerLocaleData(localeEs, 'es-ES'); multi: true }, { provide: LOCALE_ID, useValue: 'es-ES' }, + { provide: MatPaginatorIntl, useClass: SpanishPaginatorIntl }, provideAnimationsAsync(), provideHttpClient(withInterceptorsFromDi()), ConfigService, diff --git a/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.html b/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.html index 6139635..f441c10 100644 --- a/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.html +++ b/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.html @@ -38,6 +38,12 @@ + + + Clientes por página + + Número de clientes mostrados por página en el listado + diff --git a/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.ts b/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.ts index add5556..5642ee8 100644 --- a/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.ts +++ b/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.ts @@ -48,6 +48,7 @@ export class AddUserModalComponent implements OnInit { password: ['', Validators.required], role: ['', Validators.required], groupsView: ['card', Validators.required], + clientsPerPage: [null, [Validators.min(1)]], organizationalUnits: [[]] }); @@ -82,7 +83,8 @@ export class AddUserModalComponent implements OnInit { username: response.username, role: response.userGroups.length > 0 ? response.userGroups[0]['@id'] : null, organizationalUnits: organizationalUnitIds, - groupsView: response.groupsView + groupsView: response.groupsView, + clientsPerPage: response.clientsPerPage ?? null }); this.userId = response['@id']; @@ -108,7 +110,8 @@ export class AddUserModalComponent implements OnInit { allowedOrganizationalUnits: this.userForm.value.organizationalUnits, enabled: true, userGroups: [this.userForm.value.role], - groupsView: this.userForm.value.groupsView + groupsView: this.userForm.value.groupsView, + clientsPerPage: this.userForm.value.clientsPerPage || null }; if (!this.userId && this.userForm.value.password) { diff --git a/ogWebconsole/src/app/components/admin/users/users/users.component.ts b/ogWebconsole/src/app/components/admin/users/users/users.component.ts index 73abe86..a7d3ce5 100644 --- a/ogWebconsole/src/app/components/admin/users/users/users.component.ts +++ b/ogWebconsole/src/app/components/admin/users/users/users.component.ts @@ -38,6 +38,11 @@ export class UsersComponent implements OnInit { header: 'Vista de Grupos', cell: (user: any) => `${user.groupsView}` }, + { + columnDef: 'clientsPerPage', + header: 'Clientes por página', + cell: (user: any) => user.clientsPerPage != null ? `${user.clientsPerPage}` : '-' + }, { columnDef: 'allowedOrganizationalUnits', header: 'Unidades Organizacionales Permitidas', diff --git a/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.css b/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.css index b215f90..25e66c5 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.css +++ b/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.css @@ -78,7 +78,7 @@ table { } /* Estilos para los botones de gestión */ -.schedule-btn, .script-btn { +.schedule-btn, .script-btn, .traces-btn { display: inline-flex; align-items: center; justify-content: center; @@ -114,7 +114,17 @@ table { box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); } -.schedule-btn mat-icon, .script-btn mat-icon { +.traces-btn { + background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%); + color: white; +} + +.traces-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3); +} + +.schedule-btn mat-icon, .script-btn mat-icon, .traces-btn mat-icon { font-size: 18px; width: 18px; height: 18px; @@ -123,15 +133,58 @@ table { justify-content: center; } -.schedule-btn span, .script-btn span { +.schedule-btn span, .script-btn span, .traces-btn span { display: inline-block; vertical-align: middle; line-height: 1.2; } +/* Indicadores de estado de trazas */ +.trace-stats { + display: flex; + gap: 4px; + align-items: center; +} + +.trace-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 24px; + height: 24px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + color: white; + padding: 0 6px; + cursor: default; +} + +.trace-success { + background-color: #4caf50; +} + +.trace-in-progress { + background-color: #2196f3; +} + +.trace-pending { + background-color: #ff9800; +} + +.trace-failed { + background-color: #f44336; +} + +.trace-no-data { + color: #999; + font-style: italic; + font-size: 13px; +} + /* Responsive para los botones */ @media (max-width: 768px) { - .schedule-btn, .script-btn { + .schedule-btn, .script-btn, .traces-btn { width: 100%; justify-content: center; margin: 4px 0; diff --git a/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.html b/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.html index 993c225..54f469d 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.html +++ b/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.html @@ -46,11 +46,11 @@ diff --git a/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.ts b/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.ts index 2d9f531..e445c50 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.ts +++ b/ogWebconsole/src/app/components/commands/commands-task/commands-task.component.ts @@ -71,6 +71,7 @@ export class CommandsTaskComponent implements OnInit { ); } + updateDisplayedColumns(): void { const hasNonClientTasks = this.tasks.some(task => task.scope !== 'clients'); @@ -127,7 +128,7 @@ export class CommandsTaskComponent implements OnInit { deleteTask(task: any): void { this.dialog.open(DeleteModalComponent, { width: '300px', - data: { name: task.createdBy }, + data: { name: task.name }, }).afterClosed().subscribe((result) => { if (result) { this.http.delete(`${this.apiUrl}/${task.uuid}`).subscribe({ diff --git a/ogWebconsole/src/app/components/commands/commands-task/create-task-schedule/create-task-schedule.component.html b/ogWebconsole/src/app/components/commands/commands-task/create-task-schedule/create-task-schedule.component.html index ac8f63f..fb61d6d 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/create-task-schedule/create-task-schedule.component.html +++ b/ogWebconsole/src/app/components/commands/commands-task/create-task-schedule/create-task-schedule.component.html @@ -1,32 +1,32 @@

- {{ data.schedule ? 'Editar' : 'Crear' }} Programación de Tarea + {{ (data.schedule ? 'editSchedule' : 'createSchedule') | translate }} {{ 'taskSchedule' | translate }}

- Repetición + {{ 'repetition' | translate }} {{ opt.label }} - Fecha de ejecución + {{ 'executionDate' | translate }} - Hora de ejecución + {{ 'executionTime' | translate }} - Hora en formato 24h (ej: 14:30) + {{ 'timeFormatHint' | translate }}
- +
next_plan - Próxima ejecución: {{ nextExecutionDate | date:'full' }} + {{ 'nextExecution' | translate }}: {{ nextExecutionDate | date:'full' }}
repeat - Se ejecutará {{ executionCount }} veces + {{ 'willExecuteTimes' | translate:{ count: executionCount } }}
diff --git a/ogWebconsole/src/app/components/commands/commands-task/create-task-schedule/create-task-schedule.component.ts b/ogWebconsole/src/app/components/commands/commands-task/create-task-schedule/create-task-schedule.component.ts index 69e238b..0104241 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/create-task-schedule/create-task-schedule.component.ts +++ b/ogWebconsole/src/app/components/commands/commands-task/create-task-schedule/create-task-schedule.component.ts @@ -168,6 +168,34 @@ export class CreateTaskScheduleComponent implements OnInit{ onSubmit() { const formData = this.form.value; + if (formData.recurrenceType === 'custom') { + const selectedDayNames = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]); + const selectedMonthNames = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]); + + if (selectedDayNames.length === 0) { + this.toastr.error('Debe seleccionar al menos un día de la semana'); + return; + } + + if (selectedMonthNames.length === 0) { + this.toastr.error('Debe seleccionar al menos un mes'); + return; + } + + const initDate = formData.recurrenceDetails?.initDate; + const endDate = formData.recurrenceDetails?.endDate; + + if (!initDate || !endDate) { + this.toastr.error('Debe especificar fecha de inicio y fecha de fin'); + return; + } + + if (new Date(endDate) <= new Date(initDate)) { + this.toastr.error('La fecha de fin debe ser posterior a la fecha de inicio'); + return; + } + } + const payload: any = { commandTask: this.data.task['@id'], executionDate: formData.recurrenceType === 'none' ? this.convertDateToLocalISO(formData.executionDate) : null, diff --git a/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.css b/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.css index 3fc5f7d..8ab6216 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.css +++ b/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.css @@ -1,31 +1,312 @@ - -.divider { - margin: 20px 0; +.dialog-title { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + padding: 16px 24px; } -table { +.back-icon { + cursor: pointer; + color: #666; + transition: color 0.2s; +} + +.back-icon:hover { + color: #1976d2; +} + +.title-with-action { + display: flex; + align-items: center; + gap: 8px; +} + +/* ===== Category cards ===== */ +.categories-grid { + display: flex; + flex-direction: column; + gap: 12px; + padding: 8px 0; +} + +.category-card { + display: flex; + align-items: center; + gap: 16px; + padding: 18px 20px; + border-radius: 10px; + border: 1px solid #e0e0e0; + border-left: 5px solid; + cursor: pointer; + transition: all 0.2s ease; + background: #fff; +} + +.category-card:hover { + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.1); + transform: translateX(4px); +} + +.category-icon { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 12px; + flex-shrink: 0; +} + +.category-icon mat-icon { + color: #fff; + font-size: 26px; + width: 26px; + height: 26px; +} + +.category-info { + flex: 1; +} + +.category-info h3 { + margin: 0 0 4px 0; + font-size: 15px; + font-weight: 600; + color: #333; +} + +.category-info p { + margin: 0; + font-size: 13px; + color: #777; +} + +.category-arrow { + color: #bbb; + transition: transform 0.2s; +} + +.category-card:hover .category-arrow { + transform: translateX(4px); + color: #666; +} + +.info-icon { + color: #90a4ae; +} + +/* ===== Info panel (for assistant-required categories) ===== */ +.info-panel { + display: flex; + flex-direction: column; + align-items: center; + padding: 24px 16px; + text-align: center; +} + +.info-panel-icon .big-info-icon { + font-size: 56px; + width: 56px; + height: 56px; + margin-bottom: 16px; + opacity: 0.8; +} + +.info-panel-content { + max-width: 520px; +} + +.info-panel-content h3 { + font-size: 18px; + font-weight: 600; + color: #333; + margin: 0 0 12px 0; +} + +.info-panel-content > p { + font-size: 14px; + color: #666; + line-height: 1.6; + margin: 0 0 24px 0; +} + +.info-steps { + display: flex; + flex-direction: column; + gap: 14px; + text-align: left; + margin-bottom: 28px; + padding: 20px; + background: #f8f9fa; + border-radius: 10px; +} + +.info-step { + display: flex; + align-items: center; + gap: 14px; + font-size: 14px; + color: #444; +} + +.step-number-badge { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: #1976d2; + color: #fff; + font-weight: 600; + font-size: 13px; + flex-shrink: 0; +} + +.info-supported-ops { + text-align: left; +} + +.info-supported-label { + font-size: 13px; + font-weight: 600; + color: #555; + margin: 0 0 10px 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.info-op-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + margin: 4px 6px 4px 0; + background: #f0f4f8; + border-radius: 20px; + font-size: 13px; + color: #444; +} + +.info-op-chip mat-icon { + font-size: 18px; + width: 18px; + height: 18px; +} + +/* ===== Action selector within category ===== */ +.actions-grid { + display: flex; + flex-direction: column; + gap: 12px; + padding: 8px 0; +} + +.actions-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.action-card { + display: flex; + align-items: center; + gap: 14px; + padding: 16px 20px; + border-radius: 8px; + border: 1px solid #e0e0e0; + border-left: 4px solid; + cursor: pointer; + transition: all 0.2s ease; + background: #fff; + font-size: 15px; + font-weight: 500; + color: #333; +} + +.action-card:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transform: translateX(4px); +} + +.action-card mat-icon:first-child { + font-size: 24px; + width: 24px; + height: 24px; +} + +.action-card span { + flex: 1; +} + +/* ===== Form sections ===== */ +.action-form { + padding: 8px 0; +} + +.order-field { + width: 200px; + margin-bottom: 16px; +} + +.form-section { + margin-top: 8px; +} + +.full-width { width: 100%; - margin-top: 50px; } -.task-form { - padding: 20px; -} - -.search-container { +.radio-row { display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - box-sizing: border-box; + gap: 24px; + margin-bottom: 20px; + padding: 12px 16px; + background-color: #f8f9fa; + border-radius: 8px; } -.deploy-container { +/* ===== System action info ===== */ +.system-action-info { display: flex; - justify-content: space-between; + flex-direction: column; align-items: center; - padding: 20px; + padding: 32px 16px; + text-align: center; +} + +.system-action-info .big-icon { + font-size: 48px; + width: 48px; + height: 48px; + margin-bottom: 16px; +} + +.system-action-info p { + color: #666; + font-size: 14px; + max-width: 400px; +} + +/* ===== Script section ===== */ +.script-type-selector { + margin-bottom: 16px; +} + +.new-command-container { + display: flex; + flex-direction: column; gap: 10px; + padding: 15px; + background-color: #eaeff6; + border: 1px solid #ddd; + border-radius: 8px; +} + +.new-command-container textarea { + font-family: monospace; + resize: vertical; } .script-container { @@ -33,7 +314,7 @@ table { padding: 20px; background-color: #eaeff6; border-radius: 12px; - margin-top: 20px; + margin-top: 12px; } .script-content { @@ -46,35 +327,6 @@ table { min-width: 35%; } -@media (max-width: 768px) { - .script-container { - flex-direction: column; - } - - .script-content, .script-params { - min-width: 100%; - } -} - -.select-container { - margin-top: 20px; - align-items: center; - padding: 20px; - box-sizing: border-box; -} - -.input-group { - display: flex; - flex-wrap: wrap; - gap: 16px; - margin-top: 20px; -} - -.input-field { - flex: 1 1 calc(33.33% - 16px); - min-width: 250px; -} - .script-preview { background-color: #f4f4f4; border: 1px solid #ccc; @@ -85,145 +337,7 @@ table { min-height: 50px; } -.custom-width { - width: 50%; - margin-bottom: 16px; -} - -.search-string { - flex: 2; - padding: 5px; -} - -.search-boolean { - flex: 1; - padding: 5px; -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 10px; - border-bottom: 1px solid #ddd; -} - -.mat-elevation-z8 { - box-shadow: 0px 0px 0px rgba(0,0,0,0.2); -} - -.paginator-container { - display: flex; - justify-content: end; - margin-bottom: 30px; -} - -.clients-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 8px; -} - -.client-item { - position: relative; -} - -.client-card { - background: #ffffff; - border-radius: 6px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - overflow: hidden; - position: relative; - padding: 8px; - text-align: center; - cursor: pointer; - transition: background-color 0.3s, transform 0.2s; - - &:hover { - background-color: #f0f0f0; - transform: scale(1.02); - } -} - -.client-details { - margin-top: 4px; -} - -.client-name { - font-size: 0.9em; - font-weight: 600; - color: #333; - margin-bottom: 5px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 150px; - display: inline-block; -} - -.client-ip { - display: block; - font-size: 0.9em; - color: #666; -} - -.header-container-title { - flex-grow: 1; - text-align: left; - padding-left: 1em; -} - -.button-row { - display: flex; - padding-right: 1em; -} - -.client-card { - background: #ffffff; - border-radius: 6px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - overflow: hidden; - position: relative; - padding: 8px; - text-align: center; - cursor: pointer; - transition: background-color 0.3s, transform 0.2s; - - &:hover { - background-color: #f0f0f0; - transform: scale(1.02); - } -} - -::ng-deep .custom-tooltip { - white-space: pre-line !important; - max-width: 200px; - background: rgba(0, 0, 0, 0.8); - color: white; - padding: 8px; - border-radius: 4px; -} - -.selected-client { - background-color: #a0c2e5 !important; - color: white !important; -} - -.button-row { - display: flex; - padding-right: 1em; -} - -.disabled-client { - pointer-events: none; - opacity: 0.5; -} - -.action-button { - margin-top: 10px; - margin-bottom: 10px; -} - +/* ===== Action buttons ===== */ .action-container { display: flex; justify-content: flex-end; @@ -231,178 +345,14 @@ table { padding: 1.5em; } -.mat-expansion-panel-header-description { - justify-content: space-between; - align-items: center; -} - -.new-command-container { - display: flex; - flex-direction: column; - gap: 10px; - padding: 15px; - background-color: #eaeff6; - border: 1px solid #ddd; - border-radius: 8px; - box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1); - margin-top: 15px; -} - -.new-command-container mat-form-field { - width: 100%; -} - -.new-command-container textarea { - font-family: monospace; - resize: vertical; -} - -.new-command-container .action-button { - align-self: flex-end; - color: white; - padding: 8px 16px; - border: none; - border-radius: 4px; - cursor: pointer; -} - -.full-width { - width: 100%; -} - -.script-selector-card { - margin: 20px 20px; - padding: 16px; -} - -/* Estilos para las pestañas principales */ -.action-tabs { - margin-top: 20px; -} - -.action-tabs ::ng-deep .mat-tab-header { - border-bottom: 2px solid #e0e0e0; - margin-bottom: 20px; -} - -.action-tabs ::ng-deep .mat-tab-label { - min-width: 160px; - padding: 12px 24px; - font-weight: 500; - color: #666; - transition: all 0.3s ease; -} - -.action-tabs ::ng-deep .mat-tab-label:hover { - color: #1976d2; - background-color: rgba(25, 118, 210, 0.04); -} - -.action-tabs ::ng-deep .mat-tab-label.mat-tab-label-active { - color: #1976d2; - font-weight: 600; -} - -.action-tabs ::ng-deep .mat-ink-bar { - background-color: #1976d2; - height: 3px; -} - -/* Contenido de las pestañas */ -.tab-content { - padding: 20px 0; -} - -.action-description { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 24px; - padding: 16px; - background-color: #f8f9fa; - border-radius: 8px; - border-left: 4px solid #1976d2; -} - -.action-description mat-icon { - color: #1976d2; - font-size: 24px; - width: 24px; - height: 24px; -} - -.action-description span { - color: #495057; - font-size: 14px; - font-weight: 500; -} - -/* Selector de tipo de script */ -.script-type-selector { - margin-bottom: 24px; - padding: 20px; - background-color: #f8f9fa; - border-radius: 8px; - border: 1px solid #e9ecef; -} - -.radio-group { - display: flex; - gap: 24px; -} - -.radio-button { - display: flex; - align-items: center; - gap: 8px; - padding: 12px 16px; - border-radius: 6px; - transition: all 0.2s ease; -} - -.radio-button:hover { - background-color: rgba(25, 118, 210, 0.04); -} - -.radio-button mat-icon { - color: #1976d2; - font-size: 20px; - width: 20px; - height: 20px; -} - -/* Contenedor de acciones básicas */ -.basic-action-container { - display: flex; - flex-direction: column; - gap: 16px; -} - -/* Contenedor de scripts */ -.script-options { - display: flex; - flex-direction: column; - gap: 16px; -} - -/* Responsive design */ +/* ===== Responsive ===== */ @media (max-width: 768px) { - .action-tabs ::ng-deep .mat-tab-label { - min-width: 120px; - padding: 8px 16px; - font-size: 14px; - } - - .radio-group { + .radio-row { flex-direction: column; - gap: 16px; + gap: 12px; } - - .action-description { - flex-direction: column; - text-align: center; - gap: 8px; + + .info-steps { + padding: 14px; } } - - diff --git a/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.html b/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.html index 1715658..5b4a1c7 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.html +++ b/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.html @@ -1,118 +1,169 @@ -

Añadir acción a: {{ data.task?.name }}

+

+ arrow_back + {{ 'addActionTo' | translate }}: {{ data.commandTask?.name }} + + {{ categories | keyvalue : undefined }} + + + {{ cat.icon }} + {{ cat.labelKey | translate }} + + + + + {{ getSelectedActionIcon() }} + {{ getSelectedActionLabel() }} + +

-
- - - - - - -
-
- power_settings_new - Selecciona una acción básica del sistema -
- - - Tipo de Acción - - - power Encender Equipo - - - power_off Apagar Equipo - - - restart_alt Reiniciar Equipo - - - login Iniciar Sesión - - - - - Orden de ejecución - - -
-
- - - -
-
- code - Ejecuta un script personalizado o crea uno nuevo -
- - -
-
- - - edit Nuevo Script - - - storage Script Guardado - - -
- - -
- - Orden de ejecución - - - - Ingrese el script - - - -
- - -
- - Seleccione script a ejecutar - - {{ script.name }} - - -
- - -
- - Orden de ejecución - - - -
-

Script:

-
-
- -
-

Ingrese los parámetros:

-
- - {{ paramName }} - - -
-
-
-
-
-
-
+ +
+
+
+ {{ category.icon }} +
+
+

{{ category.labelKey | translate }}

+

{{ category.descriptionKey | translate }}

+
+ chevron_right + info_outline +
+ + +
+
+ + {{ cat.icon }} + +
+ +
+

{{ 'assistantRequiredTitle' | translate }}

+

{{ 'assistantRequiredDesc' | translate }}

+ +
+
+
1
+ {{ 'assistantStep1' | translate }} +
+
+
2
+ {{ 'assistantStep2' | translate }} +
+
+
3
+ {{ 'assistantStep3' | translate }} +
+
+ +
+

{{ 'supportedOpsLabel' | translate }}:

+ + +
+ {{ action.icon }} + {{ action.labelKey | translate }} +
+
+
+
+
+
+ + +
+
+ + +
+ {{ action.icon }} + {{ action.labelKey | translate }} + chevron_right +
+
+
+
+
+ + +
+ + + + {{ 'executionOrder' | translate }} + + + + +
+
+ {{ getSelectedActionIcon() }} +

{{ 'systemActionHint' | translate }}

+
+
+ + +
+
+ + + edit {{ 'newScript' | translate }} + + + storage {{ 'savedScript' | translate }} + + +
+ +
+ + {{ 'enterScript' | translate }} + + + +
+ +
+ + {{ 'selectScriptToRun' | translate }} + + {{ script.name }} + + +
+ +
+
+

{{ 'scriptLabel' | translate }}:

+
+
+
+

{{ 'enterParameters' | translate }}:

+
+ + {{ paramName }} + + +
+
+
+
+
+ - + diff --git a/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.spec.ts b/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.spec.ts index 7c6c1de..a35af98 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.spec.ts +++ b/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.spec.ts @@ -31,6 +31,7 @@ import {ConfigService} from "@services/config.service"; import {ActivatedRoute} from "@angular/router"; import {MatButtonToggleModule} from "@angular/material/button-toggle"; import {MatRadioModule} from "@angular/material/radio"; +import {MatCheckboxModule} from "@angular/material/checkbox"; describe('CreateTaskScriptComponent', () => { let component: CreateTaskScriptComponent; @@ -68,6 +69,7 @@ describe('CreateTaskScriptComponent', () => { MatMenuModule, MatButtonToggleModule, MatRadioModule, + MatCheckboxModule, MatTreeModule, TranslateModule.forRoot(), JoyrideModule.forRoot(), diff --git a/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.ts b/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.ts index d96ec4c..3f99b43 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.ts +++ b/ogWebconsole/src/app/components/commands/commands-task/create-task-script/create-task-script.component.ts @@ -9,6 +9,23 @@ import { SaveScriptComponent } from "../../../groups/components/client-main-view/run-script-assistant/save-script/save-script.component"; import {FormBuilder, FormGroup} from "@angular/forms"; +import {TranslateService} from "@ngx-translate/core"; + +interface ActionCategory { + id: string; + icon: string; + labelKey: string; + descriptionKey: string; + color: string; + infoOnly: boolean; + actions: ActionOption[]; +} + +interface ActionOption { + type: string; + icon: string; + labelKey: string; +} @Component({ selector: 'app-create-task-script', @@ -19,20 +36,72 @@ export class CreateTaskScriptComponent implements OnInit { form: FormGroup; baseUrl: string; @Output() dataChange = new EventEmitter(); - errorMessage = ''; loading: boolean = false; scripts: any[] = []; scriptContent: string = ""; parameters: any = {}; - selectedTabIndex: number = 0; - selectedBasicAction: string = ''; commandType: string = 'existing'; selectedScript: any = null; newScript: string = ''; - executionOrder: Number = 0; + executionOrder: number = 0; selection = new SelectionModel(true, []); parameterNames: string[] = Object.keys(this.parameters); commandTask: any; + + selectedCategory: string = ''; + selectedAction: string = ''; + + categories: ActionCategory[] = [ + { + id: 'image-ops', + icon: 'perm_media', + labelKey: 'catImageOps', + descriptionKey: 'catImageOpsDesc', + color: '#1565c0', + infoOnly: true, + actions: [ + { type: 'deploy-image', icon: 'cloud_download', labelKey: 'deployImage' }, + { type: 'create-image', icon: 'add_photo_alternate', labelKey: 'createImage' }, + ] + }, + { + id: 'partition', + icon: 'storage', + labelKey: 'catPartition', + descriptionKey: 'catPartitionDesc', + color: '#2e7d32', + infoOnly: true, + actions: [ + { type: 'partition-assistant', icon: 'view_column', labelKey: 'partitionAndFormat' }, + ] + }, + { + id: 'system', + icon: 'settings_power', + labelKey: 'catSystemControl', + descriptionKey: 'catSystemControlDesc', + color: '#e65100', + infoOnly: false, + actions: [ + { type: 'power-on', icon: 'power', labelKey: 'powerOnDevice' }, + { type: 'shutdown', icon: 'power_off', labelKey: 'shutdownDevice' }, + { type: 'reboot', icon: 'restart_alt', labelKey: 'rebootDevice' }, + { type: 'login', icon: 'login', labelKey: 'loginSession' }, + ] + }, + { + id: 'script', + icon: 'code', + labelKey: 'catCustomScript', + descriptionKey: 'catCustomScriptDesc', + color: '#6a1b9a', + infoOnly: false, + actions: [ + { type: 'run-script', icon: 'terminal', labelKey: 'runScript' }, + ] + } + ]; + constructor( private fb: FormBuilder, private http: HttpClient, @@ -42,25 +111,56 @@ export class CreateTaskScriptComponent implements OnInit { private router: Router, private dialog: MatDialog, private route: ActivatedRoute, + private translate: TranslateService, @Inject(MAT_DIALOG_DATA) public data: any ) { this.baseUrl = this.configService.apiUrl; this.commandTask = this.data.commandTask; - - this.loadScripts() + + this.loadScripts(); this.form = this.fb.group({ content: [''], order: [''], - }) + }); } - ngOnInit(): void { + ngOnInit(): void {} + selectCategory(categoryId: string): void { + const cat = this.categories.find(c => c.id === categoryId); + if (cat?.infoOnly) { + this.selectedCategory = categoryId; + this.selectedAction = ''; + return; + } + this.selectedCategory = categoryId; + if (cat && cat.actions.length === 1) { + this.selectAction(cat.actions[0].type); + } else { + this.selectedAction = ''; + } + } + + selectAction(actionType: string): void { + this.selectedAction = actionType; + } + + goBackToCategories(): void { + this.selectedCategory = ''; + this.selectedAction = ''; + } + + goBackToActions(): void { + this.selectedAction = ''; + } + + isInfoOnlyCategory(): boolean { + const cat = this.categories.find(c => c.id === this.selectedCategory); + return cat?.infoOnly || false; } loadScripts(): void { this.loading = true; - this.http.get(`${this.baseUrl}/commands?readOnly=false&enabled=true`).subscribe((data: any) => { this.scripts = data['hydra:member']; this.loading = false; @@ -70,75 +170,16 @@ export class CreateTaskScriptComponent implements OnInit { }); } - onTabChange(index: number): void { - this.selectedTabIndex = index; - if (index === 0) { - this.selectedBasicAction = ''; - } else { - this.selectedScript = null; - this.newScript = ''; - } - } - - onBasicActionChange(): void { - this.parameters = {}; - this.parameterNames = Object.keys(this.parameters); - } - - getBasicActionType(): string { - const actionMap: { [key: string]: string } = { - 'poweron': 'POWER-ON', - 'shutdown': 'SHUTDOWN', - 'reboot': 'REBOOT', - 'login': 'LOGIN', - }; - - return actionMap[this.selectedBasicAction] || ''; - } - - validateBasicAction(): boolean { - if (!this.selectedBasicAction) { - this.toastService.error('Debe seleccionar un tipo de acción'); - return false; - } - - if (this.executionOrder === null || this.executionOrder === undefined) { - this.toastService.error('Debe especificar el orden de ejecución'); - return false; - } - - return true; - } - - saveNewScript() { - if (!this.newScript.trim()) { - this.toastService.error('Debe ingresar un script antes de guardar.'); - return; - } - const dialogRef = this.dialog.open(SaveScriptComponent, { - width: '400px', - data: this.newScript - }); - - dialogRef.afterClosed().subscribe(result => { - if (result) { - this.toastService.success('Script guardado correctamente'); - } - }); - } - - onScriptChange() { + onScriptChange(): void { if (this.selectedScript) { this.scriptContent = this.selectedScript.script; const matches = this.scriptContent.match(/@(\w+)/g) || []; - const uniqueParams = Array.from(new Set(matches.map(m => m.slice(1)))); + const uniqueParams = Array.from(new Set(matches.map((m: string) => m.slice(1)))); this.parameters = {}; uniqueParams.forEach(param => this.parameters[param] = ''); - this.parameterNames = uniqueParams; - this.updateScript(); } } @@ -150,16 +191,82 @@ export class CreateTaskScriptComponent implements OnInit { updateScript(): void { let updatedScript = this.selectedScript.script; - for (const [key, value] of Object.entries(this.parameters)) { const regex = new RegExp(`@${key}\\b`, 'g'); updatedScript = updatedScript.replace(regex, value || `@${key}`); } - this.scriptContent = updatedScript; } - validateScript(): boolean { + saveNewScript(): void { + if (!this.newScript.trim()) { + this.toastService.error('Debe ingresar un script antes de guardar.'); + return; + } + const dialogRef = this.dialog.open(SaveScriptComponent, { + width: '400px', + data: this.newScript + }); + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.toastService.success('Script guardado correctamente'); + } + }); + } + + onSubmit(): void { + if (!this.selectedAction) { + this.toastService.error(this.translate.instant('selectActionTypeError')); + return; + } + + if (this.executionOrder === null || this.executionOrder === undefined) { + this.toastService.error(this.translate.instant('executionOrderRequired')); + return; + } + + let type = ''; + let content = ''; + + switch (this.selectedAction) { + case 'run-script': + if (!this.validateScript()) return; + type = 'run-script'; + content = this.commandType === 'existing' ? this.scriptContent : this.newScript; + break; + + case 'power-on': + case 'shutdown': + case 'reboot': + case 'login': + type = this.selectedAction; + content = this.selectedAction; + break; + + default: + this.toastService.error('Tipo de acción no reconocido'); + return; + } + + const payload: any = { + commandTask: this.commandTask['@id'], + content: content, + order: this.executionOrder, + type: type, + }; + + this.http.post(`${this.baseUrl}/command-task-scripts`, payload).subscribe({ + next: () => { + this.toastService.success(this.translate.instant('actionAddedSuccess')); + this.dialogRef.close(true); + }, + error: (error) => { + this.toastService.error(error.error['hydra:description']); + } + }); + } + + private validateScript(): boolean { if (this.commandType === 'new') { if (!this.newScript.trim()) { this.toastService.error('Debe ingresar un script'); @@ -171,60 +278,31 @@ export class CreateTaskScriptComponent implements OnInit { return false; } } - - if (this.executionOrder === null || this.executionOrder === undefined) { - this.toastService.error('Debe especificar el orden de ejecución'); - return false; - } - return true; } - onSubmit() { - let isValid = false; - let content = ''; - let type = ''; - - if (this.selectedTabIndex === 0) { - isValid = this.validateBasicAction(); - if (isValid) { - content = this.getBasicActionType(); - type = 'basic-action'; - } - } else { // Pestaña de scripts - isValid = this.validateScript(); - if (isValid) { - content = this.commandType === 'existing' ? this.scriptContent : this.newScript; - type = 'run-script'; - } - } - - if (!isValid) { - return; - } - - if (!this.data) { - this.toastService.error('Error: No se recibieron datos del diálogo'); - return; - } - - this.http.post(`${this.baseUrl}/command-task-scripts`, { - commandTask: this.commandTask['@id'], - content: content, - order: this.executionOrder, - type: this.selectedBasicAction, - }).subscribe({ - next: () => { - this.toastService.success('Tarea creada con éxito'); - this.dialogRef.close(true); - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - } - }) - } - onCancel(): void { this.dialogRef.close(false); } + + getSelectedCategoryColor(): string { + const cat = this.categories.find(c => c.id === this.selectedCategory); + return cat?.color || '#1976d2'; + } + + getSelectedActionLabel(): string { + for (const cat of this.categories) { + const action = cat.actions.find(a => a.type === this.selectedAction); + if (action) return this.translate.instant(action.labelKey); + } + return ''; + } + + getSelectedActionIcon(): string { + for (const cat of this.categories) { + const action = cat.actions.find(a => a.type === this.selectedAction); + if (action) return action.icon; + } + return ''; + } } diff --git a/ogWebconsole/src/app/components/commands/commands-task/create-task/create-task.component.html b/ogWebconsole/src/app/components/commands/commands-task/create-task/create-task.component.html index 1c50316..0e0600a 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/create-task/create-task.component.html +++ b/ogWebconsole/src/app/components/commands/commands-task/create-task/create-task.component.html @@ -9,32 +9,32 @@ add_task - Crear tarea + {{ 'createTaskLabel' | translate }} playlist_add - Introducir en tarea existente + {{ 'addToExistingTask' | translate }}
- Seleccione una tarea + {{ 'selectTask' | translate }} {{ task.name }} - Orden de ejecución + {{ 'executionOrder' | translate }}
@@ -42,30 +42,30 @@ {{ 'nameLabel' | translate }} - + {{ 'requiredFieldError' | translate }} {{ 'notesLabel' | translate }} - + - ¿Quieres programar la tarea al finalizar su creación? + {{ 'scheduleAfterCreate' | translate }} - Ámbito + {{ 'scopeLabel' | translate }} - Unidad Organizativa - Grupo de aulas - Aulas - Grupos de clientes - Clientes + {{ 'scopeOrganizationalUnit' | translate }} + {{ 'scopeClassroomsGroup' | translate }} + {{ 'scopeClassroom' | translate }} + {{ 'scopeClientsGroup' | translate }} + {{ 'scopeClients' | translate }} @@ -80,11 +80,11 @@
-

Clientes seleccionados

+

{{ 'selectedClients' | translate }}

info - Los clientes han sido pre-seleccionados desde el componente de despliegue de imágenes. + {{ 'preSelectedInfo' | translate }}
diff --git a/ogWebconsole/src/app/components/commands/commands-task/create-task/create-task.component.ts b/ogWebconsole/src/app/components/commands/commands-task/create-task/create-task.component.ts index 9a12fb0..d697a52 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/create-task/create-task.component.ts +++ b/ogWebconsole/src/app/components/commands/commands-task/create-task/create-task.component.ts @@ -7,6 +7,7 @@ import { ConfigService } from '@services/config.service'; import {of} from "rxjs"; import {startWith, switchMap} from "rxjs/operators"; import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component"; +import {ShowTaskScriptComponent} from "../show-task-script/show-task-script.component"; @Component({ selector: 'app-create-task', @@ -291,17 +292,12 @@ export class CreateTaskComponent implements OnInit { next: response => { this.toastr.success('Tarea creada con éxito'); this.dialogRef.close(response); - if (formData.scheduleAfterCreate) { - const dialogRef = this.dialog.open(CreateTaskScheduleComponent, { - width: '800px', - data: { task: response } - }); - - dialogRef.afterClosed().subscribe(result => { - if (result) { - this.toastr.success('Tarea programada correctamente'); - } - }); + if (this.data?.source === 'assistant') { + if (formData.scheduleAfterCreate) { + this.openScheduleDialog(response); + } + } else { + this.openPostCreationFlow(response, formData.scheduleAfterCreate); } }, error: () => { @@ -311,6 +307,40 @@ export class CreateTaskComponent implements OnInit { } } + private openScheduleDialog(task: any): void { + this.dialog.open(CreateTaskScheduleComponent, { + width: '800px', + data: { task: task } + }).afterClosed().subscribe(result => { + if (result) { + this.toastr.success('Tarea programada correctamente'); + } + }); + } + + private openPostCreationFlow(task: any, scheduleAfterCreate: boolean): void { + const actionsDialog = this.dialog.open(ShowTaskScriptComponent, { + width: '85vw', + height: '85vh', + maxWidth: '85vw', + maxHeight: '85vh', + data: { commandTask: task } + }); + + actionsDialog.afterClosed().subscribe(() => { + if (scheduleAfterCreate) { + this.dialog.open(CreateTaskScheduleComponent, { + width: '800px', + data: { task: task } + }).afterClosed().subscribe(result => { + if (result) { + this.toastr.success('Tarea programada correctamente'); + } + }); + } + }); + } + close(): void { this.dialogRef.close(false); } diff --git a/ogWebconsole/src/app/components/commands/commands-task/detail-task/detail-task.component.html b/ogWebconsole/src/app/components/commands/commands-task/detail-task/detail-task.component.html index 4217141..3a1cb86 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/detail-task/detail-task.component.html +++ b/ogWebconsole/src/app/components/commands/commands-task/detail-task/detail-task.component.html @@ -8,48 +8,40 @@

{{ 'taskId' | translate }}: {{ task.uuid }}

-

{{ 'status' | translate }}: {{ task.status }}

{{ 'creationDate' | translate }}: {{ task.createdAt | date: 'short' }}

{{ 'notes' | translate }}: {{ task.notes }}

- -

{{ 'includedCommandGroups' | translate }}

- - - - - - - - - - - - - -
{{ 'commandGroupColumn' | translate }} {{ group.name }} UUID {{ group.uuid }}

{{ 'commandsToExecute' | translate }}

-
-

{{ 'group' | translate }}: {{ group.name }}

- - - - - +
{{ 'commandColumn' | translate }} {{ command.name }}
+ + + + - - - - + + + + - - -
Orden{{ script.order }} UUID {{ command.uuid }} Tipo{{ script.type }}
-
+ + Contenido + + {{ script.content }} + + (parámetros configurados) + + + + + + + + +

No hay acciones configuradas.

- +
diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.css b/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.css index 70bff3e..6a1437a 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.css +++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.css @@ -89,7 +89,6 @@ table { .recurrence-type-container { display: flex; - justify-content: center; } .recurrence-chip { @@ -158,14 +157,12 @@ table { /* Estilos para el campo executionTime */ .time-display { - display: flex; + display: inline-flex; align-items: center; - justify-content: center; padding: 8px 12px; background-color: #667eea; color: white; border-radius: 16px; - min-width: 100px; } .time-icon { @@ -182,14 +179,12 @@ table { /* Estilos para el campo nextExecution */ .next-execution { - display: flex; + display: inline-flex; align-items: center; - justify-content: center; padding: 8px 12px; background-color: #4facfe; color: white; border-radius: 16px; - min-width: 120px; } .execution-icon { @@ -201,7 +196,6 @@ table { /* Estilos para el campo daysOfWeek */ .days-display { - text-align: center; } .days-header { @@ -229,7 +223,6 @@ table { display: flex; flex-wrap: wrap; gap: 4px; - justify-content: center; } .day-chip { @@ -245,7 +238,6 @@ table { /* Estilos para el campo months */ .months-display { - text-align: center; } .months-header { @@ -273,7 +265,6 @@ table { display: flex; flex-wrap: wrap; gap: 4px; - justify-content: center; } .month-chip { @@ -290,18 +281,15 @@ table { /* Estilos para el campo enabled */ .status-container { display: flex; - justify-content: center; } .status-indicator { - display: flex; + display: inline-flex; align-items: center; padding: 8px 16px; border-radius: 20px; font-weight: 600; font-size: 14px; - min-width: 100px; - justify-content: center; } .status-active { @@ -323,8 +311,7 @@ table { /* Estilos para datos no definidos */ .no-data { - text-align: center; - padding: 8px; + padding: 8px 0; } .no-data-text { @@ -375,7 +362,7 @@ table { .paginator-container { margin-top: 20px; display: flex; - justify-content: center; + justify-content: flex-end; } /* Estilos para los botones de acción en la tabla */ diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.html b/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.html index 7243b80..c079b2d 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.html +++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.html @@ -1,44 +1,30 @@ -

Gestionar programaciones de tareas en {{ data.commandTask?.name }}

+

{{ 'manageTaskSchedules' | translate }} {{ data.commandTask?.name }}

- - Buscar programación - + + {{ 'searchScheduleLabel' | translate }} + search - - Pulsar 'enter' para buscar - - - Buscar por tipo - - search - - Pulsar 'enter' para buscar + {{ 'searchHint' | translate }}
- +
+ @@ -126,8 +112,7 @@
{{ column.header }} @@ -54,7 +40,7 @@ - +
schedule @@ -65,45 +51,45 @@
schedule - {{ column.cell(schedule) || 'No calculada' }} + {{ column.cell(schedule) || ('notCalculated' | translate) }}
- +
- {{ day }}
- No definido + {{ 'notDefined' | translate }}
- +
- {{ month }}
- No definido + {{ 'notDefined' | translate }}
- - {{ column.cell(schedule) }} - +
{{ schedule.enabled ? 'check_circle' : 'cancel' }} - {{ schedule.enabled ? 'Activo' : 'Inactivo' }} + {{ (schedule.enabled ? 'active' : 'inactive') | translate }}
@@ -111,13 +97,13 @@ -
Acciones{{ 'columnActions' | translate }} - -
-
+
@@ -135,5 +120,5 @@ - + diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.ts b/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.ts index 2c0583e..8cfe8af 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.ts +++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-schedule/show-task-schedule.component.ts @@ -36,6 +36,8 @@ export class ShowTaskScheduleComponent implements OnInit{ ]; displayedColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'daysOfWeek', 'months', 'enabled', 'actions']; + allColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'daysOfWeek', 'months', 'enabled', 'actions']; + compactColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'enabled', 'actions']; constructor( private toastService: ToastrService, @@ -60,6 +62,7 @@ export class ShowTaskScheduleComponent implements OnInit{ (data) => { this.dataSource.data = data['hydra:member']; this.length = data['hydra:totalItems']; + this.updateDisplayedColumns(); this.loading = false; }, (error) => { @@ -295,6 +298,11 @@ export class ShowTaskScheduleComponent implements OnInit{ return mapping[monthName?.toLowerCase?.()] ?? -1; } + updateDisplayedColumns(): void { + const hasRecurrent = this.dataSource.data.some((s: any) => s.recurrenceType !== 'none'); + this.displayedColumns = hasRecurrent ? this.allColumns : this.compactColumns; + } + onPageChange(event: any): void { this.page = event.pageIndex; this.itemsPerPage = event.pageSize; diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.css b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.css index bd8e860..abebd3a 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.css +++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.css @@ -283,26 +283,129 @@ mat-spinner { border: 1px solid #e0e0e0; } -/* Estilos para el indicador de orden */ -.order-indicator { +/* Badge de paso */ +.step-badge { + display: inline-flex; + align-items: center; + justify-content: center; +} + +.step-number { + background-color: #3f51b5; + color: white; + border-radius: 50%; + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 15px; + box-shadow: 0 2px 4px rgba(63, 81, 181, 0.3); +} + +/* Display de tipo de acción */ +.type-display { display: flex; align-items: center; gap: 8px; - padding: 6px 12px; - background: #f8f9fa; + font-size: 13px; + font-weight: 500; + color: #333; +} + +.type-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: #666; +} + +/* Contenido del script en celda */ +.script-content-cell { + background-color: #f5f5f5; + padding: 8px 12px; + border: 1px solid #e0e0e0; + border-radius: 4px; + font-family: 'Monaco', 'Menlo', monospace; + white-space: pre-wrap; + font-size: 12px; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; +} + +.no-content-hint { + color: #999; + font-size: 13px; + font-style: italic; +} + +/* Hint secuencial */ +.sequential-hint { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 24px; + color: #1565c0; + background: #e3f2fd; + border-radius: 4px; + margin: 0 24px; + font-size: 13px; +} + +.sequential-hint mat-icon { + font-size: 18px; + width: 18px; + height: 18px; +} + +/* Timeline resumen */ +.timeline-summary { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 16px 24px; + background: #fafafa; + border: 1px solid #e8e8e8; + border-radius: 8px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.timeline-step { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + background: white; + border: 1px solid #d0d0d0; border-radius: 20px; - border: 1px solid #e9ecef; + font-size: 13px; } -.order-icon { - font-size: 16px; - width: 16px; - height: 16px; - color: #6c757d; +.timeline-number { + background: #3f51b5; + color: white; + border-radius: 50%; + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 12px; } -.order-number { - font-weight: 600; - color: #495057; - font-size: 14px; +.timeline-label { + color: #444; + font-weight: 500; +} + +.timeline-arrow { + color: #9e9e9e; + font-size: 20px; + width: 20px; + height: 20px; } diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.html b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.html index 4f767f7..f23a2b2 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.html +++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.html @@ -1,62 +1,81 @@ -

Gestionar scripts de tareas en {{ data.commandTask?.name }}

+

{{ 'taskActionsTitle' | translate }}: {{ data.commandTask?.name }}

+ +
+ info + {{ 'taskActionsSequentialHint' | translate }} +
-
- - Buscar contenido de script - - search - - Pulsar 'enter' para buscar - +
+ +
+ {{ item.order }} + {{ getTypeDisplay(item.type).label }} +
+ arrow_forward +
- - - - @@ -92,15 +98,17 @@ - - diff --git a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts index 8a6e185..4ca9eab 100644 --- a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts +++ b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts @@ -61,6 +61,11 @@ export class ShowMonoliticImagesComponent implements OnInit { header: 'Estado', cell: (image: any) => image.status }, + { + columnDef: 'datasize', + header: 'Tamaño', + cell: (image: any) => this.formatSize(image.datasize) + }, { columnDef: 'description', header: 'Descripción', @@ -144,17 +149,19 @@ export class ShowMonoliticImagesComponent implements OnInit { showImageInfo(event: MouseEvent, image:any) { event.stopPropagation(); + this.loading = true; this.loadImageAlert(image).subscribe( response => { this.alertMessage = response; + this.loading = false; + this.loadData(); this.dialog.open(ServerInfoDialogComponent, { width: '800px', data: { message: this.alertMessage } }); - this.loading = false; }, error => { this.toastService.error(error.error['hydra:description']); @@ -407,7 +414,7 @@ export class ShowMonoliticImagesComponent implements OnInit { this.loading = true; this.loadAlert().subscribe( response => { - this.alertMessage = response.output; + this.alertMessage = response.details; this.dialog.open(ServerInfoDialogComponent, { width: '800px', @@ -427,5 +434,19 @@ export class ShowMonoliticImagesComponent implements OnInit { this.dialogRef.close(); } + formatSize(bytes: string | number | null | undefined): string { + if (bytes === null || bytes === undefined || bytes === '') return '-'; + const n = Number(bytes); + if (isNaN(n) || n === 0) return '-'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let i = 0; + let value = n; + while (value >= 1024 && i < units.length - 1) { + value /= 1024; + i++; + } + return `${value.toFixed(2)} ${units[i]}`; + } + protected readonly isDevMode = isDevMode; } diff --git a/ogWebconsole/src/app/components/task-logs/task-logs.component.css b/ogWebconsole/src/app/components/task-logs/task-logs.component.css index f2077a2..81763a6 100644 --- a/ogWebconsole/src/app/components/task-logs/task-logs.component.css +++ b/ogWebconsole/src/app/components/task-logs/task-logs.component.css @@ -2,7 +2,7 @@ display: flex; justify-content: space-between; align-items: center; - padding: 10px 10px; + padding: 6px 10px; border-bottom: 1px solid #ddd; background: white; color: #333; @@ -56,10 +56,10 @@ } .stats-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 15px; - margin: 20px 0; + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 10px 0; padding: 0 10px; position: relative; } @@ -119,12 +119,16 @@ .stat-card { background: white; - border-radius: 12px; - padding: 20px; + border-radius: 20px; + padding: 8px 16px; text-align: center; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - transition: transform 0.3s ease, box-shadow 0.3s ease; - border-left: 4px solid #667eea; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); + transition: transform 0.2s ease, box-shadow 0.2s ease; + border-left: none; + display: flex; + align-items: center; + gap: 8px; + min-width: 0; } .stat-card.clickable { @@ -149,8 +153,8 @@ } .stat-card.clickable:hover { - transform: translateY(-3px); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .stat-card.clickable:active { @@ -238,6 +242,28 @@ color: #212529; } +.stat-pending { + border-left-color: #ff9800; + background: #ff9800; + color: white; +} + +.stat-pending .stat-number, +.stat-pending .stat-label { + color: white; +} + +.stat-cancelled { + border-left-color: #9e9e9e; + background: #9e9e9e; + color: white; +} + +.stat-cancelled .stat-number, +.stat-cancelled .stat-label { + color: white; +} + .stat-sent { border-left-color: #b19cd9; background: #b19cd9; @@ -250,23 +276,25 @@ } .stat-number { - font-size: 2rem; - font-weight: bold; + font-size: 1.25rem; + font-weight: 700; color: #667eea; - margin-bottom: 5px; + margin-bottom: 0; + line-height: 1; } .stat-label { - font-size: 0.9rem; + font-size: 0.75rem; color: #666; font-weight: 500; + white-space: nowrap; } .filters-section { background: #f8f9fa; border-radius: 8px; - margin: 20px 0; - padding: 20px; + margin: 8px 0; + padding: 10px 16px; border: 1px solid #e9ecef; } @@ -274,7 +302,7 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 15px; + margin-bottom: 6px; } .filters-header-actions { @@ -315,25 +343,25 @@ .search-container { display: flex; flex-direction: column; - gap: 15px; + gap: 4px; transition: all 0.3s ease; } .search-container.expanded { - gap: 20px; + gap: 8px; } .filter-row { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 15px; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 8px; align-items: end; } .advanced-filters { border-top: 1px solid #dee2e6; - padding-top: 15px; - margin-top: 10px; + padding-top: 8px; + margin-top: 4px; } .calendar-button-row { @@ -360,22 +388,22 @@ table { .search-string { flex: 1; - padding: 5px; + padding: 0; } .search-boolean { flex: 1; - padding: 5px; + padding: 0; } .search-select { flex: 2; - padding: 5px; + padding: 0; } .search-date { flex: 1; - padding: 5px; + padding: 0; } .mat-elevation-z8 { @@ -395,7 +423,7 @@ table { display: flex; justify-content: space-between; align-items: center; - padding: 15px 20px; + padding: 6px 16px; background: #f8f9fa; border-bottom: 1px solid #dee2e6; } @@ -505,8 +533,8 @@ table { .paginator-container { display: flex; - justify-content: end; - margin: 20px 0; + justify-content: flex-end; + margin: 8px 0; padding: 0 10px; } @@ -667,7 +695,7 @@ button.cancel-button { } .stats-container { - grid-template-columns: repeat(2, 1fr); + gap: 6px; } .filter-row { diff --git a/ogWebconsole/src/app/components/task-logs/task-logs.component.html b/ogWebconsole/src/app/components/task-logs/task-logs.component.html index b2df6d8..798a273 100644 --- a/ogWebconsole/src/app/components/task-logs/task-logs.component.html +++ b/ogWebconsole/src/app/components/task-logs/task-logs.component.html @@ -57,6 +57,20 @@
{{ getStatusCount('in-progress') }}
{{ 'inProgress' | translate }}
+
+
{{ getStatusCount('pending') }}
+
{{ 'pending' | translate }}
+
+
+
{{ getStatusCount('cancelled') }}
+
{{ 'cancelled' | translate }}
+
close - {{ 'enterClientName' | translate }} diff --git a/ogWebconsole/src/app/components/task-logs/task-logs.component.ts b/ogWebconsole/src/app/components/task-logs/task-logs.component.ts index 1326dec..a8012e2 100644 --- a/ogWebconsole/src/app/components/task-logs/task-logs.component.ts +++ b/ogWebconsole/src/app/components/task-logs/task-logs.component.ts @@ -116,6 +116,7 @@ export class TaskLogsComponent implements OnInit, OnDestroy { filteredCommands!: Observable; commandControl = new FormControl(); activeFilter: string = ''; + private eventSource: EventSource | null = null; constructor(private http: HttpClient, private joyrideService: JoyrideService, @@ -145,10 +146,10 @@ export class TaskLogsComponent implements OnInit, OnDestroy { map(name => (name ? this._filterClients(name) : this.clients.slice())) ); - const eventSource = new EventSource(`${this.mercureUrl}?topic=` + this.eventSource = new EventSource(`${this.mercureUrl}?topic=` + encodeURIComponent(`traces`)); - eventSource.onmessage = (event) => { + this.eventSource.onmessage = (event) => { const data = JSON.parse(event.data); if (data && data['@id']) { this.updateTracesStatus(data['@id'], data.status, data.progress); @@ -157,6 +158,9 @@ export class TaskLogsComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { + if (this.eventSource) { + this.eventSource.close(); + } } toggleFilters(): void { @@ -693,6 +697,20 @@ export class TaskLogsComponent implements OnInit, OnDestroy { this.loadTraces(); } + filterByPending(): void { + this.resetAllFilters(); + this.filters['status'] = 'pending'; + this.activeFilter = 'pending'; + this.loadTraces(); + } + + filterByCancelled(): void { + this.resetAllFilters(); + this.filters['status'] = 'cancelled'; + this.activeFilter = 'cancelled'; + this.loadTraces(); + } + filterBySent(): void { this.resetAllFilters(); this.filters['status'] = 'sent'; @@ -716,6 +734,10 @@ export class TaskLogsComponent implements OnInit, OnDestroy { return 'Trazas fallidas'; case 'in-progress': return 'Trazas en progreso'; + case 'pending': + return 'Trazas pendientes'; + case 'cancelled': + return 'Trazas canceladas'; case 'sent': return 'Trazas enviadas'; default: diff --git a/ogWebconsole/src/app/layout/header/header.component.css b/ogWebconsole/src/app/layout/header/header.component.css index 7e7c1dd..e186ef8 100644 --- a/ogWebconsole/src/app/layout/header/header.component.css +++ b/ogWebconsole/src/app/layout/header/header.component.css @@ -1,15 +1,16 @@ +:host { + display: block; +} + mat-toolbar { - /*height: 7vh;*/ min-height: 65px; min-width: 375px; background: rgba(226, 232, 240, 0.8); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); color: black; - position: fixed; + position: sticky; top: 0; - left: 0; - right: 0; z-index: 1000; box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1); transition: all 0.3s ease; @@ -20,6 +21,15 @@ mat-toolbar { padding: 0 16px; } +.sidebar-toggle-button { + flex-shrink: 0; + color: #3f51b5 !important; +} + +.sidebar-toggle-button .navbar-icon { + color: #3f51b5 !important; +} + /* Estilos específicos para el botón del sidebar */ .navbar-icon { color: #3f51b5; @@ -67,6 +77,26 @@ mat-toolbar button[mat-icon-button]:hover { cursor: pointer; } +.download-agent-button { + display: inline-flex; + align-items: center; + gap: 4px; + text-decoration: none; + color: inherit; + padding: 4px 10px !important; + font-size: 13px !important; + line-height: 1; + font-family: Roboto, "Helvetica Neue", sans-serif; + font-weight: 400; +} + +.download-icon { + font-size: 14px !important; + width: 14px !important; + height: 14px !important; + line-height: 14px !important; +} + .logout-button { background-color: #f00e0e; color: white; diff --git a/ogWebconsole/src/app/layout/header/header.component.html b/ogWebconsole/src/app/layout/header/header.component.html index 353e3da..b520422 100644 --- a/ogWebconsole/src/app/layout/header/header.component.html +++ b/ogWebconsole/src/app/layout/header/header.component.html @@ -1,18 +1,25 @@ + +
{{ column.header }} - -
- {{ column.cell(schedule) }} -
-
+ - - - - - + + + + - - {{ column.cell(schedule) }} - + + + + + + + + + + + + + - - + @@ -65,8 +84,7 @@
{{ 'executionOrder' | translate }} +
+ {{ item.order }} +
+
{{ 'actionType' | translate }} +
+ {{ getTypeDisplay(item.type).icon }} + {{ getTypeDisplay(item.type).label }} +
+
{{ 'scriptLabel' | translate }} +
+ {{ item.content }} +
+ + ({{ 'viewParameters' | translate }}) + +
{{ 'viewParameters' | translate }} + + — Acciones - {{ 'columnActions' | translate }} +
-
+
@@ -74,5 +92,5 @@ - + diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.ts b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.ts index 9be4dc7..7622307 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.ts +++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.ts @@ -25,14 +25,31 @@ export class ShowTaskScriptComponent implements OnInit{ filters: { [key: string]: string } = {}; columns = [ - { columnDef: 'id', header: 'ID', cell: (client: any) => client.id }, - { columnDef: 'order', header: 'Orden', cell: (client: any) => client.order }, - { columnDef: 'content', header: 'Script', cell: (client: any) => client.content }, - { columnDef: 'type', header: 'Tipo', cell: (client: any) => client.type }, - { columnDef: 'parameters', header: 'Parámetros', cell: (client: any) => client.parameters }, + { columnDef: 'id', header: 'ID', cell: (item: any) => item.id }, + { columnDef: 'order', header: 'Orden', cell: (item: any) => item.order }, + { columnDef: 'type', header: 'Tipo', cell: (item: any) => item.type }, + { columnDef: 'parameters', header: 'Parámetros', cell: (item: any) => item.parameters }, + { columnDef: 'content', header: 'Contenido', cell: (item: any) => item.content }, ]; - displayedColumns: string[] = ['id', 'order', 'type', 'parameters', 'content', 'actions']; + displayedColumns: string[] = ['order', 'type', 'content', 'parameters', 'actions']; + + typeLabels: { [key: string]: { label: string, icon: string } } = { + 'partition-and-format': { label: 'Particionar y formatear', icon: 'storage' }, + 'partition-assistant': { label: 'Asistente de particionado', icon: 'storage' }, + 'deploy-image': { label: 'Desplegar imagen', icon: 'cloud_download' }, + 'create-image': { label: 'Crear imagen', icon: 'cloud_upload' }, + 'run-script': { label: 'Ejecutar script', icon: 'code' }, + 'reboot': { label: 'Reiniciar', icon: 'restart_alt' }, + 'shutdown': { label: 'Apagar', icon: 'power_off' }, + 'poweron': { label: 'Encender', icon: 'power' }, + 'power-on': { label: 'Encender', icon: 'power' }, + 'login': { label: 'Iniciar sesión', icon: 'login' }, + }; + + getTypeDisplay(type: string): { label: string, icon: string } { + return this.typeLabels[type] || { label: type, icon: 'play_arrow' }; + } constructor( private toastService: ToastrService, @@ -53,9 +70,11 @@ export class ShowTaskScriptComponent implements OnInit{ loadData() { this.loading = true; - this.http.get(`${this.baseUrl}/command-task-scripts?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&commandTask.id=${this.data.commandTask?.id}`, { params: this.filters }).subscribe( + this.http.get(`${this.baseUrl}/command-task-scripts?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&commandTask.id=${this.data.commandTask?.id}&order[order]=ASC`, { params: this.filters }).subscribe( (data) => { - this.dataSource.data = data['hydra:member']; + const items = data['hydra:member']; + items.sort((a: any, b: any) => (a.order ?? 0) - (b.order ?? 0)); + this.dataSource.data = items; this.length = data['hydra:totalItems']; this.loading = false; }, diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.css index cf41b39..2154a9a 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.css +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.css @@ -10,6 +10,26 @@ align-items: center; } +/* Layout compacto: tipo + configuración en una fila */ +.compact-config-row { + display: flex; + gap: 20px; + margin-bottom: 20px; +} + +.compact-section { + padding: 20px !important; + margin-bottom: 0 !important; +} + +.type-section { + flex: 0 0 280px; +} + +.config-section { + flex: 1; +} + /* Secciones del formulario */ .form-section { background: white !important; @@ -249,6 +269,14 @@ mat-form-field { /* Responsive design */ @media (max-width: 768px) { + .compact-config-row { + flex-direction: column; + } + + .type-section { + flex: none; + } + .select-container { padding: 16px; } @@ -879,8 +907,8 @@ mat-form-field { .git-info-grid { display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 16px; + grid-template-columns: 1fr; + gap: 12px; } .git-info-item { diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.html index d28e1b6..62861a4 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.html @@ -33,14 +33,13 @@
-
-
- settings - Configuración de tipo de imagen -
- -
- +
+
+
+ settings + Tipo de imagen +
+ Tipo de imagen Monolítica @@ -48,89 +47,128 @@
-
-
-
- code - Configuración Git +
+
+ image + Configuración +
+ +
+ + + Crear imagen + + + Actualizar imagen + + +
+ +
+ + Nombre canónico + + Nombre para la nueva imagen + +
+ +
+ + Seleccione imagen + + Seleccionar imagen para actualizar + {{ image?.name }} + + + Selecciona la imagen existente que quieres actualizar. + +
- -
-
-
- info - La imagen en caché a actualizar es: - -
-
-
-
- - - folder - Repositorio - - - -
-
- - - account_tree - Rama origen - - - -
-
- - - call_split - Rama destino - - - - -
+ +
+
+ code + Configuración Git +
+ +
+
+
+ info + La imagen en caché a actualizar es: +
- -
- warning -
-
Repositorio no encontrado
-
- El repositorio "{{ gitData.repo }}" no existe en el listado de repositorios disponibles. - Es posible que necesites crearlo primero. +
+
+
+ + + folder + Repositorio + + + +
+
+ + + account_tree + Rama origen + + + +
+
+ + + call_split + Rama destino + + + + +
+
+ +
+ warning +
+
Repositorio no encontrado
+
+ El repositorio "{{ gitData.repo }}" no existe en el listado de repositorios disponibles. + Es posible que necesites crearlo primero. +
-
-
- Cargando información de Git... -
-
- No se encontró información de Git para esta partición +
+ Cargando información de Git... +
+
+ No se encontró información de Git para esta partición +
-
- - -
- - Opción 'Forzar push' - -
- + +
+ + Opción 'Forzar push' + +
+
- -
- - Seleccionar repositorio Git - - Seleccionar repositorio git / SO - {{ repo.name }} - - - - info - Selecciona el repositorio git para obtener las imágenes disponibles. - - No hay repositorios disponibles. Crea uno nuevo para continuar. - - - - - -
- -
- -
-
- image - Configuración de imagen monolítica -
- -
- - - Crear imagen - - - Actualizar imagen - - -
-
- info - Crea una nueva imagen con el nombre especificado - Actualiza una imagen existente seleccionada -
- -
- - Nombre canónico - - Introduce el nombre para la nueva imagen que se creará. - -
- -
- - Seleccione imagen - - Seleccionar imagen para actualizar - {{ image?.name }} - - - Selecciona la imagen existente que quieres actualizar. - + +
+ + Seleccionar repositorio Git + + Seleccionar repositorio git / SO + {{ repo.name }} + + + + info + Selecciona el repositorio git para obtener las imágenes disponibles. + + No hay repositorios disponibles. Crea uno nuevo para continuar. + + + +
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.css index 05cebdb..8726114 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.css +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.css @@ -2,10 +2,10 @@ display: flex; justify-content: space-between; align-items: center; - padding: 24px 32px; + padding: 16px 24px; background: white; border-radius: 12px; - margin-bottom: 20px; + margin-bottom: 16px; } .header-container-title { @@ -55,37 +55,81 @@ cursor: not-allowed; } +.secondary-button { + background: #e3f2fd !important; + color: #1976d2 !important; +} + +.secondary-button:hover:not(:disabled) { + background: #bbdefb !important; +} + /* Contenedor principal */ .select-container { background: white !important; - margin-top: 20px; + margin-top: 12px; align-items: center; - padding: 20px; + padding: 0 20px 12px; box-sizing: border-box; } +.config-container { + background: transparent !important; +} + /* Secciones del formulario */ .form-section { background: white !important; border-radius: 16px; - padding: 32px; - margin-bottom: 24px; + padding: 24px; + margin-bottom: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); border: 1px solid #bbdefb; } +.compact-section { + padding: 20px !important; + margin-bottom: 12px !important; +} + .form-section-title { display: flex; align-items: center; - gap: 12px; - margin-bottom: 24px; - font-size: 20px; + gap: 10px; + margin-bottom: 16px; + font-size: 17px; font-weight: 600; color: #2c3e50; - padding-bottom: 16px; + padding-bottom: 12px; border-bottom: 2px solid #f8f9fa; } +/* Layout horizontal: imagen + partición */ +.deploy-config-row { + display: flex; + gap: 16px; + margin-bottom: 4px; +} + +.image-config-section { + flex: 0 0 340px; +} + +.image-config-section .filters-row { + flex-direction: column; + gap: 0; + margin-bottom: 0; +} + +.image-config-section .filters-row mat-form-field { + width: 100% !important; +} + +.partition-select-section { + flex: 1; + min-width: 0; +} + .form-section-title mat-icon { color: #667eea; font-size: 24px; @@ -93,14 +137,6 @@ height: 24px; } -/* Selectores */ -.selector { - display: flex; - gap: 20px; - margin-bottom: 20px; - flex-wrap: wrap; -} - .half-width { flex: 1; min-width: 250px; @@ -276,9 +312,9 @@ mat-form-field { /* Tabla de particiones */ .partition-table-container { background: white !important; - border-radius: 12px; - padding: 24px; - margin-top: 20px; + border-radius: 8px; + padding: 0; + margin-top: 0; } table { @@ -296,24 +332,48 @@ table { background: white !important; color: #2c3e50; font-weight: 600; - padding: 16px; + padding: 10px 12px; } ::ng-deep .mat-cell { - padding: 16px; + padding: 8px 12px; color: #495057; } -/* Opciones avanzadas */ -.input-group { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 20px; - margin-top: 20px; +/* Resumen de configuración avanzada */ +.method-config-summary { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: #f0f4ff; + border: 1px solid #d0d9ff; + border-radius: 8px; + margin-top: 8px; } -.input-field { - width: 100%; +.summary-content { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.summary-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: #667eea; + flex-shrink: 0; +} + +.summary-text { + font-size: 12px; + font-weight: 500; + color: #495057; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } /* Mensajes de error */ @@ -568,25 +628,13 @@ table { gap: 40px; } -.monolithic-row { - display: flex; - gap: 24px; - align-items: flex-end; - margin-bottom: 20px; -} +@media (max-width: 900px) { + .deploy-config-row { + flex-direction: column; + } -.monolithic-row .half-width { - flex: 1 1 200px; - min-width: 200px; -} - -.monolithic-row .full-width { - flex: 2; -} - -@media (max-width: 768px) { - .monolithic-row .full-width { - flex: 2; + .image-config-section { + flex: none; } } diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html index 7258ff2..d86dc8d 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html @@ -86,58 +86,183 @@ -
- -
+
+
+ +
+
+ image + Configuración de imagen +
+ +
+ + Tipo de imagen + + Monolítica + Git + + +
+ + +
+ + Seleccione método de deploy + + {{ method.name }} + + + + Seleccione imagen + + +
{{ image.name }}
+
{{ image.description }}
+
+
+
+
+ + +
+ + Seleccionar Repositorio + + + {{ repo }} + + + hourglass_empty + + + + Seleccionar Rama + + + {{ branch }} + + + hourglass_empty + + + + Tag + + No + Sí + + +
+ + + @if (imageType === 'monolithic' && selectedMethod && !isDirectMethod()) { +
+
+ cached + Modo de operación +
+ + +
+ storage +
+ Despliegue completo + Actualiza caché y restaura partición +
+
+
+ +
+ download +
+ Solo actualizar caché + Descarga imagen sin restaurar partición +
+
+
+
+
+ } + +
+ {{ errorMessage }} +
+ +
+
+ tune + {{ getMethodConfigSummary() }} +
+ +
+
+ + + @if (deployMode !== 'update-cache') { +
+
+ storage + Selección de partición +
+ +
+
+ + + Instrucciones generadas + + + +
{{ ogInstructions }}
+
+
+
+ + + +
+
+ } + + + @if (deployMode === 'update-cache') { +
+
+ info + Estado actual de particiones +
+
+ + +
+
+ } +
+ + +
- image - Configuración de imagen + history + Selección de commit
-
- - Tipo de imagen - - Monolítica - Git - - -
- - -
- - Seleccionar Repositorio - - - {{ repo }} - - - hourglass_empty - - - - Seleccionar Rama - - - {{ branch }} - - - hourglass_empty - - - - Tag - - No - Sí - - -
- -
+
@@ -179,182 +304,10 @@
Seleccionar
- -
- - Seleccione método de deploy - - {{ method.name }} - - - - Seleccione imagen - - -
{{ image.name }}
-
{{ image.description }}
-
-
-
-
- - - @if (imageType === 'monolithic' && selectedMethod && !isDirectMethod()) { -
-
- cached - Modo de operación -
- - -
- storage -
- Despliegue completo - Actualiza caché y restaura partición -
-
-
- -
- download -
- Solo actualizar caché - Descarga imagen sin restaurar partición -
-
-
-
-
- } - -
- {{ errorMessage }} -
- -
-
- settings - Opciones multicast - Opciones torrent -
- -
- - Puerto - - - - - Dirección - - - - - Modo Multicast - - - {{ option.name }} - - - - - - Velocidad - - - - - Máximo Clientes - - - - - Tiempo Máximo de Espera - - -
- -
- - Modo P2P - - - {{ option.name }} - - - - - - Semilla (minutos) - - -
-
- - - @if (deployMode !== 'update-cache') { -
-
- storage - Selección de partición -
- -
- @if (showInstructions) { - - - Instrucciones generadas - - - -
{{ ogInstructions }}
-
-
- } - - - -
-
- } - - - @if (deployMode === 'update-cache') { -
-
- info - Estado actual de particiones -
-
- - -
-
- }
- \ No newline at end of file + diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts index db27f68..58de3dc 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts @@ -5,6 +5,7 @@ import { HttpClient } from "@angular/common/http"; import { ToastrService } from "ngx-toastr"; import { ActivatedRoute, Router } from "@angular/router"; import { ConfigService } from '@services/config.service'; +import {DeployMethodConfigDialogComponent} from "./deploy-method-config-dialog/deploy-method-config-dialog.component"; 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"; @@ -108,6 +109,7 @@ export class DeployImageComponent implements OnInit { showOnlyTagged: boolean = false; filteredCommits: any[] = []; + methodConfigured: boolean = false; constructor( private http: HttpClient, @@ -267,22 +269,74 @@ export class DeployImageComponent implements OnInit { validateImageSize() { if (this.selectedImage && this.selectedPartition) { if ((this.selectedImage.datasize / 1024) / 1024 > this.selectedPartition.size) { - this.errorMessage = "El tamaño de la imagen seleccionada excede el tamaño de la partición."; return false; } } this.errorMessage = ""; - if (this.selectedMethod) { - setTimeout(() => { - this.scrollToPartitionSection(); - }, 300); + if (this.selectedMethod && this.needsAdvancedConfig(this.selectedMethod)) { + this.openMethodConfigDialog(); + } else { + this.methodConfigured = false; } return true; } + needsAdvancedConfig(method: string): boolean { + return ['udpcast', 'uftp', 'udpcast-direct', 'p2p'].includes(method); + } + + openMethodConfigDialog(): void { + const dialogRef = this.dialog.open(DeployMethodConfigDialogComponent, { + width: '540px', + disableClose: false, + data: { + method: this.selectedMethod, + mcastIp: this.mcastIp, + mcastPort: this.mcastPort, + mcastMode: this.mcastMode, + mcastSpeed: this.mcastSpeed, + mcastMaxClients: this.mcastMaxClients, + mcastMaxTime: this.mcastMaxTime, + p2pMode: this.p2pMode, + p2pTime: this.p2pTime, + multicastModeOptions: this.multicastModeOptions, + p2pModeOptions: this.p2pModeOptions, + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.mcastIp = result.mcastIp; + this.mcastPort = result.mcastPort; + this.mcastMode = result.mcastMode; + this.mcastSpeed = result.mcastSpeed; + this.mcastMaxClients = result.mcastMaxClients; + this.mcastMaxTime = result.mcastMaxTime; + this.p2pMode = result.p2pMode; + this.p2pTime = result.p2pTime; + this.methodConfigured = true; + } + }); + } + + getMethodConfigSummary(): string { + if (!this.selectedMethod) return ''; + if (['udpcast', 'uftp', 'udpcast-direct'].includes(this.selectedMethod)) { + return `${this.mcastIp}:${this.mcastPort} · ${this.mcastMode} · ${this.mcastSpeed} Mbps`; + } + if (this.selectedMethod === 'p2p') { + let summary = this.p2pMode || ''; + if (this.p2pMode === 'seeder' && this.p2pTime) { + summary += ` · ${this.p2pTime} min`; + } + return summary; + } + return ''; + } + save(): void { const needsPartition = this.deployMode !== 'update-cache'; @@ -525,6 +579,7 @@ export class DeployImageComponent implements OnInit { this.ogInstructions = `og-deploy-image --image ${this.selectedImage.name} --partition ${this.selectedPartition?.partitionNumber || 1} --method ${this.selectedMethod}`; } + scrollToPartitionSection() { const partitionSection = document.getElementById('partition-selection'); if (partitionSection) { diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.css new file mode 100644 index 0000000..64abc47 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.css @@ -0,0 +1,51 @@ +h2[mat-dialog-title] { + display: flex; + align-items: center; + gap: 10px; + margin: 0 0 8px 0; + padding: 20px 24px 12px; + font-size: 18px; + font-weight: 600; + color: #2c3e50; + border-bottom: 2px solid #f0f0f0; +} + +.dialog-title-icon { + color: #667eea; + font-size: 22px; + width: 22px; + height: 22px; +} + +.config-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px 20px; + padding: 16px 0 8px; +} + +mat-dialog-content { + min-width: 460px; + padding: 8px 24px !important; +} + +mat-dialog-actions { + padding: 16px 24px 20px !important; + margin: 0 !important; + border-top: 1px solid #f0f0f0; + gap: 12px; +} + +::ng-deep .mat-dialog-actions button { + min-width: 100px; +} + +@media (max-width: 600px) { + .config-grid { + grid-template-columns: 1fr; + } + + mat-dialog-content { + min-width: auto; + } +} diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.html new file mode 100644 index 0000000..d2ba893 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.html @@ -0,0 +1,67 @@ +

+ settings + Configuración de {{ getMethodLabel() }} +

+ + + +
+ + Dirección + + + + + Puerto + + + + + Modo Multicast + + + {{ option.name }} + + + + + + Velocidad + + + + + Máx. Clientes + + + + + Tiempo máx. espera + + +
+ + +
+ + Modo P2P + + + {{ option.name }} + + + + + + Semilla (minutos) + + +
+
+ + + + + diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.ts new file mode 100644 index 0000000..8d491b2 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.ts @@ -0,0 +1,93 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +export interface DeployMethodConfigData { + method: string; + mcastIp: string; + mcastPort: number; + mcastMode: string; + mcastSpeed: number; + mcastMaxClients: number; + mcastMaxTime: number; + p2pMode: string; + p2pTime: number; + multicastModeOptions: { name: string; value: string }[]; + p2pModeOptions: { name: string; value: string }[]; +} + +@Component({ + selector: 'app-deploy-method-config-dialog', + templateUrl: './deploy-method-config-dialog.component.html', + styleUrls: ['./deploy-method-config-dialog.component.css'] +}) +export class DeployMethodConfigDialogComponent { + mcastIp: string; + mcastPort: number; + mcastMode: string; + mcastSpeed: number; + mcastMaxClients: number; + mcastMaxTime: number; + p2pMode: string; + p2pTime: number; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DeployMethodConfigData + ) { + this.mcastIp = data.mcastIp; + this.mcastPort = data.mcastPort; + this.mcastMode = data.mcastMode; + this.mcastSpeed = data.mcastSpeed; + this.mcastMaxClients = data.mcastMaxClients; + this.mcastMaxTime = data.mcastMaxTime; + this.p2pMode = data.p2pMode; + this.p2pTime = data.p2pTime; + } + + isMulticast(): boolean { + return ['udpcast', 'uftp', 'udpcast-direct'].includes(this.data.method); + } + + isP2P(): boolean { + return this.data.method === 'p2p'; + } + + getMethodLabel(): string { + const labels: Record = { + 'udpcast': 'Multicast', + 'uftp': 'UFTP', + 'udpcast-direct': 'Multicast (directo)', + 'p2p': 'Torrent (P2P)', + }; + return labels[this.data.method] || this.data.method; + } + + isValid(): boolean { + if (this.isMulticast()) { + return !!(this.mcastPort && this.mcastIp && this.mcastMode && this.mcastSpeed && this.mcastMaxClients && this.mcastMaxTime); + } + if (this.isP2P()) { + if (!this.p2pMode) return false; + if (this.p2pMode === 'seeder' && !this.p2pTime) return false; + return true; + } + return true; + } + + onSave(): void { + this.dialogRef.close({ + mcastIp: this.mcastIp, + mcastPort: this.mcastPort, + mcastMode: this.mcastMode, + mcastSpeed: this.mcastSpeed, + mcastMaxClients: this.mcastMaxClients, + mcastMaxTime: this.mcastMaxTime, + p2pMode: this.p2pMode, + p2pTime: this.p2pTime, + }); + } + + onCancel(): void { + this.dialogRef.close(null); + } +} diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css index 4739a62..a069c39 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css @@ -1,20 +1,21 @@ .partition-assistant { - padding: 32px; - margin: 20px; + padding: 16px 20px; + margin: 0 20px 20px; background: white !important; - border-radius: 16px; + border-radius: 0 0 16px 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); border: 1px solid #bbdefb; + border-top: none; } .header-container { display: flex; justify-content: space-between; align-items: center; - padding: 24px 32px; + padding: 16px 24px; background: white; border-radius: 12px; - margin-bottom: 20px; + margin-bottom: 16px; } .header-container-title { @@ -114,76 +115,138 @@ cursor: not-allowed; } +.secondary-button { + background: #e3f2fd !important; + color: #1976d2 !important; +} + +.secondary-button:hover:not(:disabled) { + background: #bbdefb !important; +} + .row-button { display: flex; align-items: center; - gap: 30px; + gap: 16px; } -.disk-selector-card { - padding: 32px; - margin: 20px; - background: white !important; - border-radius: 16px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); - border: 1px solid #bbdefb; - overflow: hidden; +.disk-and-partitions-container { + margin: 0 20px; } -.card-header { +.disk-toolbar { display: flex; align-items: center; - padding: 24px 32px; - background: transparent; - color: #1976d2; - border-bottom: 1.5px solid #1976d2; + gap: 16px; + padding: 12px 20px; + background: white; + border-radius: 16px 16px 0 0; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + border: 1px solid #bbdefb; + flex-wrap: wrap; } -.card-icon { - font-size: 28px; - width: 28px; - height: 28px; - margin-right: 16px; - color: #1976d2; +.disk-toolbar-select { + flex: 0 0 280px; + margin-bottom: -1.25em; } -.card-title h3 { - margin: 0 0 4px 0; - font-size: 20px; +.disk-toolbar-info { + display: flex; + align-items: center; + gap: 16px; + flex: 1; + min-width: 0; +} + +.disk-stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.disk-stat-label { + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #6c757d; +} + +.disk-stat-value { + font-size: 13px; font-weight: 600; - color: #1976d2; + color: #333; } -.card-title p { - margin: 0; - font-size: 14px; - opacity: 0.9; - color: #1976d2; +.disk-stat-value.used { + color: #dc3545; } -.card-content { - padding: 24px 32px; +.disk-stat-value.free { + color: #28a745; } -.disk-select-field { - width: 100%; - margin-bottom: 20px; -} - -.selected-disk-hint { +.disk-usage-bar-inline { display: flex; align-items: center; gap: 8px; - color: #2e7d32; - font-weight: 500; - font-size: 14px; + flex: 1; + min-width: 80px; + max-width: 200px; } -.hint-icon { - font-size: 16px; - width: 16px; - height: 16px; - color: #4caf50; +.disk-usage-bar-inline .usage-bar-container { + flex: 1; +} + +.usage-percent-label { + font-size: 12px; + font-weight: 600; + color: #666; + white-space: nowrap; +} + +.disk-toolbar-meta { + display: flex; + gap: 8px; + align-items: center; +} + +.meta-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 16px; + background: #f0f4ff; + color: #495057; + font-size: 12px; + font-weight: 500; + border: 1px solid #d0d9ff; +} + +.meta-chip mat-icon { + font-size: 14px; + width: 14px; + height: 14px; + color: #667eea; +} + +.add-partition-btn { + display: inline-flex; + align-items: center; + gap: 4px; + white-space: nowrap; + margin: 0 !important; + padding: 8px 16px !important; +} + +.add-partition-btn mat-icon { + font-size: 18px; + width: 18px; + height: 18px; } /* Opciones del select */ @@ -386,11 +449,11 @@ } .select-container { - margin-top: 20px; + margin-top: 12px; align-items: center; width: 100%; box-sizing: border-box; - padding: 20px; + padding: 0 20px 12px; } /* Expansion panel */ @@ -503,8 +566,8 @@ } .form-container { - flex: 0 0 65%; - max-width: 65%; + flex: 1; + min-width: 0; padding-right: 20px; box-sizing: border-box; background: white; @@ -512,73 +575,6 @@ border: none; } -.disk-space-info-container { - margin: 24px 0; - background: none; - border-radius: 0; - border: none; - padding: 0; -} - -.disk-space-card { - display: flex; - gap: 24px; - margin-bottom: 20px; - flex-wrap: wrap; -} - -.space-info-item { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 20px; - background: #f8f9fa; - border-radius: 12px; - border: 1px solid #e9ecef; - min-width: 180px; -} - -.space-icon { - font-size: 24px; - width: 24px; - height: 24px; -} - -.used-icon { - color: #dc3545; -} - -.free-icon { - color: #28a745; -} - -.total-icon { - color: #007bff; -} - -.space-details { - display: flex; - flex-direction: column; - gap: 4px; -} - -.space-label { - font-size: 12px; - font-weight: 500; - color: #6c757d; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.space-value { - font-size: 16px; - font-weight: 600; - color: #212529; -} - -.disk-usage-bar { - margin-top: 16px; -} .usage-bar-container { width: 100%; @@ -596,13 +592,6 @@ transition: width 0.3s ease; } -.usage-percentage { - font-size: 14px; - font-weight: 600; - color: #666; - margin-top: 8px; - text-align: center; -} .partition-table { width: 100%; @@ -656,10 +645,9 @@ } .chart-container { - flex: 0 0 35%; - max-width: 35%; - align-self: center; - justify-self: center; + flex: 0 0 280px; + max-width: 280px; + align-self: flex-start; } .chart-header { @@ -1263,13 +1251,17 @@ padding: 20px; } - .disk-space-card { + .disk-toolbar { flex-direction: column; - gap: 12px; + align-items: stretch; } - - .space-info-item { - min-width: auto; + + .disk-toolbar-select { + flex: none; + } + + .disk-toolbar-info { + flex-wrap: wrap; } .row { @@ -1293,7 +1285,7 @@ } .partition-validation-indicator { - margin: 16px 0; + margin: 8px 0; border-radius: 12px; transition: all 0.3s ease; } @@ -1432,45 +1424,6 @@ ::ng-deep .model-selector .mat-radio-outer-circle { border-color: #667eea !important; } ::ng-deep .model-selector .mat-radio-inner-circle { background-color: #667eea !important; } -/* Overrides: info badges sin fondo ni borde, estilo neutro */ -.info-badge { - background: transparent; - border: none; - box-shadow: none; -} - -.info-badge:hover { - transform: none; - box-shadow: none; -} - -.info-badge:first-of-type, -.info-badge:last-of-type { - background: transparent; - border-color: transparent; -} - -.info-badge:first-of-type .info-icon, -.info-badge:last-of-type .info-icon, -.info-badge:first-of-type .info-label, -.info-badge:last-of-type .info-label, -.info-badge:first-of-type .info-value, -.info-badge:last-of-type .info-value { - color: inherit; -} - -/* Colores neutros en badges */ -.info-badge .info-label { - color: #333; -} - -.info-badge .info-value { - color: #333; -} - -.info-badge .info-icon { - color: #6c757d; -} diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html index 21f0df9..7eb5410 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html @@ -17,15 +17,9 @@
-
- -
- -
- -
@@ -108,18 +102,10 @@ -
-
- storage -
-

Selección de Disco

-

Elige el disco donde se realizarán las operaciones de particionado

-
-
- -
- - Seleccionar disco +
+
+ + Disco
@@ -133,84 +119,57 @@
- Selecciona el disco que deseas particionar - - check_circle - Disco {{ selectedDisk.diskNumber }} seleccionado - {{ (selectedDisk.totalDiskSize / 1024).toFixed(2) }} GB -
- -
- warning -
- No hay discos disponibles - Asegúrate de que el cliente modelo tenga discos configurados + +
+
+ Usado + {{ selectedDisk.used | number:'1.0-0' }} MB
+
+ Libre + {{ (selectedDisk.totalDiskSize - selectedDisk.used) | number:'1.0-0' }} MB +
+
+ Total + {{ selectedDisk.totalDiskSize | number:'1.0-0' }} MB +
+
+
+
+
+ {{ ((selectedDisk.used / selectedDisk.totalDiskSize) * 100) | number:'1.0-0' }}% +
+
+ +
+ + memory {{ selectedModelClient.firmwareType }} + + + storage {{ partitionCode }} + +
+ + +
+ +
+ warning +
+ No hay discos disponibles + Asegúrate de que el cliente modelo tenga discos configurados
-
- - -
- memory -
- Firmware - {{ selectedModelClient.firmwareType }} -
-
- -
- storage -
- Tabla de particiones - {{ partitionCode }} -
-
-
- - -
-
-
-
- storage -
- Espacio usado - {{ selectedDisk.used | number:'1.2-2' }} MB -
-
- -
- cloud_done -
- Espacio libre - {{ (selectedDisk.totalDiskSize - selectedDisk.used) | number:'1.2-2' }} MB -
-
- -
- dns -
- Espacio total - {{ selectedDisk.totalDiskSize | number:'1.2-2' }} MB -
-
-
- -
-
-
-
- {{ ((selectedDisk.used / selectedDisk.totalDiskSize) * 100) | number:'1.1-1' }}% usado -
-
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.css index 9227a68..10c99de 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.css +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.css @@ -104,6 +104,7 @@ table { padding: 24px 32px; background: white; border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 20px; } @@ -389,13 +390,13 @@ table { .new-command-container { display: flex; flex-direction: column; - gap: 10px; - padding: 15px; - background-color: #eaeff6; - border: 1px solid #ddd; - border-radius: 8px; - box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1); - margin-top: 15px; + gap: 12px; + padding: 20px; + background: white; + border: 1px solid #e9ecef; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + margin-top: 16px; } .new-command-container mat-form-field { @@ -407,43 +408,10 @@ table { resize: vertical; } -.new-command-container .action-button { - align-self: flex-end; - color: white; - padding: 8px 16px; - border: none; - border-radius: 4px; - cursor: pointer; -} - .full-width { width: 100%; } -/* Secciones del formulario */ -.form-section { - background: white !important; - border-radius: 8px; - padding: 24px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - margin-bottom: 20px; - border: 1px solid #bbdefb; - padding: 20px; -} - -.form-section-title { - font-size: 18px; - font-weight: 600; - color: #333; - margin-bottom: 20px; - display: flex; - align-items: center; - gap: 8px; -} - -.form-section-title mat-icon { - color: #2196f3; -} .toggle-options { display: flex; gap: 10px; diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.html index 7e772c9..36f19de 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.html @@ -7,7 +7,7 @@
- cloud_download + terminal
Destino {{ runScriptTitle }} @@ -25,7 +25,6 @@
-
@@ -65,7 +64,7 @@ class="client-image" />
- {{ client.name }} + {{ client.name | slice:0:20 }} {{ client.ip }} {{ client.mac }}
@@ -83,6 +82,12 @@ Configuración de script
+
+ + Ejecutar como administrador + +
+
@@ -190,7 +195,7 @@
-
+
{{ scriptContent }}
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.ts index b844010..2366286 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.ts @@ -36,6 +36,7 @@ export class RunScriptAssistantComponent implements OnInit{ parameterNames: string[] = Object.keys(this.parameters); runScriptContext: any = null; newScriptParameters: string[] = []; + runAsAdmin: boolean = false; constructor( private http: HttpClient, @@ -200,7 +201,8 @@ export class RunScriptAssistantComponent implements OnInit{ this.http.post(`${this.baseUrl}/commands/run-script`, { clients: this.selectedClients.map((client: any) => client.uuid), script: this.commandType === 'existing' ? this.scriptContent : this.newScript, - queue: result + queue: result, + runAsAdmin: this.runAsAdmin }).subscribe( response => { this.toastService.success('Script ejecutado correctamente'); diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index 6075acd..2763305 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -10,7 +10,7 @@ display: flex; justify-content: space-between; align-items: center; - padding: 10px 10px; + padding: 6px 10px; border-bottom: 1px solid #ddd; } @@ -125,18 +125,19 @@ .groups-button-row { display: flex; - gap: 15px; + gap: 8px; padding-right: 0.5rem; + align-items: center; } .clients-view-header { display: flex; flex-direction: row; justify-content: space-between; - margin-bottom: 0.5rem; - margin-top: 0.5rem; + margin-bottom: 0.25rem; + margin-top: 0.25rem; align-items: center; - padding-right: 1rem; + padding-right: 0.75rem; } @media (max-width: 768px) { @@ -147,10 +148,10 @@ } .clients-title-name { - font-size: x-large; + font-size: 1.15rem; display: block; - padding: 1rem 1rem 1rem 13px; - margin-left: 0.6rem; + padding: 0.5rem 0.5rem 0.5rem 12px; + margin-left: 0.4rem; flex: 1 1 auto; white-space: nowrap; overflow: hidden; @@ -189,6 +190,10 @@ padding-left: 1rem; } +.cards-select-all-inline { + margin-right: 4px; +} + .list-view { overflow-x: auto; display: flex; @@ -229,13 +234,11 @@ .paginator-container { display: flex; - justify-content: end; + justify-content: flex-end; margin-top: auto; } .cards-paginator { - width: 100%; - margin-left: 1rem; background-color: #fafafa; margin-bottom: 1rem; } @@ -422,7 +425,8 @@ mat-tree mat-tree-node.disabled:hover { .filters-container { display: flex; flex-direction: column; - padding: 1em; + padding: 8px 12px; + gap: 2px; flex-grow: 1; } @@ -433,6 +437,31 @@ mat-tree mat-tree-node.disabled:hover { box-sizing: border-box; } +.direct-clients-toggle { + display: flex; + align-items: center; + padding: 4px 2px; + font-size: 13px; +} + +::ng-deep .filters-container .mat-form-field-wrapper { + padding-bottom: 4px !important; + margin: 0 !important; +} + +::ng-deep .filters-container .mat-form-field-infix { + padding: 6px 0 !important; + font-size: 13px; +} + +::ng-deep .filters-container .mat-form-field-appearance-outline .mat-form-field-outline { + top: 0 !important; +} + +::ng-deep .filters-container .mat-form-field-label { + font-size: 13px !important; +} + .filters-panel { flex-shrink: 0; } @@ -962,9 +991,9 @@ mat-button-toggle-group { .clients-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 10px; - padding: 0.5rem 1rem 1rem 1rem; + grid-template-columns: repeat(auto-fill, minmax(135px, 1fr)); + gap: 8px; + padding: 0.25rem 0.75rem 0.75rem 0.75rem; width: 100%; box-sizing: border-box; } diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 64451eb..f5b47d9 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -15,7 +15,7 @@
-
+
+
+ + {{ 'directClientsToggle' | translate }} + +
+
- -
@@ -400,7 +412,7 @@ more_vert - + @@ -413,7 +425,7 @@ visibility {{ 'viewDetails' | translate }} -
+ [pageSizeOptions]="pageSizeOptions" (page)="onPageChange($event)">
@@ -497,7 +509,7 @@
Client Icon - +
diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 93d99ea..5334c38 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -75,13 +75,13 @@ export class GroupsComponent implements OnInit, OnDestroy { currentView: string = 'list'; savedFilterNames: [string, string][] = []; selectedTreeFilter = ''; - syncStatus = false; - syncingClientId: string | null = null; + syncingClientIds = new Set(); private originalTreeData: TreeNode[] = []; arrayClients: any[] = []; filters: { [key: string]: string } = {}; private clientFilterSubject = new Subject(); loading = false; + showOnlyDirectClients: boolean = false; // Nuevas propiedades para funcionalidades mejoradas selectedClient: any = null; @@ -151,6 +151,10 @@ export class GroupsComponent implements OnInit, OnDestroy { this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); this.currentView = this.auth.groupsView || 'list'; + this.itemsPerPage = this.auth.clientsPerPage || 20; + if (!this.pageSizeOptions.includes(this.itemsPerPage)) { + this.pageSizeOptions = [...this.pageSizeOptions, this.itemsPerPage].sort((a, b) => a - b); + } } @@ -406,6 +410,10 @@ export class GroupsComponent implements OnInit, OnDestroy { this.selectedNode = node; const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid); this.fetchClientsForNode(node, selectedClientsBeforeEdit); + + if (node.type === NodeType.Classroom || node.type === NodeType.ClientsGroup) { + this.getStatusForAllClientsInNode(node); + } } onMenuClick(event: Event, node: any): void { @@ -428,11 +436,21 @@ export class GroupsComponent implements OnInit, OnDestroy { this.isLoadingClients = true; this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: backendParams }).subscribe({ next: (response: any) => { - this.selectedClients.data = response['hydra:member']; - if (this.selectedNode) { - this.selectedNode.clients = response['hydra:member']; + let members = response['hydra:member']; + let total = response['hydra:totalItems']; + + if (this.showOnlyDirectClients && node['@id']) { + members = members.filter((client: any) => + client.organizationalUnit?.['@id'] === node['@id'] + ); + total = members.length; } - this.length = response['hydra:totalItems']; + + this.selectedClients.data = members; + if (this.selectedNode) { + this.selectedNode.clients = members; + } + this.length = total; this.arrayClients = this.selectedClients.data; this.hasClients = this.selectedClients.data.length > 0; this.isLoadingClients = false; @@ -455,6 +473,13 @@ export class GroupsComponent implements OnInit, OnDestroy { }); } + onDirectClientsToggleChange(): void { + this.page = 0; + if (this.selectedNode) { + this.fetchClientsForNode(this.selectedNode); + } + } + onPageChange(event: PageEvent): void { this.page = event.pageIndex; this.itemsPerPage = event.pageSize; @@ -796,24 +821,20 @@ export class GroupsComponent implements OnInit, OnDestroy { getStatus(client: Client, node: any): void { if (!client.uuid || !client['@id']) return; - this.syncingClientId = client.uuid; - this.syncStatus = true; + this.syncingClientIds.add(client.uuid); const parentNodeId = client.organizationalUnit?.id || node.id; - console.log('Parent node id:', parentNodeId); this.subscriptions.add( this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe( () => { this.toastr.success('Cliente actualizado correctamente'); - this.syncStatus = false; - this.syncingClientId = null; + this.syncingClientIds.delete(client.uuid); this.refreshData(parentNodeId) }, (error) => { this.toastr.error(error.error['hydra:description'] || 'Error al actualizar el cliente'); - this.syncStatus = false; - this.syncingClientId = null; + this.syncingClientIds.delete(client.uuid); this.refreshData(parentNodeId) } ) @@ -821,6 +842,27 @@ export class GroupsComponent implements OnInit, OnDestroy { } + getStatusForAllClientsInNode(node: any): void { + this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&itemsPerPage=1000`).subscribe({ + next: (response: any) => { + const clients: Client[] = response['hydra:member']; + const validClients = clients.filter(client => client.uuid && client['@id']); + if (validClients.length === 0) return; + + validClients.forEach(client => { + this.syncingClientIds.add(client.uuid); + this.subscriptions.add( + this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe({ + next: () => this.syncingClientIds.delete(client.uuid), + error: () => this.syncingClientIds.delete(client.uuid), + }) + ); + }); + } + }); + } + + isAllSelected() { const numSelected = this.selection.selected.length; const numRows = this.selectedClients.data.length; diff --git a/ogWebconsole/src/app/components/groups/model/model.ts b/ogWebconsole/src/app/components/groups/model/model.ts index d1dac81..f85cc05 100644 --- a/ogWebconsole/src/app/components/groups/model/model.ts +++ b/ogWebconsole/src/app/components/groups/model/model.ts @@ -47,6 +47,7 @@ export interface Client { createdBy: string; uuid: string; parentName?: string; + notes?: string; } export interface ClientCollection { diff --git a/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.css b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.css index ffbf8b8..abd568e 100644 --- a/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.css +++ b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.css @@ -86,6 +86,21 @@ table { box-shadow: none; } +.notes-card { + background-color: #f1f1f1; + padding: 24px; + border-radius: 12px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} + +.notes-value { + font-size: 1rem; + color: #222; + white-space: pre-wrap; + word-break: break-word; + margin-top: 8px; +} + .table-header-container { display: flex; align-items: center; diff --git a/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.html b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.html index 808ba25..d6e94c7 100644 --- a/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.html +++ b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.html @@ -28,6 +28,11 @@
+
+
{{ 'notesLabel' | translate }}
+
{{ clientNotes || '--' }}
+
+

Discos/Particiones

@if (clientData.firmwareType) { diff --git a/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.ts b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.ts index 57e8fe2..055ec00 100644 --- a/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.ts @@ -125,6 +125,10 @@ export class ClientDetailsComponent { ]; } + get clientNotes(): string { + return this.clientData?.notes || ''; + } + calculateDiskUsage() { const diskUsageMap = new Map(); diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css index e67f4b7..3ae50b9 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css @@ -44,6 +44,10 @@ mat-option .unit-path { row-gap: 20px; } +.full-width { + grid-column: 1 / -1; +} + .action-container { display: flex; justify-content: flex-end; diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html index 7257e6b..75febb5 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html @@ -106,6 +106,12 @@ {{ 'maintenance' | translate }} + + + {{ 'notesLabel' | translate }} + +
diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.ts b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.ts index 69b13d9..7254741 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.ts @@ -88,7 +88,8 @@ export class ManageClientComponent implements OnInit { repository: [null], menu: [null], maintenance: [false], - resolution: [null] + resolution: [null], + notes: [''] }); } @@ -250,7 +251,8 @@ export class ManageClientComponent implements OnInit { pxeTemplate: data.pxeTemplate ? data.pxeTemplate['@id'] : null, menu: data.menu ? data.menu['@id'] : null, maintenance: data.maintenance, - resolution: data.resolution + resolution: data.resolution, + notes: data.notes || '' }); resolve(); }, diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts index 7d573b9..8576dda 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts @@ -1,10 +1,11 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { DataService } from "../../../services/data.service"; import { ToastrService } from "ngx-toastr"; import { ConfigService } from '@services/config.service'; +import { DeleteModalComponent } from '../../../../../shared/delete_modal/delete-modal/delete-modal.component'; @Component({ selector: 'app-manage-organizational-unit', @@ -59,6 +60,7 @@ export class ManageOrganizationalUnitComponent implements OnInit { private dataService: DataService, private configService: ConfigService, private toastService: ToastrService, + private dialog: MatDialog, @Inject(MAT_DIALOG_DATA) public data: any ) { this.isEditMode = !!data?.uuid; @@ -270,71 +272,113 @@ export class ManageOrganizationalUnitComponent implements OnInit { onSubmit() { if (this.generalFormGroup.valid && this.additionalInfoFormGroup.valid && this.networkSettingsFormGroup.valid) { - const parentValue = this.generalFormGroup.value.parent; - - // Preparar networkSettings con lógica condicional para p2pTime - const networkSettings = { ...this.networkSettingsFormGroup.value }; - if (networkSettings.p2pMode !== 'seeder') { - networkSettings.p2pTime = null; - } - - const formData = { - name: this.generalFormGroup.value.name, - excludeParentChanges: this.generalFormGroup.value.excludeParentChanges, - parent: parentValue || null, - description: this.generalFormGroup.value.description, - comments: this.additionalInfoFormGroup.value.comments, - remoteCalendar: this.generalFormGroup.value.remoteCalendar, - type: this.generalFormGroup.value.type, - networkSettings: networkSettings, - location: this.classroomInfoFormGroup.value.location, - projector: this.classroomInfoFormGroup.value.projector, - board: this.classroomInfoFormGroup.value.board, - capacity: this.classroomInfoFormGroup.value.capacity, - }; - if (this.isEditMode) { - const putUrl = `${this.baseUrl}/organizational-units/${this.data.uuid}`; - const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); + const confirmRef = this.dialog.open(DeleteModalComponent, { + width: '400px', + data: { + title: 'Propagar parámetros', + message: '¿Desea propagar los parámetros de red (ogLive, repositorio, menú, plantilla PXE, perfil hardware) a todos los clientes de esta unidad organizativa?', + confirmText: 'Sí, propagar', + cancelText: 'No, solo guardar', + } + }); - this.http.put(putUrl, formData, { headers }).subscribe( - response => { + confirmRef.afterClosed().subscribe((propagate: boolean) => { + this.submitForm(propagate === true); + }); + } else { + this.submitForm(false); + } + } + } + + private submitForm(propagate: boolean) { + const parentValue = this.generalFormGroup.value.parent; + + const networkSettings = { ...this.networkSettingsFormGroup.value }; + if (networkSettings.p2pMode !== 'seeder') { + networkSettings.p2pTime = null; + } + + const formData = { + name: this.generalFormGroup.value.name, + excludeParentChanges: this.generalFormGroup.value.excludeParentChanges, + parent: parentValue || null, + description: this.generalFormGroup.value.description, + comments: this.additionalInfoFormGroup.value.comments, + remoteCalendar: this.generalFormGroup.value.remoteCalendar, + type: this.generalFormGroup.value.type, + networkSettings: networkSettings, + location: this.classroomInfoFormGroup.value.location, + projector: this.classroomInfoFormGroup.value.projector, + board: this.classroomInfoFormGroup.value.board, + capacity: this.classroomInfoFormGroup.value.capacity, + }; + + if (this.isEditMode) { + const putUrl = `${this.baseUrl}/organizational-units/${this.data.uuid}`; + const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); + + this.http.put(putUrl, formData, { headers }).subscribe( + response => { + if (propagate) { + this.propagateNetworkSettings(); + } else { this.unitAdded.emit(); this.dialogRef.close({ success: true }); this.toastService.success('Editado exitosamente', 'Éxito'); - }, - error => { - console.error('Error al realizar PUT:', error); - const errorMessages = error.error['hydra:description'].split('\n'); - errorMessages.forEach((message: string) => { - const cleanedMessage = message.replace(/networkSettings\.(\w+):/, 'Error $1:'); - this.toastService.error(cleanedMessage); - }); } - ); - } else { - const postUrl = `${this.baseUrl}/organizational-units`; - const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); + }, + error => { + console.error('Error al realizar PUT:', error); + const errorMessages = error.error['hydra:description'].split('\n'); + errorMessages.forEach((message: string) => { + const cleanedMessage = message.replace(/networkSettings\.(\w+):/, 'Error $1:'); + this.toastService.error(cleanedMessage); + }); + } + ); + } else { + const postUrl = `${this.baseUrl}/organizational-units`; + const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); - this.http.post(postUrl, formData, { headers }).subscribe( - response => { - this.unitAdded.emit(response); - this.dialogRef.close({ success: true }); - this.toastService.success('Creado exitosamente', 'Éxito'); - }, - error => { - console.error('Error al realizar POST:', error); - const errorMessages = error.error['hydra:description'].split('\n'); - errorMessages.forEach((message: string) => { - const cleanedMessage = message.replace(/networkSettings\.(\w+):/, 'Error $1:'); - this.toastService.error(cleanedMessage); - }); - } - ); - } + this.http.post(postUrl, formData, { headers }).subscribe( + response => { + this.unitAdded.emit(response); + this.dialogRef.close({ success: true }); + this.toastService.success('Creado exitosamente', 'Éxito'); + }, + error => { + console.error('Error al realizar POST:', error); + const errorMessages = error.error['hydra:description'].split('\n'); + errorMessages.forEach((message: string) => { + const cleanedMessage = message.replace(/networkSettings\.(\w+):/, 'Error $1:'); + this.toastService.error(cleanedMessage); + }); + } + ); } } + private propagateNetworkSettings() { + const propagateUrl = `${this.baseUrl}/organizational-units/${this.data.uuid}/propagate-network-settings`; + const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); + + this.http.post(propagateUrl, {}, { headers }).subscribe( + () => { + this.unitAdded.emit(); + this.dialogRef.close({ success: true }); + this.toastService.success('Editado y parámetros propagados exitosamente', 'Éxito'); + }, + error => { + console.error('Error al propagar parámetros:', error); + this.unitAdded.emit(); + this.dialogRef.close({ success: true }); + this.toastService.warning('Guardado, pero hubo un error al propagar los parámetros', 'Aviso'); + } + ); + } + getSelectedParentName(): string | undefined { const parentId = this.generalFormGroup.get('parent')?.value; return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name; diff --git a/ogWebconsole/src/app/components/ogdhcp/server-info-dialog/server-info-dialog.component.css b/ogWebconsole/src/app/components/ogdhcp/server-info-dialog/server-info-dialog.component.css index e69de29..2f1d175 100644 --- a/ogWebconsole/src/app/components/ogdhcp/server-info-dialog/server-info-dialog.component.css +++ b/ogWebconsole/src/app/components/ogdhcp/server-info-dialog/server-info-dialog.component.css @@ -0,0 +1,14 @@ +::ng-deep .mat-mdc-dialog-container { + max-height: 80vh; +} + +::ng-deep .mat-mdc-dialog-content { + max-height: 65vh !important; + overflow-y: auto !important; +} + +.info-content { + margin: 0; + white-space: pre-wrap; + word-break: break-word; +} diff --git a/ogWebconsole/src/app/components/ogdhcp/server-info-dialog/server-info-dialog.component.html b/ogWebconsole/src/app/components/ogdhcp/server-info-dialog/server-info-dialog.component.html index 2a71554..f176224 100644 --- a/ogWebconsole/src/app/components/ogdhcp/server-info-dialog/server-info-dialog.component.html +++ b/ogWebconsole/src/app/components/ogdhcp/server-info-dialog/server-info-dialog.component.html @@ -1,6 +1,6 @@

Información

-
{{ data | json }}
+
{{ data | json }}
diff --git a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html index cf7485c..ab803b9 100644 --- a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html +++ b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html @@ -8,7 +8,7 @@
- + @@ -60,6 +60,12 @@ {{ image.partitionInfo?.osName }}
+ +
{{ formatSize(image.datasize) }}
+
+ Comprimido: {{ formatSize(image.size) }} +
+
{{ image.image?.isGlobal ? 'Sí' : 'No' }} @@ -78,7 +84,7 @@ + *ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status' && column.columnDef !== 'isGlobal' && column.columnDef !== 'name' && column.columnDef !== 'datasize'"> {{ column.cell(image) }}