From a1c2fb7c2e100b491fc123deff34965653a0a5c1 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 26 Jun 2025 15:45:37 +0200 Subject: [PATCH] refs #1984. Integration ogGit. Crete and deploy Image. Show git images in repository --- ogWebconsole/src/app/app.module.ts | 19 +- .../create-image/create-image.component.css | 708 ++++++++++++++++-- .../create-image/create-image.component.html | 250 +++++-- .../create-image/create-image.component.ts | 311 +++++++- .../create-repository-modal.component.css | 40 + .../create-repository-modal.component.html | 17 + .../create-repository-modal.component.ts | 64 ++ .../deploy-image/deploy-image.component.css | 551 +++++++++++--- .../deploy-image/deploy-image.component.html | 269 ++++--- .../deploy-image/deploy-image.component.ts | 332 +++++--- .../repositories/repositories.component.html | 4 +- .../repositories/repositories.component.ts | 4 +- .../show-git-images.component.css | 93 +++ .../show-git-images.component.html | 138 ++-- .../show-git-images.component.spec.ts | 12 +- .../show-git-images.component.ts | 405 ++++------ ogWebconsole/src/locale/en.json | 39 +- ogWebconsole/src/locale/es.json | 42 +- ogWebconsole/src/styles.css | 96 +++ 19 files changed, 2588 insertions(+), 806 deletions(-) create mode 100644 ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.css create mode 100644 ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.html create mode 100644 ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.ts diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index a172a3c..67add43 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -91,6 +91,7 @@ import { MatSliderModule } from '@angular/material/slider'; import { ImagesComponent } from './components/images/images.component'; import { CreateImageComponent } from './components/images/create-image/create-image.component'; import { CreateClientImageComponent } from './components/groups/components/client-main-view/create-image/create-image.component'; +import { CreateRepositoryModalComponent } from './components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component'; import { PartitionAssistantComponent } from './components/groups/components/client-main-view/partition-assistant/partition-assistant.component'; import { SoftwareComponent } from './components/software/software.component'; import { CreateSoftwareComponent } from './components/software/create-software/create-software.component'; @@ -139,7 +140,7 @@ import { SaveScriptComponent } from "./components/groups/components/client-main-view/run-script-assistant/save-script/save-script.component"; import { EditImageComponent } from './components/repositories/edit-image/edit-image.component'; -import { ShowGitImagesComponent } from './components/repositories/show-git-images/show-git-images.component'; +import { ShowGitCommitsComponent } from './components/repositories/show-git-images/show-git-images.component'; import { RenameImageComponent } from './components/repositories/rename-image/rename-image.component'; import { ClientDetailsComponent } from './components/groups/shared/client-details/client-details.component'; import { PartitionTypeOrganizatorComponent } from './components/groups/shared/partition-type-organizator/partition-type-organizator.component'; @@ -154,6 +155,10 @@ import { BootSoPartitionComponent } from './components/commands/main-commands/ex import { RemoveCacheImageComponent } from './components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component'; import { ChangeParentComponent } from './components/groups/shared/change-parent/change-parent.component'; import { SoftwareProfilePartitionComponent } from './components/commands/main-commands/execute-command/software-profile-partition/software-profile-partition.component'; +import { ClientPendingTasksComponent } from './components/task-logs/client-pending-tasks/client-pending-tasks.component'; +import { QueueConfirmationModalComponent } from './shared/queue-confirmation-modal/queue-confirmation-modal.component'; +import { ModalOverlayComponent } from './shared/modal-overlay/modal-overlay.component'; +import { ScrollToTopComponent } from './shared/scroll-to-top/scroll-to-top.component'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './locale/', '.json'); @@ -182,6 +187,7 @@ registerLocaleData(localeEs, 'es-ES'); GroupsComponent, ManageClientComponent, DeleteModalComponent, + QueueConfirmationModalComponent, ClassroomViewComponent, ClientViewComponent, ShowOrganizationalUnitComponent, @@ -204,6 +210,8 @@ registerLocaleData(localeEs, 'es-ES'); CalendarComponent, CreateCalendarComponent, CreateClientImageComponent, + CreateRepositoryModalComponent, + PartitionAssistantComponent, CreateCalendarRuleComponent, CommandsGroupsComponent, CommandsTaskComponent, @@ -216,7 +224,6 @@ registerLocaleData(localeEs, 'es-ES'); StatusComponent, ImagesComponent, CreateImageComponent, - PartitionAssistantComponent, SoftwareComponent, CreateSoftwareComponent, SoftwareProfileComponent, @@ -230,7 +237,6 @@ registerLocaleData(localeEs, 'es-ES'); ExecuteCommandOuComponent, DeployImageComponent, MainRepositoryViewComponent, - ExecuteCommandOuComponent, EnvVarsComponent, MenusComponent, CreateMenuComponent, @@ -251,7 +257,7 @@ registerLocaleData(localeEs, 'es-ES'); RunScriptAssistantComponent, SaveScriptComponent, EditImageComponent, - ShowGitImagesComponent, + ShowGitCommitsComponent, RenameImageComponent, ClientDetailsComponent, PartitionTypeOrganizatorComponent, @@ -265,7 +271,10 @@ registerLocaleData(localeEs, 'es-ES'); BootSoPartitionComponent, RemoveCacheImageComponent, ChangeParentComponent, - SoftwareProfilePartitionComponent + SoftwareProfilePartitionComponent, + ClientPendingTasksComponent, + ModalOverlayComponent, + ScrollToTopComponent ], bootstrap: [AppComponent], imports: [BrowserModule, 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 53d269b..d7c8b69 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 @@ -1,105 +1,677 @@ -.title { - font-size: 24px; -} - -.calendar-button-row { - display: flex; - justify-content: flex-start; - margin-top: 16px; -} - -.divider { - margin: 20px 0; -} - -.lists-container { - padding: 16px; -} - -.card.unidad-card { - height: 100%; - box-sizing: border-box; -} - -table { - width: 100%; - margin-top: 50px; - background-color: #eaeff6; -} - -.search-container { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - padding: 0 5px; - box-sizing: border-box; -} - +/* Contenedor principal modernizado */ .select-container { - gap: 16px; + gap: 24px; width: 100%; box-sizing: border-box; - padding: 20px; + padding: 32px; + background: white !important; + border-radius: 4px; + margin: 20px 0; + align-items: center; +} + +/* 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; +} + +.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; } .selector { display: flex; - gap: 16px; + gap: 20px; width: 100%; - margin-top: 30px; + margin-top: 16px; box-sizing: border-box; + align-items: start; } .half-width { flex: 1; - max-width: 50%; + max-width: calc(50% - 10px); } - -.search-string { - flex: 2; - padding: 5px; -} - -.search-boolean { +.full-width { flex: 1; - padding: 5px; + width: 100%; } +/* Header modernizado */ .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; + padding: 24px 32px; + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; } .header-container-title { flex-grow: 1; text-align: left; - padding-left: 1em; +} + +.header-container-title h2 { + margin: 0 0 8px 0; + color: #333; + font-weight: 600; +} + +/* Estilos modernos para el badge de destino */ +.destination-info { + margin-top: 12px; +} + +.destination-badge { + display: inline-flex; + align-items: center; + background: #e3f2fd; + color: #1565c0; + padding: 12px 16px; + border-radius: 12px; + border: 1px solid #bbdefb; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: all 0.2s ease; +} + +.destination-icon { + font-size: 20px; + width: 20px; + height: 20px; + margin-right: 12px; + color: #1976d2; +} + +.destination-content { + display: flex; + flex-direction: column; + gap: 2px; +} + +.destination-label { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #1976d2; + line-height: 1; +} + +.destination-value { + font-size: 14px; + font-weight: 600; + line-height: 1.2; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #0d47a1; } .button-row { display: flex; - padding-right: 1em; + gap: 12px; + padding-right: 0; } +/* Tabla de particiones modernizada */ .partition-table-container { - background-color: #eaeff6; - padding: 20px; + background: white; + padding: 24px; border-radius: 12px; - margin-top: 20px; + margin-top: 24px; +} + +.partition-table-container h3 { + margin: 0 0 20px 0; + color: #333; + font-weight: 600; + font-size: 18px; +} + +.repository-label { + font-weight: 500; + margin-right: 8px; +} + +mat-chip { + margin-top: 8px !important; + border-radius: 20px !important; +} + +mat-icon { + margin-right: 4px; +} + +/* Botón de crear repositorio modernizado */ +.create-repository-button { + color: white; + border: none; + padding: 12px 20px; + border-radius: 8px; + font-weight: 500; + transition: all 0.3s ease; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; +} + +.create-repository-button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3); +} + +.create-repository-button mat-icon { + font-size: 18px; + width: 18px; + height: 18px; +} + +/* Botones modernizados */ +.action-button { + border-radius: 8px; + font-weight: 500; + padding: 12px 24px; + transition: all 0.3s ease; +} + +.action-button:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +/* Campos de formulario modernizados */ +mat-form-field { + margin-bottom: 8px; +} + +::ng-deep .mat-form-field-appearance-fill .mat-form-field-flex { + border-radius: 8px; + background-color: white !important; + border: 1px solid #e9ecef; + transition: all 0.3s ease; +} + +::ng-deep .mat-form-field-appearance-fill.mat-focused .mat-form-field-flex { + background-color: white; + border-color: #2196f3; + box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2); +} + +/* Overlay de carga para creación de repositorio */ +.creating-repository-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.7); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 9999; + color: white; + backdrop-filter: blur(4px); +} + +.creating-repository-overlay p { + margin-top: 16px; + font-size: 16px; + font-weight: 500; +} + +/* Estilo para hacer el backdrop no clickeable */ +::ng-deep .non-clickable-backdrop { + pointer-events: none !important; +} + +/* Responsive design */ +@media (max-width: 768px) { + .select-container { + padding: 16px; + } + + .header-container { + padding: 16px; + flex-direction: column; + gap: 16px; + align-items: stretch; + } + + .selector { + flex-direction: column; + gap: 16px; + } + + .half-width { + max-width: 100%; + } + + .create-repository-button { + min-width: 100%; + margin-left: 0; + } + + .button-row { + justify-content: center; + } + + .destination-badge { + padding: 10px 14px; + border-radius: 10px; + } + + .destination-icon { + font-size: 18px; + width: 18px; + height: 18px; + margin-right: 10px; + } + + .destination-value { + max-width: 150px; + font-size: 13px; + } + + .destination-label { + font-size: 10px; + } +} + +/* Estilos para elementos específicos */ +.unit-name { + font-weight: 500; + color: #2c3e50; +} + +/* Estilos para las opciones de acción Git */ +.git-action-selector { + margin: 24px 0; + padding: 20px; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 12px; + border: 1px solid #dee2e6; +} + +.action-chips-container { + margin-bottom: 16px; +} + +::ng-deep .action-chip { + margin: 8px !important; + padding: 12px 20px !important; + border-radius: px !important; + font-weight: 500 !important; + font-size: 14px !important; + transition: all 0.3s ease !important; + border: 2px solid transparent !important; + background: white !important; + color: #6c757d !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; + cursor: pointer !important; + display: flex !important; + align-items: center !important; + gap: 8px !important; + min-height: 48px !important; +} + +::ng-deep .action-chip:hover { + transform: translateY(-2px) !important; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15) !important; +} + +::ng-deep .action-chip.mat-mdc-chip-selected { + border-color: #667eea !important; + box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2) !important; +} + +::ng-deep .create-chip.mat-mdc-chip-selected { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important; + color: white !important; +} + +::ng-deep .update-chip.mat-mdc-chip-selected { + background: linear-gradient(135deg, #007bff 0%, #0056b3 100%) !important; + color: white !important; +} + +::ng-deep .action-chip mat-icon { + font-size: 18px !important; + width: 18px !important; + height: 18px !important; +} + +.action-hint { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + background: rgba(102, 126, 234, 0.1); + border-radius: 8px; + border-left: 4px solid #667eea; + color: #495057; + font-size: 14px; +} + +.action-hint mat-icon { + color: #667eea; + font-size: 16px; + width: 16px; + height: 16px; +} + +.git-action-section { + background: white !important; + border-radius: 8px; + padding: 16px; + margin-top: 16px; + border-left: 4px solid #667eea; +} + +/* Eliminar sombra de la tabla */ +.mat-elevation-z8 { + box-shadow: none !important; +} + +/* Animaciones para transiciones de formulario */ +.form-transition { + transition: all 0.3s ease-in-out; + opacity: 1; + transform: translateY(0); +} + +.form-transition.ng-enter { + opacity: 0; + transform: translateY(10px); +} + +.form-transition.ng-enter-active { + opacity: 1; + transform: translateY(0); +} + +.form-transition.ng-leave { + opacity: 1; + transform: translateY(0); +} + +.form-transition.ng-leave-active { + opacity: 0; + transform: translateY(-10px); +} + +/* Estilos para los formularios específicos */ +.git-form-section { + background: white; + border-radius: 4px; + padding: 20px; + margin-top: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + border: 1px solid #e9ecef; + transition: all 0.3s ease; +} + +.git-form-section:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); +} + +/* Estilos para la sección de repositorio Git */ +.git-repository-section { + background: white !important; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + border: 1px solid #e9ecef; +} + +.repository-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 12px; +} + +.repository-header h4 { + margin: 0; + color: #2c3e50; + font-weight: 600; + font-size: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +.repository-selector { + display: flex; + gap: 20px; + align-items: flex-start; +} + +.repository-field { + flex: 0 0 300px; + min-width: 250px; +} + +.repository-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 8px; +} + +.info-item { + display: flex; + align-items: flex-start; + gap: 8px; + color: #6c757d; + font-size: 13px; + line-height: 1.4; +} + +.info-item mat-icon { + color: #667eea; + font-size: 16px; + width: 16px; + height: 16px; + margin-top: 2px; + flex-shrink: 0; +} + +.create-repository-button { + background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + color: white; + border: none; + padding: 10px 16px; + border-radius: 6px; + font-weight: 500; + font-size: 14px; + transition: all 0.3s ease; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + box-shadow: 0 2px 8px rgba(40, 167, 69, 0.2); +} + +.create-repository-button:hover:not(:disabled) { + background: linear-gradient(135deg, #218838 0%, #1ea085 100%); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3); +} + +.create-repository-button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.create-repository-button mat-icon { + font-size: 16px; + width: 16px; + height: 16px; +} + +.no-repositories-hint { + display: flex; + align-items: flex-start; + gap: 8px; + color: #dc3545; + font-weight: 500; + font-size: 13px; + padding: 4px 4px 4px 12px; + background: rgba(220, 53, 69, 0.1); + border-radius: 6px; + border-left: 3px solid #dc3545; + line-height: 1.4; +} + +.no-repositories-hint mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + margin-top: 2px; + flex-shrink: 0; +} + +/* Estilos para el hint del formulario */ +::ng-deep .mat-form-field-hint { + display: flex !important; + align-items: center !important; + gap: 6px !important; + color: #6c757d !important; + font-size: 12px !important; +} + +::ng-deep .mat-form-field-hint mat-icon { + font-size: 14px !important; + width: 14px !important; + height: 14px !important; + color: #667eea !important; +} + +/* Responsive */ +@media (max-width: 768px) { + .header-container { + flex-direction: column; + gap: 20px; + text-align: center; + } + + .button-row { + flex-wrap: wrap; + justify-content: center; + } + + .selector { + flex-direction: column; + } + + .half-width { + min-width: auto; + } + + .clients-grid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + } + + .input-group { + grid-template-columns: 1fr; + } + + .select-container { + padding: 0 16px; + } + + .form-section { + padding: 20px; + } + + /* Responsive para repositorio Git */ + .repository-header { + flex-direction: column; + gap: 12px; + align-items: flex-start; + } + + .repository-selector { + flex-direction: column; + gap: 16px; + } + + .repository-field { + flex: none; + width: 100%; + min-width: auto; + } + + .repository-info { + flex: none; + } + + .create-repository-button { + width: 100%; + justify-content: center; + } +} + +/* Botón flotante para scroll hacia arriba */ +.scroll-to-top-button { + position: fixed; + bottom: 30px; + left: 30px; + z-index: 1000; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.scroll-to-top-button:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); +} + +/* Animación de entrada/salida */ +.scroll-to-top-button { + animation: fadeInUp 0.3s ease; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Responsive para el botón */ +@media (max-width: 768px) { + .scroll-to-top-button { + bottom: 20px; + left: 20px; + } } 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 ea2ed83..5092b02 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 @@ -1,87 +1,205 @@ - + + +
-

