From 5b7d3c72e297fa2a4b798ec02dff2791ee571ddf Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 26 Mar 2026 18:21:57 +0100 Subject: [PATCH 01/11] refs #2903. Ux improvements --- ogWebconsole/src/app/app.module.ts | 21 +- .../commands-task/commands-task.component.css | 61 +- .../commands-task.component.html | 4 +- .../commands-task/commands-task.component.ts | 3 +- .../create-task-schedule.component.html | 27 +- .../create-task-schedule.component.ts | 28 + .../create-task-script.component.css | 662 ++++++++---------- .../create-task-script.component.html | 269 ++++--- .../create-task-script.component.spec.ts | 2 + .../create-task-script.component.ts | 324 +++++---- .../create-task/create-task.component.html | 32 +- .../create-task/create-task.component.ts | 52 +- .../detail-task/detail-task.component.html | 58 +- .../show-task-schedule.component.css | 23 +- .../show-task-schedule.component.html | 73 +- .../show-task-schedule.component.ts | 8 + .../show-task-script.component.css | 131 +++- .../show-task-script.component.html | 104 +-- .../show-task-script.component.ts | 35 +- .../create-image/create-image.component.css | 32 +- .../create-image/create-image.component.html | 296 ++++---- .../deploy-image/deploy-image.component.css | 136 ++-- .../deploy-image/deploy-image.component.html | 309 ++++---- .../deploy-image/deploy-image.component.ts | 95 ++- .../deploy-method-config-dialog.component.css | 51 ++ ...deploy-method-config-dialog.component.html | 67 ++ .../deploy-method-config-dialog.component.ts | 93 +++ .../partition-assistant.component.css | 307 ++++---- .../partition-assistant.component.html | 135 ++-- .../components/groups/groups.component.css | 52 +- .../components/groups/groups.component.html | 12 +- .../task-logs/task-logs.component.css | 94 ++- .../task-logs/task-logs.component.html | 15 +- .../task-logs/task-logs.component.ts | 26 +- .../app/layout/header/header.component.css | 18 +- .../app/layout/header/header.component.html | 13 +- .../main-layout/main-layout.component.css | 5 +- .../app/layout/sidebar/sidebar.component.html | 7 + ogWebconsole/src/locale/en.json | 93 ++- ogWebconsole/src/locale/es.json | 93 ++- 40 files changed, 2305 insertions(+), 1561 deletions(-) create mode 100644 ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.css create mode 100644 ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.html create mode 100644 ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component.ts diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index 7f5df2f..e2712a2 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'; @@ -249,6 +266,7 @@ registerLocaleData(localeEs, 'es-ES'); ExecuteCommandComponent, ExecuteCommandOuComponent, DeployImageComponent, + DeployMethodConfigDialogComponent, MainRepositoryViewComponent, EnvVarsComponent, MenusComponent, @@ -360,6 +378,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/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 +
- - - -
{{ 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 e2e79a6..e118657 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 1d6cb4e..d2e10dd 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,71 @@ 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 320px; +} + +.partition-select-section { + flex: 1; + min-width: 0; +} + .form-section-title mat-icon { color: #667eea; font-size: 24px; @@ -93,14 +127,6 @@ height: 24px; } -/* Selectores */ -.selector { - display: flex; - gap: 20px; - margin-bottom: 20px; - flex-wrap: wrap; -} - .half-width { flex: 1; min-width: 250px; @@ -276,9 +302,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 +322,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 +618,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 4dbb9f9..bd1c343 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 @@ -19,15 +19,9 @@ -
- -
- -
- -
- - -
- -
-
- image - Configuración de imagen -
- -
- +
+
+ +
+
+ image + Configuración de imagen +
+ + Tipo de imagen Monolítica Git + + +
+ + Método de deploy + + {{ method.name }} + + + + Seleccione imagen + + +
{{ image.name }}
+
{{ image.description }}
+
+
+
+
+ + +
+ + Repositorio + + + {{ repo }} + + + hourglass_empty + + + + Rama + + + {{ branch }} + + + hourglass_empty + + + + Filtrar por tag + + No + Sí + + +
+ +
+ {{ errorMessage }} +
+ +
+
+ tune + {{ getMethodConfigSummary() }} +
+ +
- -
- - Seleccionar Repositorio - - - {{ repo }} - - - hourglass_empty - + +
+
+ storage + Selección de partición +
+ +
+
+ + + Instrucciones generadas + + + +
{{ ogInstructions }}
+
+
+
- - Seleccionar Rama - - - {{ branch }} - - - hourglass_empty - + + + + + - - Tag - - No - Sí - - + + + + + + + +
Seleccionar + + + + + {{ column.header }} + {{ column.cell(image) }} +
+
+
+
+ + +
+
+ history + Selección de commit
-
+
@@ -188,144 +267,8 @@
Seleccionar
- -
- - Seleccione método de deploy - - {{ method.name }} - - - - Seleccione imagen - - -
{{ image.name }}
-
{{ image.description }}
-
-
-
-
- -
- {{ errorMessage }} -
- -
-
- storage - Selección de partición -
- -
-
- - - Instrucciones generadas - - - -
{{ ogInstructions }}
-
-
-
- - - - - - - - - - - - - - -
Seleccionar partición - - - - - {{ column.header }} - {{ column.cell(image) }} -
-
-
- - -
-
- 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) - - -
-
{ - this.scrollToPartitionSection(); - }, 200); } get runScriptTitle(): string { @@ -178,9 +177,6 @@ export class DeployImageComponent implements OnInit{ onModelSelected(client: any) { this.loadPartitions(client); - setTimeout(() => { - this.scrollToPartitionSection(); - }, 200); } toggleClientSelection(client: any) { @@ -270,22 +266,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 { if (!this.selectedClients.length) { @@ -519,28 +567,7 @@ export class DeployImageComponent implements OnInit{ this.ogInstructions = `og-deploy-image --image ${this.selectedImage.name} --partition ${this.selectedPartition.partitionNumber} --method ${this.selectedMethod}`; } - scrollToPartitionSection() { - const partitionSection = document.getElementById('partition-selection'); - if (partitionSection) { - partitionSection.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - console.log('Scroll ejecutado'); - } - } - scrollToAdvancedOptions() { - const advancedOptions = document.getElementById('advanced-options'); - - if (advancedOptions) { - advancedOptions.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - console.log('Scroll hacia opciones avanzadas ejecutado'); - } - } get selectedPartition(): any { return this._selectedPartition; 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/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index 6075acd..5d5aa1c 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,24 @@ mat-tree mat-tree-node.disabled:hover { box-sizing: border-box; } +::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 +984,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..efa0798 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -15,7 +15,7 @@
-
+
- {{ '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..c7927d9 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; diff --git a/ogWebconsole/src/app/layout/header/header.component.html b/ogWebconsole/src/app/layout/header/header.component.html index 353e3da..df21d8c 100644 --- a/ogWebconsole/src/app/layout/header/header.component.html +++ b/ogWebconsole/src/app/layout/header/header.component.html @@ -1,13 +1,14 @@ + +