diff --git a/.gitignore b/.gitignore index ac68a18..a601f62 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ debian/*.substvars debian/*.log debian/.debhelper/ debian/files +.venv/ +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2893fdd..2b9ed3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,27 @@ # Changelog +## [1.2.5] - 2026-03-27 +### Fixed +- Corregido error 401 Unauthorized en la edición de menús con nombres que contienen la palabra "login". Se ha refinado el interceptor para aplicar el prefijo "Bearer" de forma selectiva mediante validación exacta del path `/auth/login`. + +## [1.2.4] - 2026-03-20 +### Added +- Se ha mejorado el editor de menus para poder editar directamente la plantilla twig en lugar del html renderizado. + +## [1.2.3] - 2026-03-17 +### Added +- Se ha creado una nueva opción para gestionar assets en la pantalla de menus. + +## [1.2.2] - 2026-03-17 +### Added +- Se ha integrado un editor interactivo en texto plano para los menús HTML directamente en la pantalla de edición, permitiendo previsualizar y modificar el código en vivo sin necesidad de resubir archivos. Utiliza la sintaxis moderna `@if/@else` de Angular 17+ para su lógica de renderizado. + +## [1.2.1] - 2026-03-11 +### Added +- Se ha añadido la funcionalidad para poder adjuntar un fichero HTML local en la creación/edición de menús, codificándolo y enviándolo en Base64 de forma transparente. + +### Improved +- Se ha mejorado la UX integrando el campo de subida de ficheros en el formulario de menús (`create-menu`) utilizando la estética nativa de Angular Material. + ## [1.2.0] - 2025-10-21 ### Added - Se ha añadido una nueva funcionalidad la cual permite poder borrar varios equipos a la vez. diff --git a/debian/oggui.postinst b/debian/oggui.postinst index 0346f22..996fe05 100644 --- a/debian/oggui.postinst +++ b/debian/oggui.postinst @@ -9,6 +9,8 @@ db_get opengnsys/oggui_ogcoreUrl OGCORE_URL="$RET" db_get opengnsys/oggui_ogmercureUrl OGMERCURE_URL="$RET" +db_get opengnsys/oggui_oglogUrl +OGLOG_URL="$RET" # Asegurarse de que el usuario exista USER="opengnsys" @@ -35,7 +37,7 @@ restore_config_if_modified() { # Detectar si es una instalación nueva o una actualización if [ "$1" = "configure" ] && [ -z "$2" ]; then - jq --arg apiUrl "$OGCORE_URL" --arg mercureUrl "$OGMERCURE_URL" '.apiUrl = $apiUrl | .mercureUrl = $mercureUrl' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE" + jq --arg apiUrl "$OGCORE_URL" --arg mercureUrl "$OGMERCURE_URL" --arg logUrl "$OGLOG_URL" '.apiUrl = $apiUrl | .mercureUrl = $mercureUrl | .logUrl = $logUrl' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE" ln -s /opt/opengnsys/oggui/etc/nginx/oggui.conf /etc/nginx/sites-enabled/oggui.conf ln -s $CONFIG_FILE /opt/opengnsys/oggui/etc/config.json mkdir -p /etc/nginx/certs/ diff --git a/debian/oggui.templates b/debian/oggui.templates index dc0d172..21f5cd0 100644 --- a/debian/oggui.templates +++ b/debian/oggui.templates @@ -7,3 +7,8 @@ Template: opengnsys/oggui_ogmercureUrl Type: string Default: https://127.0.0.1:3000/.well-known/mercure Description: Introduzca el endpoint de mercure + +Template: opengnsys/oggui_oglogUrl +Type: string +Default: https://127.0.0.1:3030/ +Description: Oglog Grafana URL \ No newline at end of file diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index ab4e07b..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'; @@ -114,6 +131,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component'; import { MatSortModule } from '@angular/material/sort'; import { MenusComponent } from './components/menus/menus.component'; +import { AssetManagerModalComponent } from './components/menus/asset-manager-modal/asset-manager-modal.component'; import { CreateMenuComponent } from './components/menus/create-menu/create-menu.component'; import { CreateMultipleClientComponent } from './components/groups/shared/clients/create-multiple-client/create-multiple-client.component'; import { ExportImageComponent } from './components/images/export-image/export-image.component'; @@ -171,7 +189,6 @@ import { HardwareProfileComponent } from './components/hardware-profile/hardware import { HardwareCollectionModalComponent } from './components/hardware-profile/hardware-collection-modal/hardware-collection-modal.component'; import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component'; import { ShowDetailsComponent } from './components/hardware-type/show-details/show-details.component'; - export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './locale/', '.json'); } @@ -249,6 +266,7 @@ registerLocaleData(localeEs, 'es-ES'); ExecuteCommandComponent, ExecuteCommandOuComponent, DeployImageComponent, + DeployMethodConfigDialogComponent, MainRepositoryViewComponent, EnvVarsComponent, MenusComponent, @@ -298,7 +316,8 @@ registerLocaleData(localeEs, 'es-ES'); HardwareProfileComponent, HardwareCollectionModalComponent, HardwareTypeComponent, - ShowDetailsComponent + ShowDetailsComponent, + AssetManagerModalComponent ], bootstrap: [AppComponent], imports: [BrowserModule, @@ -359,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/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..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 @@
-
+
+ + + + + + +
+ + + @if (isUploading) { + + } + + +
+ @if (isLoading) { +
Cargando...
+ } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tipo + + {{ element.type === 'folder' ? 'folder' : 'insert_drive_file' }} + + Nombre + @if (element.type === 'folder') { + + {{ element.name }} + + } @else { + {{ element.name }} + } + Tamaño + {{ element.type === 'file' ? formatBytes(element.size) : '--' }} + Acciones + @if (element.type === 'file') { + + } + +
No hay archivos en este directorio
+
+ + + + + diff --git a/ogWebconsole/src/app/components/menus/asset-manager-modal/asset-manager-modal.component.spec.ts b/ogWebconsole/src/app/components/menus/asset-manager-modal/asset-manager-modal.component.spec.ts new file mode 100644 index 0000000..997137e --- /dev/null +++ b/ogWebconsole/src/app/components/menus/asset-manager-modal/asset-manager-modal.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AssetManagerModalComponent } from './asset-manager-modal.component'; + +describe('AssetManagerModalComponent', () => { + let component: AssetManagerModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AssetManagerModalComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AssetManagerModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ogWebconsole/src/app/components/menus/asset-manager-modal/asset-manager-modal.component.ts b/ogWebconsole/src/app/components/menus/asset-manager-modal/asset-manager-modal.component.ts new file mode 100644 index 0000000..b95eeeb --- /dev/null +++ b/ogWebconsole/src/app/components/menus/asset-manager-modal/asset-manager-modal.component.ts @@ -0,0 +1,143 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialogRef, MatDialog } from '@angular/material/dialog'; +import { ToastrService } from 'ngx-toastr'; +import { DataService } from '../data.service'; +import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component'; + +export interface AssetNode { + name: string; + type: 'folder' | 'file'; + size: number | null; + path: string; +} + +@Component({ + selector: 'app-asset-manager-modal', + templateUrl: './asset-manager-modal.component.html', + styleUrls: ['./asset-manager-modal.component.css'] +}) +export class AssetManagerModalComponent implements OnInit { + currentPath: string = ''; + assets: AssetNode[] = []; + isLoading: boolean = false; + isUploading: boolean = false; + displayedColumns: string[] = ['icon', 'name', 'size', 'actions']; + + constructor( + public dialogRef: MatDialogRef, + private dialog: MatDialog, + private dataService: DataService, + private toastService: ToastrService + ) { } + + ngOnInit(): void { + this.loadAssets(); + } + + loadAssets(): void { + this.isLoading = true; + this.dataService.getAssets(this.currentPath).subscribe({ + next: (data) => { + this.assets = data; + this.isLoading = false; + }, + error: (err) => { + this.isLoading = false; + this.toastService.error('Error al cargar la ruta actual.'); + console.error(err); + + // If we failed traversing down, fallback to root + if (this.currentPath !== '') { + this.currentPath = ''; + this.loadAssets(); + } + } + }); + } + + enterFolder(folderName: string): void { + if (this.currentPath === '') { + this.currentPath = folderName; + } else { + this.currentPath = this.currentPath + '/' + folderName; + } + this.loadAssets(); + } + + goUp(): void { + if (this.currentPath === '') return; + + const parts = this.currentPath.split('/'); + parts.pop(); + this.currentPath = parts.join('/'); + this.loadAssets(); + } + + onFileSelected(event: any): void { + const file = event.target.files[0]; + if (file) { + const isZip = file.name.toLowerCase().endsWith('.zip'); + this.isUploading = true; + + this.dataService.uploadAssets(file, this.currentPath, isZip).subscribe({ + next: (response) => { + this.isUploading = false; + this.toastService.success(response.message || 'Fichero subido correctamente.'); + this.loadAssets(); // Refresh directory + }, + error: (error) => { + this.isUploading = false; + this.toastService.error(error?.error?.message || 'Error al subir el fichero.'); + console.error(error); + } + }); + } + + // Reset input so it triggers (change) next time even if it's the same file name + event.target.value = ''; + } + + deleteAsset(path: string): void { + this.dialog.open(DeleteModalComponent, { + width: '300px', + data: { name: path } + }).afterClosed().subscribe((result) => { + if (result) { + this.isLoading = true; + this.dataService.deleteAsset(path).subscribe({ + next: (res) => { + this.toastService.success('Elemento eliminado correctamente.'); + this.loadAssets(); + }, + error: (err) => { + this.isLoading = false; + this.toastService.error(err?.error?.message || 'No se pudo eliminar el elemento.'); + } + }); + } + }); + } + + copyPath(path: string): void { + // Generate an absolute-like path starting with /assets/ + const fullUrl = `/assets/${path}`; + navigator.clipboard.writeText(fullUrl).then(() => { + this.toastService.info(`Enlace copiado: ${fullUrl}`); + }).catch(err => { + console.error('Error al copiar al portapapeles', err); + }); + } + + formatBytes(bytes: number | null): string { + if (bytes === null || bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = 2; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + } + + close(): void { + this.dialogRef.close(); + } +} diff --git a/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.css b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.css index 5efc52e..c582433 100644 --- a/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.css +++ b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.css @@ -26,9 +26,10 @@ .iframe-container { flex: 1; display: flex; - justify-content: center; - align-items: flex-start; + flex-direction: column; + align-items: flex-end; max-width: 50%; + gap: 10px; } .iframe { @@ -39,6 +40,26 @@ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); } +.editor-controls { + display: flex; + gap: 10px; + width: 100%; + justify-content: flex-end; +} + +.html-editor { + width: 100%; + height: 600px; + font-family: 'Courier New', Courier, monospace; + font-size: 13px; + border: 1px solid #ddd; + border-radius: 8px; + padding: 10px; + resize: vertical; + background-color: #fcfcfc; + box-shadow: inset 0px 2px 5px rgba(0, 0, 0, 0.05); +} + .action-container { display: flex; justify-content: flex-end; diff --git a/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.html b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.html index d1ddfa3..db0d93c 100644 --- a/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.html +++ b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.html @@ -15,13 +15,21 @@ + + Suba el fichero que se usará como menú + + + + Resolución 640×480 (8-bit - 256 colores) 800×600 (8-bit - 256 colores) 1024×768 (8-bit - 256 colores) - 1280×1024 (8-bit - 256 colores) + 1280×1024 (8-bit - 256 colores) 640×480 (16-bit - 65k colores) 800×600 (16-bit - 65k colores) 1024×768 (16-bit - 65k colores) @@ -41,12 +49,38 @@ {{ 'defaultMenuLabel' | translate }} + - -
- -
+ + @if (safeUrl || isEditingHtml) { +
+ + +
+ @if (isEditingHtml) { + + + } @else if (safeUrl) { + + } +
+ + + @if (isEditingHtml) { + + } @else if (safeUrl) { + + } + +
+ }
diff --git a/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.ts b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.ts index b4dc91d..bf04d47 100644 --- a/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.ts +++ b/ogWebconsole/src/app/components/menus/create-menu/create-menu.component.ts @@ -18,6 +18,10 @@ export class CreateMenuComponent implements OnInit { menuId: string | null = null; softwareProfiles: any[] = []; safeUrl: SafeResourceUrl | null = null; + selectedFile: File | null = null; + fileContent: string | null = null; + isEditingHtml: boolean = false; + htmlEditorContent: string = ''; constructor( private fb: FormBuilder, @@ -48,6 +52,100 @@ export class CreateMenuComponent implements OnInit { } } + onFileSelected(event: any): void { + const file = event.target.files[0]; + if (file) { + this.selectedFile = file; + + const reader = new FileReader(); + reader.onload = (e: any) => { + // e.target.result será un string en formato 'data:text/html;base64,.....' + // Nos quedamos solo con la parte Base64 dividiendo por la coma + const base64String = e.target.result.split(',')[1]; + this.fileContent = base64String; + }; + reader.readAsDataURL(file); + } else { + this.selectedFile = null; + this.fileContent = null; + } + } + + editHtml(): void { + if (this.fileContent) { + // Decode Base64 content to text + try { + // Modern approach to decode Base64 ensuring UTF-8 + const binaryStr = window.atob(this.fileContent); + const bytes = new Uint8Array(binaryStr.length); + for (let i = 0; i < binaryStr.length; i++) { + bytes[i] = binaryStr.charCodeAt(i); + } + this.htmlEditorContent = new TextDecoder('utf-8').decode(bytes); + } catch (e) { + console.error('Failed to decode previous fileContent Base64', e); + this.htmlEditorContent = ''; + } + this.isEditingHtml = true; + } else if (this.menuForm.get('publicUrl')?.value) { + // Fetch the actual raw HTML/Twig template if it exists and no file was manually uploaded yet + const publicUrl = this.menuForm.get('publicUrl')?.value; + this.dataService.getRawMenuTemplate(publicUrl).subscribe({ + next: (htmlStr) => { + this.htmlEditorContent = htmlStr; + this.isEditingHtml = true; + }, + error: (err) => { + this.toastService.error('No se pudo descargar el HTML/Twig original del menú para editarlo.'); + console.error(err); + } + }); + } + } + + applyHtmlChanges(): void { + if (this.htmlEditorContent) { + // Encode the drafted Twig code back into Base64 enforcing UTF-8 compatibility + try { + const bytes = new TextEncoder().encode(this.htmlEditorContent); + const binaryStr = Array.from(bytes).map(byte => String.fromCharCode(byte)).join(''); + const base64Content = window.btoa(binaryStr); + + // This holds the RAW twig that will be sent via PUT/POST on save + this.fileContent = base64Content; + + // Ask the backend to compile a preview using this encoded Twig + this.dataService.previewMenuTemplate(base64Content).subscribe({ + next: (renderedHtml) => { + // Now encode the definitively rendered HTML to Base64 to populate the iframe preview + const renderedBytes = new TextEncoder().encode(renderedHtml); + const renderedBinaryStr = Array.from(renderedBytes).map(byte => String.fromCharCode(byte)).join(''); + const renderedBase64 = window.btoa(renderedBinaryStr); + + this.isEditingHtml = false; + const dataUrl = `data:text/html;base64,${renderedBase64}`; + this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(dataUrl); + this.toastService.success('Vista previa generada. Pulsa Guardar para aplicar la plantilla remota permanentemente.'); + }, + error: (err) => { + console.error('Failed to preview generated HTML', err); + const msg = err.error?.message || 'Error compilando la plantilla Twig.'; + this.toastService.error(msg); + } + }); + + } catch (e) { + console.error('Failed to encode updated HTML to Base64', e); + this.toastService.error('Hubo un problema procesando los caracteres especiales del HTML.'); + } + } + } + + cancelHtmlChanges(): void { + this.isEditingHtml = false; + this.htmlEditorContent = ''; + } + ngOnInit() { if (this.data) { this.load() @@ -74,7 +172,7 @@ export class CreateMenuComponent implements OnInit { } save(): void { - const payload = { + const payload: any = { name: this.menuForm.value.name, publicUrl: this.menuForm.value.publicUrl, resolution: this.menuForm.value.resolution, @@ -82,6 +180,10 @@ export class CreateMenuComponent implements OnInit { isDefault: this.menuForm.value.isDefault, }; + if (this.fileContent) { + payload.menuHtmlContent = this.fileContent; + } + if (this.menuId) { this.http.put(`${this.baseUrl}${this.menuId}`, payload).subscribe( (response) => { diff --git a/ogWebconsole/src/app/components/menus/data.service.ts b/ogWebconsole/src/app/components/menus/data.service.ts index 354afb5..b61dfe0 100644 --- a/ogWebconsole/src/app/components/menus/data.service.ts +++ b/ogWebconsole/src/app/components/menus/data.service.ts @@ -16,6 +16,26 @@ export class DataService { this.apiUrl = `${this.baseUrl}/menus?page=1&itemsPerPage=1000`; } + getRawMenuTemplate(publicUrl: string): Observable { + const url = `${this.baseUrl}/menu-raw/${publicUrl}`; + return this.http.get(url, { responseType: 'text' }).pipe( + catchError(error => { + console.error('Error fetching raw menu template', error); + return throwError(() => error); + }) + ); + } + + previewMenuTemplate(base64Template: string): Observable { + const url = `${this.baseUrl}/menu-preview`; + return this.http.post(url, { templateBase64: base64Template }, { responseType: 'text' }).pipe( + catchError(error => { + console.error('Error previewing menu template', error); + return throwError(() => error); + }) + ); + } + getMenus(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> { const params = new HttpParams({ fromObject: filters }); @@ -32,7 +52,7 @@ export class DataService { }), catchError(error => { console.error('Error fetching commands', error); - return throwError(error); + return throwError(() => error); }) ); } @@ -48,10 +68,44 @@ export class DataService { }), catchError(error => { console.error('Error fetching menus', error); - return throwError(error); + return throwError(() => error); }) ); } + getAssets(path: string = ''): Observable { + const params = new HttpParams().set('path', path); + return this.http.get(`${this.baseUrl}/ogassets/list`, { params }).pipe( + catchError(error => { + console.error('Error fetching assets', error); + return throwError(() => error); + }) + ); + } + + uploadAssets(file: File, path: string = '', extractZip: boolean = false): Observable { + const formData = new FormData(); + formData.append('file', file); + formData.append('path', path); + formData.append('extractZip', String(extractZip)); + + return this.http.post(`${this.baseUrl}/ogassets/upload`, formData).pipe( + catchError(error => { + console.error('Error uploading assets', error); + return throwError(() => error); + }) + ); + } + + deleteAsset(path: string): Observable { + return this.http.delete(`${this.baseUrl}/ogassets/delete`, { + body: { path } + }).pipe( + catchError(error => { + console.error('Error deleting asset', error); + return throwError(() => error); + }) + ); + } } diff --git a/ogWebconsole/src/app/components/menus/menus.component.html b/ogWebconsole/src/app/components/menus/menus.component.html index 12ecb01..7864952 100644 --- a/ogWebconsole/src/app/components/menus/menus.component.html +++ b/ogWebconsole/src/app/components/menus/menus.component.html @@ -7,6 +7,7 @@ Administrar menús
+
diff --git a/ogWebconsole/src/app/components/menus/menus.component.ts b/ogWebconsole/src/app/components/menus/menus.component.ts index ebb542a..a223f48 100644 --- a/ogWebconsole/src/app/components/menus/menus.component.ts +++ b/ogWebconsole/src/app/components/menus/menus.component.ts @@ -8,6 +8,7 @@ import { JoyrideService } from "ngx-joyride"; import { Router } from "@angular/router"; import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component"; import { CreateMenuComponent } from "./create-menu/create-menu.component"; +import { AssetManagerModalComponent } from "./asset-manager-modal/asset-manager-modal.component"; import { ConfigService } from '@services/config.service'; @Component({ @@ -85,6 +86,17 @@ export class MenusComponent implements OnInit { }); } + openAssetManager(): void { + const dialogRef = this.dialog.open(AssetManagerModalComponent, { + width: '800px', + height: '600px' + }); + + dialogRef.afterClosed().subscribe(() => { + // Nothing specific to do upon close for now + }); + } + search(): void { this.loading = true; this.http.get(`${this.apiUrl}?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe( 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/core/services/custom.interceptor.ts b/ogWebconsole/src/app/core/services/custom.interceptor.ts index 319a1e4..69768b2 100644 --- a/ogWebconsole/src/app/core/services/custom.interceptor.ts +++ b/ogWebconsole/src/app/core/services/custom.interceptor.ts @@ -12,7 +12,18 @@ export class CustomInterceptor implements HttpInterceptor { const token = localStorage.getItem('loginToken'); let newCloneRequest; - if (req.url.includes('login')) { + // Usamos el objeto URL para ser quirúrgicos y detectar si el PATH es exactamente "/auth/login": + let isLoginPath = false; + try { + // req.url puede ser relativa o absoluta + const url = new URL(req.url, window.location.origin); + isLoginPath = url.pathname === '/auth/login'; + } catch (e) { + // Fallback por si la URL es inválida, aunque no debería ocurrir + isLoginPath = req.url.includes('/auth/login') && !req.url.includes('menu-raw'); + } + + if (isLoginPath) { newCloneRequest = req.clone({ setHeaders: { Authorization: `${token}` 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 @@ + +