+

Crear imagen desde {{ clientName }}

+
+
+ cloud_upload +
+ Destino + {{ selectedRepository?.name || 'No hay repositorio asociado' }} +
+
+
- +
-
-
- - Tipo de imagen - - Monolítica - - - + +
+
+ settings + Configuración de tipo de imagen +
+ +
+ + Tipo de imagen + + Monolítica + Git + + +
-
- - Nombre canónico - - + +
+
+ code + Configuración Git +
+ +
+
+ +
+ +
+ + 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. + + + +
+
- - Seleccione imagen - - {{ image?.name }} - - - Seleccione la imagen para sobreescribir si se requiere. - + +
+
+ + + Crear imagen + + + Actualizar imagen + + +
+
+ info + Crea una nueva imagen con el nombre especificado + Actualiza una imagen existente seleccionada +
+
+ +
+
+ image + Configuración de imagen +
+ + +
+ + + 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á. + +
- - - - - - - -
Seleccionar partición - - - - - {{ column.header }} - - {{ column.cell(image) }} - - - - - -
- {{ image.size }} MB - {{ image.size / 1024 }} GB -
-
- -
+
+ + Seleccione imagen + + Seleccionar imagen para actualizar + {{ image?.name }} + + + Selecciona la imagen existente que quieres actualizar. + +
+
+ +
+
+ storage + Selección de partición +
+ +
+ + + + + + + + + + + + + +
Seleccionar partición + + + + + {{ column.header }} + + {{ column.cell(image) }} + + + +
+ {{ image.size }} MB + {{ image.size / 1024 }} GB +
+
+
+
+
+ + + + diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts index 8c6fc46..bf413eb 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts @@ -1,10 +1,13 @@ -import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {Component, EventEmitter, OnInit, Output, ViewChild, ElementRef} from '@angular/core'; import { HttpClient } from "@angular/common/http"; import { ToastrService } from "ngx-toastr"; import { ActivatedRoute, Router } from "@angular/router"; import { MatTableDataSource } from "@angular/material/table"; import { SelectionModel } from "@angular/cdk/collections"; import { ConfigService } from '@services/config.service'; +import {MatDialog} from "@angular/material/dialog"; +import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component"; +import {CreateRepositoryModalComponent} from "./create-repository-modal/create-repository-modal.component"; @Component({ selector: 'app-create-image', @@ -14,18 +17,32 @@ import { ConfigService } from '@services/config.service'; export class CreateClientImageComponent implements OnInit{ baseUrl: string; @Output() dataChange = new EventEmitter(); + @ViewChild('partitionSection', { static: false }) partitionSection!: ElementRef; errorMessage = ''; clientId: string | null = null; partitions: any[] = []; images: any[] = []; clientName: string = ''; - selectedPartition: any = null; + private _selectedPartition: any = null; name: string = ''; client: any = null; loading: boolean = false; selectedImage: any = null; - imageType : string = 'monolithic'; + private _imageType: string = 'monolithic'; + selectedRepository: any = null; + gitRepositories: any[] = []; + selectedGitRepository: any = null; + gitImageRepositories: any[] = []; + gitImageName: string = ''; + loadingGitRepositories: boolean = false; + loadingGitImageRepositories: boolean = false; + creatingRepository: boolean = false; + gitAction: string = 'create'; + monolithicAction: string = 'create'; + existingImages: any[] = []; + selectedExistingImage: any = null; + loadingExistingImages: boolean = false; dataSource = new MatTableDataSource(); columns = [ { @@ -69,11 +86,13 @@ export class CreateClientImageComponent implements OnInit{ private configService: ConfigService, private route: ActivatedRoute, private router: Router, + private dialog: MatDialog ) { this.baseUrl = this.configService.apiUrl; } ngOnInit() { + console.log('CreateImageComponent ngOnInit ejecutado'); this.clientId = this.route.snapshot.paramMap.get('id'); this.loadPartitions(); this.loadImages(); @@ -86,6 +105,7 @@ export class CreateClientImageComponent implements OnInit{ if (response.partitions) { this.client = response; this.clientName = response.name; + this.selectedRepository = response.repository; this.dataSource.data = response.partitions.filter((partition: any) => { return partition.partitionNumber !== 0; @@ -100,7 +120,6 @@ export class CreateClientImageComponent implements OnInit{ onImageTypeSelected(event: any) { this.imageType = event; - this.loadImages(); } loadImages() { @@ -115,45 +134,285 @@ export class CreateClientImageComponent implements OnInit{ ); } + loadGitRepositories() { + this.loadingGitRepositories = true; + const url = `${this.baseUrl}/git-repositories?repository=${this.selectedRepository.id}&page=1&itemsPerPage=100`; + return this.http.get(url).subscribe( + (response: any) => { + this.gitRepositories = response['hydra:member']; + this.loadingGitRepositories = false; + }, + (error) => { + console.error('Error al cargar los repositorios git:', error); + this.loadingGitRepositories = false; + } + ); + } + + loadGitImageRepositories(gitRepository: any) { + this.loadingGitImageRepositories = true; + const url = `${this.baseUrl}/git-image-repositories?gitRepository.id=${gitRepository.id}&page=1&itemsPerPage=100`; + this.http.get(url).subscribe( + (response: any) => { + this.gitImageRepositories = response['hydra:member']; + this.loadingGitImageRepositories = false; + }, + (error) => { + console.error('Error al cargar las imágenes de repositorio git:', error); + this.loadingGitImageRepositories = false; + } + ); + } + + onGitRepositorySelected(gitRepository: any) { + this.selectedGitRepository = gitRepository; + this.selectedExistingImage = null; + this.existingImages = []; + if (gitRepository) { + this.loadGitImageRepositories(gitRepository); + } else { + this.gitImageRepositories = []; + } + } + + onGitActionSelected(event: any) { + console.log('onGitActionSelected llamado con:', event); + this.gitAction = event.value; + this.selectedExistingImage = null; + this.gitImageName = ''; + + // Si se selecciona 'update' y ya hay un repositorio Git seleccionado, cargar los repositorios de imágenes + if (event.value === 'update' && this.selectedGitRepository) { + this.loadGitImageRepositories(this.selectedGitRepository); + } + + console.log('Antes del setTimeout'); + // Hacer scroll hacia la sección de partición después de un delay más largo + setTimeout(() => { + console.log('Dentro del setTimeout, llamando a scrollToPartitionSection'); + this.scrollToPartitionSection(); + }, 300); + } + + onMonolithicActionSelected(event: any) { + console.log('onMonolithicActionSelected llamado con:', event); + this.monolithicAction = event.value; + this.selectedImage = null; + this.name = ''; + + // Si se selecciona 'update', cargar las imágenes existentes + if (event.value === 'update') { + this.loadImages(); + } + + console.log('Antes del setTimeout (monolithic)'); + // Hacer scroll hacia la sección de partición después de un delay más largo + setTimeout(() => { + console.log('Dentro del setTimeout (monolithic), llamando a scrollToPartitionSection'); + this.scrollToPartitionSection(); + }, 300); + } + + loadExistingImages() { + if (!this.selectedExistingImage) return; + + this.loadingExistingImages = true; + // Aquí deberías hacer el GET al endpoint externo + // Por ahora uso un endpoint de ejemplo, ajusta según tu API + const url = `${this.baseUrl}/images?gitImageRepository.id=${this.selectedExistingImage.id}&page=1&itemsPerPage=100`; + + this.http.get(url).subscribe( + (response: any) => { + this.existingImages = response['hydra:member'] || []; + this.loadingExistingImages = false; + }, + (error) => { + console.error('Error al cargar las imágenes existentes:', error); + this.loadingExistingImages = false; + this.toastService.error('Error al cargar las imágenes existentes'); + } + ); + } + + resetGitSelections() { + this.selectedGitRepository = null; + this.selectedExistingImage = null; + this.gitImageName = ''; + this.gitAction = 'create'; + this.existingImages = []; + this.gitRepositories = []; + this.gitImageRepositories = []; + + this.selectedImage = null; + this.name = ''; + this.monolithicAction = 'create'; + } + resetCanonicalName() { this.name = this.selectedImage ? this.selectedImage.name : ''; } save(): void { - this.loading = true; - if (!this.selectedPartition) { this.toastService.error('Debes seleccionar una partición'); - this.loading = false; return; } + if (this.imageType === 'git') { + if (!this.selectedGitRepository) { + this.toastService.error('Debes seleccionar un repositorio Git'); + return; + } + if (this.gitAction === 'update' && !this.selectedExistingImage) { + this.toastService.error('Debes seleccionar un repositorio de imágenes Git'); + return; + } + } + + if (this.imageType === 'monolithic') { + if (this.monolithicAction === 'create' && !this.name) { + this.toastService.error('Debes introducir un nombre canónico para la imagen'); + return; + } + if (this.monolithicAction === 'update' && !this.selectedImage) { + this.toastService.error('Debes seleccionar una imagen para actualizar'); + return; + } + } + if (this.selectedImage) { this.toastService.warning('Aviso: Está seleccionando una imagen previamente creada. Se procede a crear un backup de la misma. '); } - const payload = { - client: `/clients/${this.clientId}`, - name: this.name, - partition: this.selectedPartition['@id'], - source: 'assistant', - type: this.imageType, - selectedImage: this.selectedImage?.['@id'] - }; + const dialogRef = this.dialog.open(QueueConfirmationModalComponent, { + width: '400px', + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop' + }); + dialogRef.afterClosed().subscribe(result => { + if (result !== undefined) { + this.loading = true; + + let payload: any = { + client: `/clients/${this.clientId}`, + partition: this.selectedPartition['@id'], + source: 'assistant', + type: this.imageType, + queue: result + }; - this.http.post(`${this.baseUrl}/images`, payload) - .subscribe({ - next: (response) => { - this.toastService.success('Petición de creación de imagen enviada'); - this.loading = false; - this.router.navigate(['/commands-logs']); - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - this.loading = false; + if (this.imageType === 'git') { + payload.gitRepository = this.selectedGitRepository.name + payload.name = this.selectedGitRepository.name; + + if (this.gitAction === 'create') { + payload.action = 'create'; + } else { + payload.action = 'update'; + } + } else { + if (this.monolithicAction === 'create') { + payload.name = this.name; + payload.action = 'create'; + } else { + payload.selectedImage = this.selectedImage['@id']; + payload.action = 'update'; + } } + + this.http.post(`${this.baseUrl}/images`, payload) + .subscribe({ + next: (response) => { + let actionText = 'creación'; + if (this.imageType === 'git' && this.gitAction === 'update') { + actionText = 'actualización'; + } else if (this.imageType === 'monolithic' && this.monolithicAction === 'update') { + actionText = 'actualización'; + } + this.toastService.success(`Petición de ${actionText} de imagen enviada`); + this.loading = false; + this.router.navigate(['/commands-logs']); + }, + error: (error) => { + this.toastService.error(error.error['hydra:description']); + this.loading = false; + } + }); } - ); + }); + } + + openCreateRepositoryModal(): void { + this.creatingRepository = true; + const dialogRef = this.dialog.open(CreateRepositoryModalComponent, { + width: '600px', + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop', + data: { + clientRepository: this.selectedRepository + } + }); + + dialogRef.afterClosed().subscribe(result => { + this.creatingRepository = false; + if (result) { + this.loadGitRepositories(); + setTimeout(() => { + const newRepository = this.gitRepositories.find(repo => repo['@id'] === result['@id']); + if (newRepository) { + this.selectedGitRepository = newRepository; + this.onGitRepositorySelected(newRepository); + } + }, 200); + } + }); + } + + get imageType(): string { + return this._imageType; + } + + set imageType(value: string) { + this._imageType = value; + this.loadImages(); + if (value === 'git') { + this.loadGitRepositories(); + this.selectedImage = null; + this.name = ''; + this.monolithicAction = 'create'; + } else { + this.resetGitSelections(); + } + } + + onGitImageRepositorySelected(gitImageRepository: any) { + this.selectedExistingImage = gitImageRepository; + this.existingImages = []; + + if (gitImageRepository) { + this.loadExistingImages(); + } + } + + scrollToPartitionSection() { + const partitionSection = document.getElementById('partition-selection'); + + if (partitionSection) { + partitionSection.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + } + + get selectedPartition(): any { + return this._selectedPartition; + } + + set selectedPartition(value: any) { + this._selectedPartition = value; } } diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.css new file mode 100644 index 0000000..f2bc788 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.css @@ -0,0 +1,40 @@ +.dialog-content { + display: flex; + flex-direction: column; + padding: 40px; +} + +.repository-form { + width: 100%; + display: flex; + flex-direction: column; +} + +.form-field { + width: 100%; + margin-top: 16px; +} + +.action-container { + display: flex; + justify-content: flex-end; + gap: 1em; + padding: 1.5em; +} + +@media (max-width: 600px) { + .form-field { + width: 100%; + } + + .dialog-actions { + flex-direction: column; + align-items: stretch; + } + + button { + width: 100%; + margin-left: 0; + margin-bottom: 8px; + } +} \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.html new file mode 100644 index 0000000..8daa4da --- /dev/null +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.html @@ -0,0 +1,17 @@ + + +

Crear nuevo repositorio de imágenes git

+ + +
+ + Nombre del repositorio + + +
+
+ +
+ + +
\ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.ts new file mode 100644 index 0000000..9fae2af --- /dev/null +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-repository-modal/create-repository-modal.component.ts @@ -0,0 +1,64 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { HttpClient } from "@angular/common/http"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { ToastrService } from "ngx-toastr"; +import { ConfigService } from '@services/config.service'; + +@Component({ + selector: 'app-create-repository-modal', + templateUrl: './create-repository-modal.component.html', + styleUrl: './create-repository-modal.component.css' +}) +export class CreateRepositoryModalComponent implements OnInit { + baseUrl: string; + repositoryForm: FormGroup; + loading: boolean = false; + clientRepository: any = null; + + constructor( + private fb: FormBuilder, + private http: HttpClient, + public dialogRef: MatDialogRef, + private toastService: ToastrService, + private configService: ConfigService, + @Inject(MAT_DIALOG_DATA) public data: any + ) { + this.baseUrl = this.configService.apiUrl; + this.clientRepository = this.data?.clientRepository || null; + this.repositoryForm = this.fb.group({ + name: [null, Validators.required], + }); + } + + ngOnInit() { + // El componente se inicializa + } + + save(): void { + if (this.repositoryForm.valid) { + this.loading = true; + const payload = { + name: this.repositoryForm.value.name, + repository: this.clientRepository ? this.clientRepository.id : null + }; + + this.http.post(`${this.baseUrl}/git-repositories`, payload).subscribe({ + next: (response) => { + this.toastService.success('Repositorio creado correctamente'); + this.dialogRef.close(response); + }, + error: (error) => { + this.toastService.error(error.error?.['hydra:description'] || 'Error al crear el repositorio'); + this.loading = false; + } + }); + } else { + this.toastService.error('Por favor, complete todos los campos requeridos'); + } + } + + close(): void { + this.dialogRef.close(); + } +} \ No newline at end of file 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 69ad087..8cdb3e0 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 @@ -1,101 +1,140 @@ - -.divider { - margin: 20px 0; -} - -table { - width: 100%; - margin-top: 50px; - background-color: #eaeff6; -} - -.search-container { +.header-container { display: flex; justify-content: space-between; align-items: center; - width: 100%; - padding: 0 5px; - box-sizing: border-box; + padding: 24px 32px; + background: white; + border-radius: 12px; + margin-bottom: 20px; } -.option-container { - margin: 20px 0; - width: 100%; +.header-container-title { + flex-grow: 1; + text-align: left; } -.deploy-container { +.header-container-title h2 { + margin: 0 0 8px 0; + color: #333; + font-weight: 600; +} + +.header-container-title h4 { + margin: 0; + font-size: 16px; + opacity: 0.9; + font-weight: 400; +} + +.button-row { display: flex; - justify-content: space-between; + padding-right: 1em; + gap: 12px; align-items: center; - width: 100%; - padding: 5px; - gap: 10px; } -.options-container { - padding: 10px; +.action-button { + margin-top: 10px; + margin-bottom: 10px; + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + font-weight: 500; + transition: all 0.3s ease; + cursor: pointer; } +.action-button:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.action-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Contenedor principal */ .select-container { + background: white !important; margin-top: 20px; align-items: center; padding: 20px; box-sizing: border-box; } -.input-group { +/* Secciones del formulario */ +.form-section { + background: white !important; + border-radius: 16px; + padding: 32px; + margin-bottom: 24px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + border: 1px solid #bbdefb; +} + +.form-section-title { display: flex; + align-items: center; + gap: 12px; + margin-bottom: 24px; + font-size: 20px; + font-weight: 600; + color: #2c3e50; + padding-bottom: 16px; + border-bottom: 2px solid #f8f9fa; +} + +.form-section-title mat-icon { + color: #667eea; + font-size: 24px; + width: 24px; + height: 24px; +} + +/* Selectores */ +.selector { + display: flex; + gap: 20px; + margin-bottom: 20px; flex-wrap: wrap; - gap: 16px; - margin-top: 20px; } -mat-option .unit-name { - display: block; -} - -.input-field { - flex: 1 1 calc(33.33% - 16px); +.half-width { + flex: 1; min-width: 250px; } .full-width { width: 100%; - margin-bottom: 16px; } -.search-string { - flex: 2; - padding: 5px; +/* Campos de formulario */ +mat-form-field { + width: 100%; + margin-bottom: 8px; } -.search-boolean { - flex: 1; - padding: 5px; +::ng-deep .mat-form-field-appearance-fill .mat-form-field-flex { + background-color: #f8f9fa; + border-radius: 8px; + border: 1px solid #e9ecef; + transition: all 0.3s ease; } -.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; +::ng-deep .mat-form-field-appearance-fill.mat-focused .mat-form-field-flex { + background-color: white; + border-color: #2196f3; + box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2); } +/* Grid de clientes */ .clients-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 8px; + gap: 12px; + margin-top: 20px; } .client-item { @@ -103,117 +142,381 @@ mat-option .unit-name { } .client-card { - background: #ffffff; - border-radius: 6px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + background: white; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); overflow: hidden; position: relative; - padding: 8px; + padding: 12px; text-align: center; cursor: pointer; - transition: background-color 0.3s, transform 0.2s; - - &:hover { - background-color: #f0f0f0; - transform: scale(1.02); - } + transition: all 0.3s ease; + border: 2px solid transparent; } -::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; +.client-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + border-color: #667eea; } .selected-client { - background-color: #a0c2e5 !important; /* Azul */ - color: white !important; + background: linear-gradient(135deg, #8fa1f0 0%, #9b7bc8 100%); + color: white; + border-color: #667eea; +} + +.selected-client .client-name, +.selected-client .client-ip { + color: white; +} + +.client-image { + width: 32px; + height: 32px; + margin-bottom: 8px; } .client-details { - margin-top: 4px; + margin-bottom: 12px; } .client-name { - font-size: 0.9em; + font-size: 12px; font-weight: 600; - color: #333; - margin-bottom: 5px; + color: #2c3e50; + margin-bottom: 2px; + display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: 150px; - display: inline-block; } .client-ip { + font-size: 10px; + color: #6c757d; 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; + margin-bottom: 1px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } +/* Tabla de particiones */ .partition-table-container { - background-color: #eaeff6; - padding: 20px; + background: white !important; border-radius: 12px; + padding: 24px; margin-top: 20px; } +table { + width: 100%; + background: white; + border-radius: 8px; + overflow: hidden; +} + +::ng-deep .mat-table { + background: white; +} + +::ng-deep .mat-header-cell { + background: white !important; + color: #2c3e50; + font-weight: 600; + padding: 16px; +} + +::ng-deep .mat-cell { + padding: 16px; + color: #495057; +} + +/* Opciones avanzadas */ +.input-group { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.input-field { + width: 100%; +} + +/* Mensajes de error */ +.error-message { + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%); + color: white; + padding: 16px 20px; + border-radius: 8px; + margin-top: 16px; + font-weight: 500; + box-shadow: 0 4px 16px rgba(255, 107, 107, 0.3); +} + +/* Instrucciones */ +.instructions-box { + margin-bottom: 20px; +} + +.instructions-card { + background: white; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); +} + +::ng-deep .instructions-card .mat-card-title { + color: #2c3e50; + font-weight: 600; + padding: 20px 20px 0 20px; +} + +::ng-deep .instructions-card .mat-card-content { + padding: 20px; +} + +.instructions-card pre { + background: white !important; + padding: 16px; + border-radius: 8px; + border: 1px solid #e9ecef; + overflow-x: auto; + font-family: 'Courier New', monospace; + font-size: 14px; + line-height: 1.5; +} + +/* Tooltip personalizado */ +::ng-deep .custom-tooltip { + background: rgba(0, 0, 0, 0.9) !important; + color: white !important; + padding: 12px !important; + border-radius: 8px !important; + font-size: 12px !important; + max-width: 250px !important; + white-space: pre-line !important; +} + +/* Responsive */ +@media (max-width: 768px) { + .header-container { + flex-direction: column; + gap: 16px; + align-items: stretch; + } + + .button-row { + justify-content: center; + } + + .selector { + flex-direction: column; + } + + .half-width { + min-width: auto; + } + + .clients-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 8px; + } + + .client-card { + padding: 8px; + } + + .client-image { + width: 24px; + height: 24px; + margin-bottom: 6px; + } + + .client-name { + font-size: 11px; + } + + .client-ip { + font-size: 9px; + } + + .input-group { + grid-template-columns: 1fr; + } + + .select-container { + padding: 0 16px; + } + + .form-section { + padding: 20px; + } + + .destination-badge { + padding: 10px 14px; + border-radius: 10px; + } + + .destination-icon { + font-size: 18px; + width: 18px; + height: 18px; + margin-right: 10px; + } + + .destination-value { + max-width: 150px; + font-size: 13px; + } + + .destination-label { + font-size: 10px; + } +} + +/* Estilos para elementos específicos */ +.unit-name { + font-weight: 500; + color: #2c3e50; +} + +/* Eliminar sombra de la tabla */ +.mat-elevation-z8 { + box-shadow: none !important; +} + +/* Estilos para el expansion panel */ +::ng-deep .mat-expansion-panel { + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important; + border-radius: 12px !important; + margin-bottom: 20px; + background: #f7fbff !important; + border: 1px solid #bbdefb !important; +} + +::ng-deep .mat-expansion-panel-header { + padding: 20px 24px !important; + border-radius: 12px !important; +} + +::ng-deep .mat-expansion-panel-header-title { + font-weight: 600 !important; + color: #2c3e50 !important; +} + +::ng-deep .mat-expansion-panel-header-description { + color: #6c757d !important; +} + +/* Otros estilos */ +.divider { + margin: 20px 0; +} + .disabled-client { pointer-events: none; opacity: 0.5; } -.error-message { - background-color: #de2323; - padding: 20px; - border-radius: 12px; - margin-top: 20px; - color: white; - font-weight: bold; -} - -.action-button { - margin-top: 10px; - margin-bottom: 10px; -} - .mat-expansion-panel-header-description { justify-content: space-between; align-items: center; } -.instructions-box { - margin-top: 15px; - background-color: #f5f5f5; - border: 1px solid #ccc; - padding: 15px; - border-radius: 6px; -} - .instructions-textarea textarea { font-family: monospace; white-space: pre; } -.instructions-card { - background-color: #f5f5f5; - box-shadow: none !important; - margin-top: 15px; +/* Estilo para hacer el backdrop no clickeable */ +::ng-deep .non-clickable-backdrop { + pointer-events: none; +} + +/* Estilos modernos para el badge de destino */ +.destination-info { + margin-top: 12px; +} + +.destination-badge { + display: inline-flex; + align-items: center; + background: #e3f2fd; + color: #1565c0; + padding: 12px 16px; + border-radius: 12px; + border: 1px solid #bbdefb; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: all 0.2s ease; +} + +.destination-icon { + font-size: 20px; + width: 20px; + height: 20px; + margin-right: 12px; + color: #1976d2; +} + +.destination-content { + display: flex; + flex-direction: column; + gap: 2px; +} + +.destination-label { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #1976d2; + line-height: 1; +} + +.destination-value { + font-size: 14px; + font-weight: 600; + line-height: 1.2; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #0d47a1; +} + +.filters-row { + display: flex; + gap: 20px; + align-items: flex-end; + margin-bottom: 20px; +} + +.git-gap { + gap: 40px; +} + +.monolithic-row { + display: flex; + gap: 24px; + align-items: flex-end; + margin-bottom: 20px; +} + +.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; + } } 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 1a5bd76..442a438 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 @@ -5,12 +5,18 @@

{{ 'deployImage' | translate }}

-

- {{ runScriptTitle }} -

+
+
+ cloud_download +
+ Destino + {{ runScriptTitle }} +
+
+
-
@@ -21,18 +27,15 @@
-
-
- - -
@@ -53,7 +56,7 @@
@@ -88,84 +91,166 @@
-
- - Tipo de imagen - - Monolítica - - - -
- -
- - Seleccione imagen - - -
{{ image.name }}
-
{{ image.description }}
-
-
-
- - - Seleccione método de deploy - - {{ method.name }} - - -
- -
- {{ errorMessage }} -
- -
-
- - - Instrucciones generadas - - - -
{{ ogInstructions }}
-
-
+ +
+
+ image + Configuración de imagen +
+ +
+ + Tipo de imagen + + Monolítica + Git + +
- - - - - + +
+ + Seleccionar Repositorio + + + {{ repo }} + + + hourglass_empty + - -
- - + + Seleccionar Rama + + + {{ branch }} + + + hourglass_empty + + - - -
Seleccionar partición - - - - - {{ column.header }} - {{ column.cell(image) }} -
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
Seleccionar + + + + Commit ID + + {{ commit.hexsha }} + + Mensaje{{ commit.message }}Fecha{{ commit.committed_date * 1000 | date:'dd/MM/yyyy HH:mm:ss' }}Tags + + {{ tag }} + Sin tags + +
+
+ + +
+ + Seleccione método de deploy + + {{ method.name }} + + + + Seleccione imagen + + +
{{ image.name }}
+
{{ image.description }}
+
+
+
+
+ +
+ {{ errorMessage }} +
- -
-

Opciones multicast

-

Opciones torrent

-
+ +
+
+ storage + Selección de partición +
+ +
+
+ + + Instrucciones generadas + + + +
{{ ogInstructions }}
+
+
+
+ + + + + + + + + + + + + + +
Seleccionar partición + + + + + {{ column.header }} + {{ column.cell(image) }} +
+
+
+ + +
+
+ settings + Opciones multicast + Opciones torrent +
+ +
Puerto
-
+
Modo P2P - - Semilla + + Semilla (minutos) + [required]="isMethod('p2p') && p2pMode === 'seeder'">
-
+ + + diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts index 5ebe993..5e655bb 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts @@ -7,6 +7,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { ConfigService } from '@services/config.service'; import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component"; import {MatDialog} from "@angular/material/dialog"; +import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component"; @Component({ selector: 'app-deploy-image', @@ -23,7 +24,7 @@ export class DeployImageComponent implements OnInit{ images: any[] = []; selectedImage: any = null; selectedMethod: string | null = null; - selectedPartition: any = null; + private _selectedPartition: any = null; mcastIp: string = ''; mcastPort: Number = 0; mcastMode: string = ''; @@ -40,6 +41,8 @@ export class DeployImageComponent implements OnInit{ ogInstructions: string = ''; deployImage: boolean = true; showInstructions: boolean = false; + loadingCommits: boolean = false; + selectedGitRepository: string = ''; protected p2pModeOptions = [ { name: 'Leecher', value: 'leecher' }, @@ -97,6 +100,15 @@ export class DeployImageComponent implements OnInit{ displayedColumns = ['select', ...this.columns.map(column => column.columnDef)]; selection = new SelectionModel(true, []); + repositories: string[] = []; + loadingRepositories: boolean = false; + branches: string[] = []; + selectedBranch: string = ''; + loadingBranches: boolean = false; + commits: any[] = []; + selectedCommit: any = null; + private initialGitLoad = true; + constructor( private http: HttpClient, private toastService: ToastrService, @@ -115,18 +127,11 @@ export class DeployImageComponent implements OnInit{ } }); this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null; - this.clientData.forEach((client: { selected: boolean; status: string}) => { - if (client.status === 'og-live') { - client.selected = true; - } - }); - this.selectedClients = this.clientData.filter( - (client: { status: string }) => client.status === 'og-live' - ); + this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; }); - this.selectedModelClient = this.clientData.find( - (client: { status: string }) => client.status === 'og-live' - ) || null; + this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected); + + this.selectedModelClient = this.clientData.find((client: { selected: boolean; status: string}) => client.selected) || null; if (this.selectedModelClient) { this.loadPartitions(this.selectedModelClient); @@ -141,7 +146,14 @@ export class DeployImageComponent implements OnInit{ onImageTypeSelected(event: any) { this.imageType = event; + if (event === 'git') { + this.selectedMethod = null; + this.loadGitRepositories(); + } this.loadImages(); + setTimeout(() => { + this.scrollToPartitionSection(); + }, 200); } get runScriptTitle(): string { @@ -211,7 +223,7 @@ export class DeployImageComponent implements OnInit{ this.loadImages(); }, (error) => { - console.error('Error al cargar los datos completos del cliente:', error); + console.error('Error al cargar las particiones:', error); } ); } else { @@ -223,11 +235,7 @@ export class DeployImageComponent implements OnInit{ toggleSelectAll() { this.allSelected = !this.allSelected; - this.clientData.forEach((client: { selected: boolean; status: string }) => { - if (client.status === "og-live") { - client.selected = this.allSelected; - } - }); + this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = this.allSelected; }); } loadImages() { @@ -265,90 +273,143 @@ export class DeployImageComponent implements OnInit{ } } this.errorMessage = ""; + + if (this.selectedMethod) { + setTimeout(() => { + this.scrollToPartitionSection(); + }, 300); + } + return true; } save(): void { - this.loading = true; - if (!this.selectedClients.length) { this.toastService.error('Debe seleccionar al menos un cliente'); - this.loading = false; return; } - if (!this.selectedImage) { + if (!this.selectedImage && this.imageType !== 'git') { this.toastService.error('Debe seleccionar una imagen'); - this.loading = false; return; } - if (!this.selectedMethod) { + if (!this.selectedMethod && this.imageType !== 'git') { this.toastService.error('Debe seleccionar un método'); - this.loading = false; return; } if (!this.selectedPartition) { this.toastService.error('Debe seleccionar una partición'); - this.loading = false; return; } + if (this.imageType === 'git' && !this.selectedCommit) { + this.toastService.error('Debe seleccionar un commit'); + return; + } + + const dialogRef = this.dialog.open(QueueConfirmationModalComponent, { + width: '400px', + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop' + }); + this.toastService.info('Preparando petición de despliegue'); - const payload = { - clients: this.selectedClients.map((client: any) => client.uuid), - method: this.selectedMethod, - // partition: this.selectedPartition['@id'], - diskNumber: this.selectedPartition.diskNumber, - partitionNumber: this.selectedPartition.partitionNumber, - p2pMode: this.p2pMode, - p2pTime: this.p2pTime, - mcastIp: this.mcastIp, - mcastPort: this.mcastPort, - mcastMode: this.mcastMode, - mcastSpeed: this.mcastSpeed, - maxTime: this.mcastMaxTime, - maxClients: this.mcastMaxClients, - }; + dialogRef.afterClosed().subscribe(result => { + if (result !== undefined) { + this.loading = true; + + let payload: any; + let url: string; - this.http.post(`${this.baseUrl}/image-image-repositories/${this.selectedImage.uuid}/deploy-image`, payload) - .subscribe({ - next: (response) => { - this.toastService.success('Petición de despliegue enviada correctamente'); - this.loading = false; - this.router.navigate(['/commands-logs']); - }, - error: (error) => { - this.toastService.error(error.error['hydra:description'], 'Se ha detectado un error en el despliegue de imágenes.', { - "closeButton": true, - "newestOnTop": false, - "progressBar": false, - "positionClass": "toast-bottom-right", - "timeOut": 0, - "extendedTimeOut": 0, - "tapToDismiss": false - }); - this.loading = false; + if (this.imageType === 'git') { + payload = { + type: 'git', + clients: this.selectedClients.map((client: any) => client.uuid), + diskNumber: this.selectedPartition.diskNumber, + partitionNumber: this.selectedPartition.partitionNumber, + repositoryName: this.selectedGitRepository, + branch: this.selectedBranch, + hexsha: this.selectedCommit.hexsha, + queue: result + }; + url = `${this.baseUrl}/git-repositories/deploy-image`; + } else { + payload = { + clients: this.selectedClients.map((client: any) => client.uuid), + method: this.selectedMethod, + diskNumber: this.selectedPartition.diskNumber, + partitionNumber: this.selectedPartition.partitionNumber, + p2pMode: this.p2pMode, + p2pTime: this.p2pMode === 'seeder' ? this.p2pTime : null, + mcastIp: this.mcastIp, + mcastPort: this.mcastPort, + mcastMode: this.mcastMode, + mcastSpeed: this.mcastSpeed, + maxTime: this.mcastMaxTime, + maxClients: this.mcastMaxClients, + type: this.imageType, + queue: result + }; + url = `${this.baseUrl}/image-image-repositories/${this.selectedImage.uuid}/deploy-image`; } - }); + + this.http.post(url, payload) + .subscribe({ + next: (response) => { + this.toastService.success('Petición de despliegue enviada correctamente'); + this.loading = false; + this.router.navigate(['/commands-logs']); + }, + error: (error) => { + this.toastService.error(error.error['hydra:description'], 'Se ha detectado un error en el despliegue de imágenes.', { + "closeButton": true, + "newestOnTop": false, + "progressBar": false, + "positionClass": "toast-bottom-right", + "timeOut": 0, + "extendedTimeOut": 0, + "tapToDismiss": false + }); + this.loading = false; + } + }); + } + }); } isFormValid(): boolean { - if (!this.allSelected || !this.selectedModelClient || !this.selectedImage || !this.selectedMethod || !this.selectedPartition) { + if (!this.allSelected || !this.selectedModelClient || !this.selectedPartition) { return false; } - if (this.isMethod('udpcast') || this.isMethod('uftp') || this.isMethod('udpcast-direct')) { - if (!this.mcastPort || !this.mcastIp || !this.mcastMode || !this.mcastSpeed || !this.mcastMaxClients || !this.mcastMaxTime) { + if (this.imageType === 'git') { + if (!this.selectedCommit) { return false; } } - if (this.isMethod('p2p')) { - if (!this.p2pMode || !this.p2pTime) { - return false; + if (this.imageType !== 'git' && !this.selectedMethod) { + return false; + } + + if (this.imageType !== 'git') { + if (this.isMethod('udpcast') || this.isMethod('uftp') || this.isMethod('udpcast-direct')) { + if (!this.mcastPort || !this.mcastIp || !this.mcastMode || !this.mcastSpeed || !this.mcastMaxClients || !this.mcastMaxTime) { + return false; + } + } + + if (this.isMethod('p2p')) { + if (!this.p2pMode) { + return false; + } + if (this.p2pMode === 'seeder' && !this.p2pTime) { + return false; + } } } @@ -372,14 +433,14 @@ export class DeployImageComponent implements OnInit{ method: this.selectedMethod, diskNumber: this.selectedPartition.diskNumber, partitionNumber: this.selectedPartition.partitionNumber, - p2pMode: this.selectedMethod === 'torrent' ? this.p2pMode : null, - p2pTime: this.selectedMethod === 'torrent' ? this.p2pTime : null, - mcastIp: this.selectedMethod === 'multicast' ? this.mcastIp : null, - mcastPort: this.selectedMethod === 'multicast' ? this.mcastPort : null, - mcastMode: this.selectedMethod === 'multicast' ? this.mcastMode : null, - mcastSpeed: this.selectedMethod === 'multicast' ? this.mcastSpeed : null, - maxTime: this.selectedMethod === 'multicast' ? this.mcastMaxTime : null, - maxClients: this.selectedMethod === 'multicast' ? this.mcastMaxClients : null, + p2pMode: this.selectedMethod === 'p2p' ? this.p2pMode : null, + p2pTime: this.selectedMethod === 'p2p' && this.p2pMode === 'seeder' ? this.p2pTime : null, + mcastIp: this.selectedMethod === 'udpcast' ? this.mcastIp : null, + mcastPort: this.selectedMethod === 'udpcast' ? this.mcastPort : null, + mcastMode: this.selectedMethod === 'udpcast' ? this.mcastMode : null, + mcastSpeed: this.selectedMethod === 'udpcast' ? this.mcastSpeed : null, + maxTime: this.selectedMethod === 'udpcast' ? this.mcastMaxTime : null, + maxClients: this.selectedMethod === 'udpcast' ? this.mcastMaxClients : null, }; this.http.post(`${this.baseUrl}/command-task-scripts`, { @@ -400,37 +461,108 @@ export class DeployImageComponent implements OnInit{ } generateOgInstructions() { - let script = ''; - const disk = this.selectedPartition?.disk; - const partition = this.selectedPartition?.partition; + this.showInstructions = true; + this.ogInstructions = `og-deploy-image --image ${this.selectedImage.name} --partition ${this.selectedPartition.partitionNumber} --method ${this.selectedMethod}`; + } - let ip = this.selectedImage?.repository?.ip || 'REPO'; - let imgName = this.selectedImage?.canonicalName || ''; - let target = ` ${disk} ${partition}`; - let log = `ogEcho log session "[0] $MSG_SCRIPTS_TASK_START `; - - if (this.deployImage) { - script = 'deployImage '; - } else { - script = 'updateCache '; - imgName += '.img'; - target = ''; + scrollToPartitionSection() { + const partitionSection = document.getElementById('partition-selection'); + if (partitionSection) { + partitionSection.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + console.log('Scroll ejecutado'); } + } - script += `${ip} /${imgName}${target} ${this.selectedMethod}`; - log += `${script}"\n`; - script = log + script; - - let params = ''; - if (['udpcast', 'uftp', 'udpcast-direct'].includes(this.selectedMethod)) { - params = `${this.mcastPort}:${this.mcastMode}:${this.mcastIp}:${this.mcastSpeed}M:${this.mcastMaxClients}:${this.mcastMaxTime}`; - } else if (this.selectedMethod === 'p2p') { - params = `${this.p2pMode}:${this.p2pTime}`; + scrollToAdvancedOptions() { + const advancedOptions = document.getElementById('advanced-options'); + + if (advancedOptions) { + advancedOptions.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + console.log('Scroll hacia opciones avanzadas ejecutado'); } + } - script += ` ${params}`; + get selectedPartition(): any { + return this._selectedPartition; + } - this.ogInstructions = script; - this.showInstructions = true + set selectedPartition(value: any) { + this._selectedPartition = value; + } + + loadGitRepositories() { + this.loadingRepositories = true; + this.http.get(`${this.baseUrl}/image-repositories/server/git/${this.selectedRepository?.uuid}/get-collection`).subscribe( + data => { + this.repositories = data.repositories || []; + this.loadingRepositories = false; + if (this.repositories.length > 0) { + this.selectedGitRepository = this.repositories[0]; + this.loadGitBranches(); + } + }, + error => { + this.toastService.error('Error al cargar los repositorios git'); + this.loadingRepositories = false; + } + ); + } + + onGitRepositoryChange(event: any) { + this.selectedGitRepository = event; + this.selectedBranch = ''; + this.branches = []; + this.commits = []; + this.selectedCommit = null; + this.loadGitBranches(); + } + + loadGitBranches() { + if (!this.selectedGitRepository) return; + this.loadingBranches = true; + this.http.post(`${this.baseUrl}/image-repositories/server/git/${this.selectedRepository?.uuid}/branches`, { repositoryName: this.selectedGitRepository }).subscribe( + data => { + this.branches = data.branches || []; + this.loadingBranches = false; + if (this.branches.length > 0) { + this.selectedBranch = this.branches[0]; + this.loadGitCommits(); + } + }, + error => { + this.toastService.error('Error al cargar las ramas'); + this.loadingBranches = false; + } + ); + } + + onGitBranchChange() { + this.selectedCommit = null; + this.commits = []; + this.loadGitCommits(); + } + + loadGitCommits() { + if (!this.selectedGitRepository || !this.selectedBranch) return; + this.loadingCommits = true; + this.http.post(`${this.baseUrl}/image-repositories/server/git/${this.selectedRepository?.uuid}/commits`, { + repositoryName: this.selectedGitRepository, + branch: this.selectedBranch + }).subscribe( + data => { + this.commits = data.commits || []; + this.loadingCommits = false; + }, + error => { + this.toastService.error('Error al cargar los commits'); + this.loadingCommits = false; + } + ); } } diff --git a/ogWebconsole/src/app/components/repositories/repositories.component.html b/ogWebconsole/src/app/components/repositories/repositories.component.html index e5a9239..ab330e5 100644 --- a/ogWebconsole/src/app/components/repositories/repositories.component.html +++ b/ogWebconsole/src/app/components/repositories/repositories.component.html @@ -51,7 +51,6 @@ (click)="openShowMonoliticImagesDialog(repository)"> {{ 'monolithicImage' | translate }} - + diff --git a/ogWebconsole/src/app/components/repositories/repositories.component.ts b/ogWebconsole/src/app/components/repositories/repositories.component.ts index 1c2dd73..82b5338 100644 --- a/ogWebconsole/src/app/components/repositories/repositories.component.ts +++ b/ogWebconsole/src/app/components/repositories/repositories.component.ts @@ -10,7 +10,7 @@ import { Router } from '@angular/router'; import { ConfigService } from '@services/config.service'; import {Subnet} from "../ogdhcp/og-dhcp-subnets.component"; import {ShowMonoliticImagesComponent} from "./show-monolitic-images/show-monolitic-images.component"; -import {ShowGitImagesComponent} from "./show-git-images/show-git-images.component"; +import {ShowGitCommitsComponent} from "./show-git-images/show-git-images.component"; import {ManageRepositoryComponent} from "./manage-repository/manage-repository.component"; @Component({ @@ -146,7 +146,7 @@ export class RepositoriesComponent implements OnInit { } openShowGitImagesDialog(repository: Subnet) { - const dialogRef = this.dialog.open(ShowGitImagesComponent, { + const dialogRef = this.dialog.open(ShowGitCommitsComponent, { width: '85vw', height: '85vh', maxWidth: '85vw', diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.css b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.css index 45b503c..c2c252a 100644 --- a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.css +++ b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.css @@ -98,3 +98,96 @@ table { gap: 1em; padding: 1.5em; } + +/* Estilos específicos para commits */ +.repository-selector-container { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; +} + +.branch-selector-container { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; +} + +.commit-id { + font-family: 'Courier New', monospace; + background-color: #f5f5f5; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.9em; +} + +.commit-message { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.commit-stats { + font-size: 0.85em; + color: #666; +} + +.commit-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.commit-tags .mat-chip { + font-size: 0.8em; + height: 24px; +} + +.no-tags { + color: #999; + font-style: italic; + font-size: 0.9em; +} + +/* Mejoras en la tabla */ +.mat-mdc-table { + border-radius: 8px; + overflow: hidden; +} + +.mat-mdc-header-cell { + background-color: #f8f9fa; + font-weight: 600; + color: #495057; +} + +.mat-mdc-row:hover { + background-color: #f8f9fa; +} + +/* Estilos para los botones de acción */ +.action-buttons { + display: flex; + gap: 4px; + justify-content: center; +} + +.action-buttons .mat-mdc-icon-button { + width: 32px; + height: 32px; + line-height: 32px; +} + +.filters-row { + display: flex; + gap: 20px; + align-items: flex-end; + margin-bottom: 20px; +} + +.repository-selector-container, +.branch-selector-container { + margin-bottom: 0 !important; +} diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.html b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.html index e591807..2b7612a 100644 --- a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.html +++ b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.html @@ -6,96 +6,96 @@ -

Gestionar imágenes git en {{data.repositoryName}}

+

Commits de Git en {{data.repositoryName}}

- -
-
- - {{ 'searchLabel' | translate }} - - search - {{ 'searchHint' | translate }} - - - Estado - - Fallido - Pendiente - Transfiriendo - Creado con éxito - En progreso - Papelera - Creando archivos auxiliares +
+ + Seleccionar Repositorio + + + {{ repo }} + + hourglass_empty + + + + Seleccionar Rama + + + {{ branch }} + + + hourglass_empty
- +
+ + Buscar commits + + search + Buscar por mensaje del commit + +
+ +
- - diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.spec.ts b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.spec.ts index 187a9b5..63b3eff 100644 --- a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.spec.ts +++ b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ShowGitImagesComponent } from './show-git-images.component'; +import { ShowGitCommitsComponent } from './show-git-images.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ToastrModule } from 'ngx-toastr'; import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; @@ -17,9 +17,9 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -describe('ShowGitImagesComponent', () => { - let component: ShowGitImagesComponent; - let fixture: ComponentFixture; +describe('ShowGitCommitsComponent', () => { + let component: ShowGitCommitsComponent; + let fixture: ComponentFixture; beforeEach(async () => { const mockConfigService = { @@ -27,7 +27,7 @@ describe('ShowGitImagesComponent', () => { }; await TestBed.configureTestingModule({ - declarations: [ShowGitImagesComponent, LoadingComponent], + declarations: [ShowGitCommitsComponent, LoadingComponent], imports: [ HttpClientTestingModule, ToastrModule.forRoot(), @@ -52,7 +52,7 @@ describe('ShowGitImagesComponent', () => { }) .compileComponents(); - fixture = TestBed.createComponent(ShowGitImagesComponent); + fixture = TestBed.createComponent(ShowGitCommitsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts index 45728b9..b7a368b 100644 --- a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts +++ b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts @@ -16,12 +16,12 @@ import {BackupImageComponent} from "../backup-image/backup-image.component"; import {EditImageComponent} from "../edit-image/edit-image.component"; @Component({ - selector: 'app-show-git-images', + selector: 'app-show-git-commits', templateUrl: './show-git-images.component.html', styleUrl: './show-git-images.component.css' }) -export class ShowGitImagesComponent implements OnInit{ -baseUrl: string; +export class ShowGitCommitsComponent implements OnInit{ + baseUrl: string; private apiUrl: string; dataSource = new MatTableDataSource(); length: number = 0; @@ -32,46 +32,54 @@ baseUrl: string; alertMessage: string | null = null; repository: any = {}; datePipe: DatePipe = new DatePipe('es-ES'); + + // Nuevas propiedades para manejar branches + branches: string[] = []; + selectedBranch: string = ''; + loadingBranches: boolean = false; + + // Nuevas propiedades para manejar repositorios + repositories: string[] = []; + selectedRepository: string = ''; + loadingRepositories: boolean = false; + + private initialLoad = true; + columns = [ { - columnDef: 'id', - header: 'Id', - cell: (image: any) => `${image.id}` + columnDef: 'hexsha', + header: 'Commit ID', + cell: (commit: any) => commit.hexsha }, { - columnDef: 'repositoryName', - header: 'Nombre del repositorio', - cell: (image: any) => image.image?.name + columnDef: 'message', + header: 'Mensaje del commit', + cell: (commit: any) => commit.message }, { - columnDef: 'name', - header: 'Nombre de imagen', - cell: (image: any) => image.name + columnDef: 'committed_date', + header: 'Fecha del commit', + cell: (commit: any) => `${this.datePipe.transform(commit.committed_date * 1000, 'dd/MM/yyyy hh:mm:ss')}` }, { - columnDef: 'tag', - header: 'Tag', - cell: (image: any) => image.tag + columnDef: 'size', + header: 'Tamaño', + cell: (commit: any) => `${commit.size} bytes` }, { - columnDef: 'isGlobal', - header: 'Imagen global', - cell: (image: any) => image.image?.isGlobal + columnDef: 'stats_total', + header: 'Estadísticas', + cell: (commit: any) => { + if (commit.stats_total) { + return `+${commit.stats_total.insertions} -${commit.stats_total.deletions} (${commit.stats_total.files} archivos)`; + } + return ''; + } }, { - columnDef: 'status', - header: 'Estado', - cell: (image: any) => image.status - }, - { - columnDef: 'description', - header: 'Descripción', - cell: (image: any) => image.description - }, - { - columnDef: 'createdAt', - header: 'Fecha de creación', - cell: (image: any) => `${this.datePipe.transform(image.createdAt, 'dd/MM/yyyy hh:mm:ss')}` + columnDef: 'tags', + header: 'Tags', + cell: (commit: any) => commit.tags?.length > 0 ? commit.tags.join(', ') : 'Sin tags' } ]; displayedColumns = [...this.columns.map(column => column.columnDef), 'actions']; @@ -83,214 +91,131 @@ baseUrl: string; private joyrideService: JoyrideService, private configService: ConfigService, private router: Router, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any ) { this.baseUrl = this.configService.apiUrl; - this.apiUrl = `${this.baseUrl}/git-image-repositories`; + this.apiUrl = `${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}`; } ngOnInit(): void { if (this.data) { - this.loadData(); + this.loadRepositories(); } } + loadRepositories(): void { + this.loadingRepositories = true; + this.http.get(`${this.apiUrl}/get-collection`).subscribe( + data => { + this.repositories = data.repositories || []; + this.loadingRepositories = false; + if (this.repositories.length > 0) { + this.selectedRepository = this.repositories[0]; + this.loadBranches(); + } + }, + error => { + console.error('Error fetching repositories', error); + this.toastService.error('Error al cargar los repositorios'); + this.loadingRepositories = false; + } + ); + } + + onRepositoryChange(): void { + this.selectedBranch = ''; + this.branches = []; + this.page = 0; + this.loadBranches(); + } + + loadBranches(): void { + if (!this.selectedRepository) { + return; + } + this.loadingBranches = true; + this.http.post(`${this.apiUrl}/branches`, { repositoryName: this.selectedRepository }).subscribe( + data => { + this.branches = data.branches || []; + this.loadingBranches = false; + if (this.branches.length > 0) { + this.selectedBranch = this.branches[0]; + this.loadData(); + if (this.initialLoad) { + this.initialLoad = false; + } + } + }, + error => { + console.error('Error fetching branches', error); + this.toastService.error('Error al cargar las ramas del repositorio'); + this.loadingBranches = false; + } + ); + } + + onBranchChange(): void { + this.page = 0; // Resetear a la primera página + this.loadData(); + } + loadData(): void { + if (!this.selectedBranch || !this.selectedRepository) { + return; + } + this.loading = true; - this.http.get(`${this.apiUrl}?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&repository.id=${this.data.repositoryId}`, { params: this.filters }).subscribe( + const payload = { + repositoryName: this.selectedRepository, + branch: this.selectedBranch + }; + + this.http.post(`${this.apiUrl}/commits`, payload).subscribe( data => { - this.dataSource.data = data['hydra:member']; - this.length = data['hydra:totalItems']; + this.dataSource.data = data.commits || []; + this.length = data.commits?.length || 0; this.loading = false; }, error => { - console.error('Error fetching image repositories', error); + console.error('Error fetching commits', error); + this.toastService.error('Error al cargar los commits'); + this.loading = false; } - ) - } - - getStatusLabel(status: string): string { - switch (status) { - case 'pending': - return 'Pendiente'; - case 'in-progress': - return 'En progreso'; - case 'aux-files-pending': - return 'Archivos auxiliares pendientes'; - case 'success': - return 'Creado con éxito'; - case 'trash': - return 'Papelera temporal'; - case 'failed': - return 'Fallido'; - case 'transferring': - return 'Transfiriendo'; - default: - return 'Estado desconocido'; - } + ); } onPageChange(event: any): void { this.page = event.pageIndex; this.itemsPerPage = event.pageSize; this.length = event.length; - this.loadData(); + // Para commits, no necesitamos paginación del servidor ya que obtenemos todos los commits + // La paginación se maneja en el cliente } - loadImageAlert(image: any): Observable { - return this.http.get(`${this.apiUrl}/server/${image.uuid}/get`, {}); - } - - importImage(): void { - this.dialog.open(ImportImageComponent, { - width: '600px', - data: { - repositoryUuid: this.data.repositoryUuid, - name: this.data.repositoryName - } - }).afterClosed().subscribe((result) => { - if (result) { - this.loadData(); - } - }); - } - - toggleAction(image: any, action:string): void { + toggleAction(commit: any, action: string): void { switch (action) { - case 'delete-trash': - if (!image.imageFullsum) { - const dialogRef = this.dialog.open(DeleteModalComponent, { - width: '400px', - data: { name: image.name }, - }); - - dialogRef.afterClosed().subscribe((result) => { - this.http.delete(`${this.baseUrl}${image['@id']}`).subscribe({ - next: () => { - this.toastService.success('Image deleted successfully'); - this.loadData() - }, - error: (error) => { - this.toastService.error('Error deleting image'); - } - }); - }); - } else { - this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/delete-trash`, - { repository: `/image-repositories/${this.data.repositoryUuid}` }) - .subscribe({ - next: () => { - this.toastService.success('Petición de eliminación de la papelera temporal enviada'); - this.loadData() - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - } - }); - } - - break; - case 'delete-permanent': - this.dialog.open(DeleteModalComponent, { - width: '300px', - data: { name: image.name }, - }).afterClosed().subscribe((result) => { - if (result) { - this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/delete-permanent`, {}).subscribe({ - next: () => { - this.toastService.success('Petición de eliminación de la papelera temporal enviada'); - this.loadData() - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - } - }); - } - }); - break; - case 'edit': - this.dialog.open(EditImageComponent, { - width: '600px', + case 'view-details': + this.dialog.open(ServerInfoDialogComponent, { + width: '800px', data: { - image: image, - } - }).afterClosed().subscribe((result) => { - if (result) { - this.loadData(); + title: 'Detalles del Commit', + content: { + 'Commit ID': commit.hexsha, + 'Mensaje': commit.message, + 'Fecha': this.datePipe.transform(commit.committed_date * 1000, 'dd/MM/yyyy hh:mm:ss'), + 'Tamaño': `${commit.size} bytes`, + 'Archivos modificados': commit.stats_total?.files || 0, + 'Líneas añadidas': commit.stats_total?.insertions || 0, + 'Líneas eliminadas': commit.stats_total?.deletions || 0, + 'Tags': commit.tags?.join(', ') || 'Sin tags' + } } }); break; - case 'recover': - this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/recover`, {}).subscribe({ - next: () => { - this.toastService.success('Petición de recuperación de la imagen enviada'); - this.loadData() - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - } - }); - break; - case 'transfer': - this.http.get(`${this.baseUrl}${image.image['@id']}`).subscribe({ - next: (response) => { - this.dialog.open(ExportImageComponent, { - width: '600px', - data: { - image: response, - imageImageRepository: image - } - }); - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - } - }); - break; - case 'transfer-global': - this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/transfer-global`, { - }).subscribe({ - next: (response) => { - this.toastService.success('Petición de exportación de imagen realizada correctamente'); - this.loading = false; - this.router.navigate(['/commands-logs']); - }, - error: error => { - this.loading = false; - this.toastService.error('Error en la petición de exportación de imagen'); - } - }); - break; - case 'backup': - this.http.get(`${this.baseUrl}${image.image['@id']}`).subscribe({ - next: (response) => { - this.dialog.open(BackupImageComponent, { - width: '600px', - data: { - image: response, - imageImageRepository: image - } - }); - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - } - }); - break; - case 'show-tags': - this.http.get(`${this.baseUrl}/git-image-repositories/server/${image.uuid}/get-tags`, {}).subscribe({ - next: (response) => { - this.dialog.open(ServerInfoDialogComponent, { - width: '800px', - data: { - repositories: response - } - }); - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - } + case 'copy-commit-id': + navigator.clipboard.writeText(commit.hexsha).then(() => { + this.toastService.success('Commit ID copiado al portapapeles'); }); break; default: @@ -302,55 +227,41 @@ baseUrl: string; iniciarTour(): void { this.joyrideService.startTour({ steps: [ - 'imagesTitleStep', - 'addImageButton', - 'searchImagesField', - 'imagesTable', + 'commitsTitleStep', + 'repositorySelector', + 'branchSelector', + 'searchCommitsField', + 'commitsTable', 'actionsHeader', - 'editImageButton', - 'deleteImageButton', - 'imagesPagination' + 'viewCommitButton', + 'copyCommitButton', + 'commitsPagination' ], showPrevButton: true, themeColor: '#3f51b5' }); } - loadAlert(): Observable { - return this.http.post(`${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/get-collection`, {}); - } - - syncRepository() { - this.http.post(`${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/sync`, {}) - .subscribe(response => { - this.toastService.success('Sincronización completada'); - this.loadData() - }, error => { - console.error('Error al sincronizar', error); - this.toastService.error('Error al sincronizar'); - }); - } - openImageInfoDialog() { - this.loadAlert().subscribe( - response => { - this.alertMessage = response.repositories; - - this.dialog.open(ServerInfoDialogComponent, { - width: '800px', - data: { - repositories: this.alertMessage - } - }); - }, - error => { - console.error('Error al cargar la información del alert', error); + this.dialog.open(ServerInfoDialogComponent, { + width: '800px', + data: { + title: 'Información del Repositorio', + content: { + 'Nombre del repositorio': this.selectedRepository || 'No seleccionado', + 'UUID del repositorio': this.data.repositoryUuid, + 'Rama seleccionada': this.selectedBranch || 'No seleccionada', + 'Total de repositorios': this.repositories.length, + 'Total de ramas': this.branches.length, + 'Total de commits': this.length + } } - ); + }); } - goToPage( image: any) { - window.location.href = `http://192.168.68.20:3000/oggit/${image.image.name}`; + goToPage(commit: any) { + // Abrir el commit en una nueva pestaña (puedes adaptar la URL según tu necesidad) + window.open(`http://localhost:3100/oggit/${this.selectedRepository}/commit/${commit.hexsha}`, '_blank'); } onNoClick(): void { diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index 6e91915..c327f13 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -543,5 +543,42 @@ "clientMacOS": "MacOS client", "clientOgLive": "OGLive client", "clientWindowsSession": "Windows session client", - "clientWindows": "Windows client" + "clientWindows": "Windows client", + "exportCSV": "Export CSV", + "totalTraces": "Total traces", + "todayTraces": "Executed today", + "successful": "Successful", + "failed": "Failed", + "inProgress": "In progress", + "showAdvanced": "Show advanced", + "hideAdvanced": "Hide advanced", + "fromDate": "From", + "toDate": "To", + "sortBy": "Sort by", + "executionDate": "Execution date", + "command": "Command", + "client": "Client", + "showingResults": "Showing {{from}} to {{to}} of {{total}} results", + "refresh": "Refresh", + "autoRefresh": "Auto-refresh", + "viewInput": "View input", + "viewOutput": "View output", + "deleteTrace": "Delete trace", + "cancelTrace": "Cancel trace", + "enterClientName": "Please enter the client name", + "organizationalUnits": "Organizational Units", + "totalEquipments": "Total Equipment", + "onlineEquipments": "Online Equipment", + "offlineEquipments": "Offline Equipment", + "busyEquipments": "Busy Equipment", + "pending": "Pending", + "cancelled": "Cancelled", + "cancelImageTransmission": "Cancel image transmission", + "success": "Success", + "limpiarAcciones": "Clear actions", + "totalClients": "Total clients", + "offline": "Offline", + "online": "Online", + "busy": "Busy", + "cancelTask": "Cancel task" } diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index 16c0409..9c7f9c1 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -546,5 +546,45 @@ "clientMacOS": "Cliente MacOS", "clientOgLive": "Cliente OGLive", "clientWindowsSession": "Cliente con sesión Windows", - "clientWindows": "Cliente Windows" + "clientWindows": "Cliente Windows", + "colaAcciones": "Cola de acciones", + "exportCSV": "Exportar CSV", + "totalTraces": "Total de trazas", + "todayTraces": "Ejecutadas hoy", + "successful": "Exitoso", + "failed": "Fallido", + "inProgress": "En progreso", + "showAdvanced": "Mostrar avanzados", + "hideAdvanced": "Ocultar avanzados", + "fromDate": "Desde", + "toDate": "Hasta", + "sortBy": "Ordenar por", + "executionDate": "Fecha de ejecución", + "command": "Comando", + "client": "Cliente", + "showingResults": "Mostrando {{from}} a {{to}} de {{total}} resultados", + "refresh": "Actualizar", + "autoRefresh": "Auto-actualizar", + "viewInput": "Ver entrada", + "viewOutput": "Ver salida", + "deleteTrace": "Eliminar traza", + "cancelTrace": "Cancelar traza", + "enterClientName": "Por favor, ingrese el nombre del cliente", + "organizationalUnits": "Unidades Organizacionales", + "totalEquipments": "Total de Equipos", + "onlineEquipments": "Equipos Online", + "offlineEquipments": "Equipos Offline", + "busyEquipments": "Equipos Ocupados", + "organizationalStructure": "Estructura Organizacional", + "pending": "Pendiente", + "cancelled": "Cancelado", + "cancelImageTransmission": "Cancelar transmisión de imagen", + "success": "Exitoso", + "limpiarAcciones": "Limpiar acciones", + "totalClients": "Total de clientes", + "offline": "Offline", + "online": "Online", + "busy": "Ocupado", + "cancelTask": "Cancelar tarea" } + \ No newline at end of file diff --git a/ogWebconsole/src/styles.css b/ogWebconsole/src/styles.css index 1612d8e..d399c19 100644 --- a/ogWebconsole/src/styles.css +++ b/ogWebconsole/src/styles.css @@ -10,6 +10,58 @@ body { font-family: Roboto, "Helvetica Neue", sans-serif; } +/* Estilos globales para asegurar que el botón del sidebar sea visible */ +.sidebar-toggle-button { + display: flex !important; + align-items: center !important; + justify-content: center !important; + min-width: 48px !important; + height: 48px !important; + border-radius: 50% !important; + transition: all 0.3s ease !important; + color: #3f51b5 !important; + background-color: #ff0000 !important; /* Fondo rojo para hacerlo visible */ + border: 2px solid #000 !important; /* Borde negro para hacerlo visible */ + cursor: pointer !important; + z-index: 1001 !important; + position: relative !important; + opacity: 1 !important; + visibility: visible !important; +} + +.sidebar-toggle-button:hover { + background-color: rgba(63, 81, 181, 0.1) !important; + transform: scale(1.05) !important; + box-shadow: 0 2px 8px rgba(63, 81, 181, 0.2) !important; +} + +.sidebar-toggle-button mat-icon { + color: #ffffff !important; /* Color blanco para que sea visible sobre el fondo rojo */ + font-size: 24px !important; + width: 24px !important; + height: 24px !important; + line-height: 24px !important; +} + +/* Asegurar que todos los botones mat-icon-button en el toolbar sean visibles */ +mat-toolbar button[mat-icon-button] { + display: flex !important; + align-items: center !important; + justify-content: center !important; + min-width: 48px !important; + height: 48px !important; + border-radius: 50% !important; + transition: all 0.3s ease !important; + color: #3f51b5 !important; + background-color: transparent !important; + border: none !important; + cursor: pointer !important; + z-index: 1001 !important; + position: relative !important; + flex-shrink: 0 !important; + opacity: 1 !important; + visibility: visible !important; +} /* Clase general para el contenedor de carga */ .loading-container { @@ -101,3 +153,47 @@ body { gap: 1em; padding: 1.5em; } + +/* Overlay blur reutilizable para modales */ +.modal-overlay-blur { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.7); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 9999; + color: white; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); /* Para Safari */ +} + +.modal-overlay-blur p { + margin-top: 16px; + font-size: 16px; + font-weight: 500; +} + +.modal-overlay-blur .spinner-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +/* Variantes del overlay */ +.modal-overlay-blur.success { + background: rgba(40, 167, 69, 0.8); +} + +.modal-overlay-blur.warning { + background: rgba(255, 193, 7, 0.8); +} + +.modal-overlay-blur.error { + background: rgba(220, 53, 69, 0.8); +}
{{ column.header }} - - - {{ image.isGlobal ? 'Sí' : 'No' }} - + + + + {{ column.cell(commit) }} + - - - {{ getStatusLabel(image[column.columnDef]) }} - + +
+ {{ column.cell(commit) }} +
- - {{ column.cell(image) }} + +
+ {{ column.cell(commit) }} +
+
+ +
+ + {{ tag }} + + + Sin tags + +
+
+ + {{ column.cell(commit) }}
Acciones - + +
+ + + - - - - - - - - - - + +