-
-
+
cloud
@@ -41,7 +38,9 @@
-
+
storage
@@ -52,7 +51,9 @@
-
+
router
@@ -62,11 +63,22 @@

Estado: Error

+ +
+
+ dns +
+
+

OgCore Server

+

Logs del servidor

+
+
- - - +
+
cloud_off @@ -75,7 +87,6 @@
-
Seleccionar repositorio @@ -88,11 +99,14 @@
-
-
-

{{ getSelectedRepositoryName() }}

+
+

{{ getSelectedRepositoryName() }}

+
@@ -111,16 +125,30 @@
+ +
+
+

Datos del repositorio: {{ getSelectedRepositoryName() }}

+

Información específica del repositorio seleccionado

+
+
+ +
+
-
storage

Selecciona un repositorio

Elige un repositorio de la lista para ver su estado detallado.

-
error_outline @@ -134,15 +162,39 @@
- +
- -
- - +
+
+
+

OgBoot Server

+ +
+ + + + +
+
+

Logs de OgBoot

+

Logs en tiempo real del servidor OgBoot

+
+
+ +
+
@@ -161,14 +213,38 @@
- +
- -
- - +
+
+
+

DHCP Server

+ +
+ + + + +
+
+

Logs de DHCP

+

Logs en tiempo real del servidor DHCP

+
+
+ +
+
@@ -187,12 +263,36 @@
- - +
+ +
+
+
+

OgCore Server

+
+ +
+
+

Logs de OgCore

+

Logs en tiempo real del servidor OgCore

+
+
+ +
+
+
+
+
+ -

Última actualización: {{ lastUpdateTime }}

diff --git a/ogWebconsole/src/app/components/global-status/global-status.component.ts b/ogWebconsole/src/app/components/global-status/global-status.component.ts index 05f74a4..2d09a25 100644 --- a/ogWebconsole/src/app/components/global-status/global-status.component.ts +++ b/ogWebconsole/src/app/components/global-status/global-status.component.ts @@ -1,8 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; import { ConfigService } from '@services/config.service'; -import { MatTabChangeEvent } from '@angular/material/tabs'; -import {ToastrService} from "ngx-toastr"; +import { ToastrService } from "ngx-toastr"; @Component({ selector: 'app-global-status', @@ -31,6 +30,7 @@ export class GlobalStatusComponent implements OnInit { repositoryStatuses: { [key: string]: any } = {}; lastUpdateTime: string = ''; selectedRepositoryUuid: string = ''; + selectedSection: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore' = 'repositories'; ogBootApiUrl: string; ogBootDiskUsage: any = {}; @@ -44,7 +44,8 @@ export class GlobalStatusComponent implements OnInit { isDhcp: boolean = false; isRepository: boolean = false; - // Loading específicos para cada sección + ogCoreApiUrl: string; + loadingOgBootOgLives: boolean = false; loadingOgBootServices: boolean = false; loadingOgBootDisk: boolean = false; @@ -60,6 +61,7 @@ export class GlobalStatusComponent implements OnInit { this.baseUrl = this.configService.apiUrl; this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`; this.dhcpApiUrl = `${this.baseUrl}/og-dhcp/status`; + this.ogCoreApiUrl = `${this.baseUrl}`; this.repositoriesUrl = `${this.baseUrl}/image-repositories`; this.ogBootDiskUsageChartData = []; @@ -290,23 +292,9 @@ export class GlobalStatusComponent implements OnInit { this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorDhcp', false); } - onTabChange(event: MatTabChangeEvent): void { - switch (event.index) { - case 0: - if (this.repositories.length === 0) { - this.loadRepositories(false); - } - break; - case 1: - this.loadOgBootStatus(); - break; - case 2: - this.loadDhcpStatus(); - break; - default: - break; - } - } + + + onRepositoryChange(repositoryUuid: string): void { this.selectedRepositoryUuid = repositoryUuid; @@ -324,6 +312,15 @@ export class GlobalStatusComponent implements OnInit { return selectedRepo ? selectedRepo.name : ''; } + getRepositoryIframeUrl(): string { + const repositoryName = this.getSelectedRepositoryName(); + if (repositoryName) { + const encodedName = encodeURIComponent(repositoryName); + return `https://localhost:3030/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`; + } + return ''; + } + refreshAll(): void { this.loading = true; this.updateLastUpdateTime(); @@ -364,4 +361,71 @@ export class GlobalStatusComponent implements OnInit { } }); } + + + + selectSection(section: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore'): void { + this.selectedSection = section; + + switch (section) { + case 'repositories': + break; + case 'ogboot': + if (!this.ogBootDiskUsage) { + this.loadOgBootStatus(); + } + break; + case 'dhcp': + if (!this.dhcpDiskUsage) { + this.loadDhcpStatus(); + } + break; + case 'ogcore': + // No se necesita cargar nada, solo mostrar los logs + break; + + } + } + + scrollToLogs(): void { + let logsSectionId: string; + switch (this.selectedSection) { + case 'repositories': + logsSectionId = 'repository-logs-section'; + break; + case 'ogboot': + logsSectionId = 'ogboot-logs-section'; + break; + case 'dhcp': + logsSectionId = 'dhcp-logs-section'; + break; + case 'ogcore': + logsSectionId = 'ogcore-logs-section'; + break; + default: + return; + } + + const checkAndScroll = () => { + const logsSection = document.getElementById(logsSectionId); + if (logsSection) { + logsSection.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + return true; + } + return false; + }; + + const interval = setInterval(() => { + if (checkAndScroll()) { + clearInterval(interval); + } + }, 100); + + setTimeout(() => { + clearInterval(interval); + }, 3000); + } } diff --git a/ogWebconsole/src/app/components/global-status/status-tab/status-tab.component.html b/ogWebconsole/src/app/components/global-status/status-tab/status-tab.component.html index eb34459..c70823b 100644 --- a/ogWebconsole/src/app/components/global-status/status-tab/status-tab.component.html +++ b/ogWebconsole/src/app/components/global-status/status-tab/status-tab.component.html @@ -1,9 +1,7 @@
-
-
@@ -32,13 +30,12 @@
{{ 'usedPercentageLabel' | translate }}: - {{ isRepository ? diskUsage.used_percentage : diskUsage.percentage }}% + {{ isRepository ? diskUsage.used_percentage : diskUsage.percentage }}
-
@@ -67,13 +64,12 @@
{{ 'usedPercentageLabel' | translate }}: - {{ ramUsage.used_percentage }}% + {{ ramUsage.used_percentage }}
-
@@ -84,7 +80,7 @@
-
{{ cpuUsage.used_percentage }}%
+
{{ cpuUsage.used_percentage }}
Uso actual
@@ -92,9 +88,7 @@
-
-
@@ -116,7 +110,6 @@
-
@@ -139,9 +132,7 @@
-
-
@@ -171,7 +162,6 @@
-
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 d7c8b69..a1ed15d 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 @@ -675,3 +675,340 @@ mat-form-field { } } +@media (max-width: 768px) { + .scroll-to-top-button { + right: 16px; + bottom: 16px; + } +} + +/* Estilos para el selector de ramas */ +.branch-selector-header { + display: flex; + gap: 16px; + align-items: flex-end; + width: 100%; +} + +.create-branch-button { + background: #4caf50; + color: white; + border: none; + padding: 12px 20px; + border-radius: 8px; + font-weight: 500; + transition: all 0.3s ease; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + white-space: nowrap; + height: 56px; +} + +.create-branch-button:hover:not(:disabled) { + background: #45a049; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); +} + +.create-branch-button:disabled { + background: #ccc; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.create-branch-button mat-icon { + font-size: 18px; + width: 18px; + height: 18px; +} + +/* Estilos para la sección de commits */ +.commits-section { + margin-top: 24px; + padding: 24px; + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #e0e0e0; +} + +.search-container { + margin-bottom: 20px; +} + +.search-string { + width: 100%; +} + +.commits-table-container { + margin-top: 20px; + overflow-x: auto; +} + +.commit-id { + font-family: 'Courier New', monospace; + background: #f5f5f5; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + color: #333; +} + +.commit-message { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.commit-stats { + font-size: 12px; + color: #666; +} + +.commit-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.commit-tags mat-chip { + font-size: 10px; + height: 20px; +} + +.no-tags { + color: #999; + font-size: 12px; + font-style: italic; +} + +.action-buttons { + display: flex; + gap: 8px; + justify-content: center; +} + +.action-buttons button { + transition: all 0.2s ease; +} + +.action-buttons button:hover { + transform: scale(1.1); +} + +.paginator-container { + margin-top: 20px; +} + +/* Responsive para ramas y commits */ +@media (max-width: 768px) { + .branch-selector-header { + flex-direction: column; + align-items: stretch; + } + + .create-branch-button { + height: auto; + padding: 12px 16px; + } + + .commits-section { + padding: 16px; + } + + .commit-message { + max-width: 200px; + } + + .action-buttons { + flex-direction: column; + gap: 4px; + } +} + +.git-info-container { + margin-bottom: 20px; +} + +.git-info-card { + background: #f8f9fa; + border-radius: 8px; + padding: 20px; + color: #495057; + border: 1px solid #e9ecef; + animation: fadeInUp 0.5s ease-out; +} + +.git-info-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.git-info-icon { + font-size: 20px; + width: 20px; + height: 20px; + color: #6c757d; +} + +.git-info-title { + font-size: 16px; + font-weight: 600; + flex-grow: 1; + color: #495057; +} + +.git-loading-spinner { + margin-left: auto; +} + +.git-info-content { + background: #ffffff; + border-radius: 6px; + padding: 16px; + margin-top: 12px; + border: 1px solid #dee2e6; +} + +.git-info-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 16px; +} + +.git-info-item { + display: flex; + flex-direction: column; +} + +.git-form-field { + width: 100%; +} + +.git-item-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: #495057; + margin-right: 4px; +} + + + + + +.git-data-display { + background: #f8f9fa; + border-radius: 4px; + padding: 12px; + margin: 0; + font-family: 'Courier New', monospace; + font-size: 12px; + color: #495057; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; + max-height: 300px; + overflow-y: auto; + border: 1px solid #e9ecef; +} + +.git-info-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + font-size: 14px; + color: #6c757d; +} + +.git-info-empty { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + font-size: 14px; + color: #6c757d; + font-style: italic; +} + +/* Estilos para la advertencia de repositorio no encontrado */ +.repository-warning { + display: flex; + align-items: flex-start; + gap: 12px; + margin-top: 16px; + padding: 16px; + background: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 8px; + color: #856404; +} + +.warning-icon { + font-size: 20px; + width: 20px; + height: 20px; + color: #f39c12; + flex-shrink: 0; + margin-top: 2px; +} + +.warning-content { + flex-grow: 1; +} + +.warning-title { + font-size: 14px; + font-weight: 600; + margin-bottom: 4px; + color: #e67e22; +} + +.warning-message { + font-size: 13px; + line-height: 1.4; + color: #856404; +} + +/* Responsive para el cuadro informativo de Git */ +@media (max-width: 768px) { + .git-info-card { + padding: 16px; + } + + .git-info-header { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .git-loading-spinner { + margin-left: 0; + align-self: flex-end; + } + + .git-data-display { + font-size: 11px; + max-height: 200px; + } + + .git-info-grid { + grid-template-columns: 1fr; + gap: 12px; + } + + .git-info-item { + padding: 10px; + } + + .git-info-value { + font-size: 13px; + } +} \ No newline at end of file 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 5092b02..70eedfe 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,4 +1,5 @@ - + + @@ -20,12 +21,11 @@
- +
-
settings @@ -43,15 +43,81 @@
-
code Configuración Git
-
-
+
+
+
+ info + La imagen en caché a actualizar es: + +
+
+
+
+ + + folder + Repositorio + + + +
+
+ + + account_tree + Rama origen + + + +
+
+ + + call_split + Rama destino + + + + +
+
+ +
+ warning +
+
Repositorio no encontrado
+
+ El repositorio "{{ gitData.repo }}" no existe en el listado de repositorios disponibles. + Es posible que necesites crearlo primero. +
+
+
+
+
+ Cargando información de Git... +
+
+ No se encontró información de Git para esta partición +
+
+
+ +
-
- - 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. - - - -
-
+
+ + 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. + + + - -
-
- - - 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á. - -
- -
- - Seleccione imagen - - Seleccionar imagen para actualizar - {{ image?.name }} - - - Selecciona la imagen existente que quieres actualizar. - -
-
- +
+
+ image + Configuración de imagen monolítica +
+ +
+ + + Crear imagen + + + Actualizar imagen + + +
+
+ info + Crea una nueva imagen con el nombre especificado + Actualiza una imagen existente seleccionada +
+ +
+ + Nombre canónico + + Introduce el nombre para la nueva imagen que se creará. + +
+ +
+ + Seleccione imagen + + Seleccionar imagen para actualizar + {{ image?.name }} + + + Selecciona la imagen existente que quieres actualizar. + +
+
+
storage 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 bf413eb..88a8f57 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 @@ -8,6 +8,7 @@ import { ConfigService } from '@services/config.service'; import {MatDialog} from "@angular/material/dialog"; import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component"; import {CreateRepositoryModalComponent} from "./create-repository-modal/create-repository-modal.component"; +import {CreateBranchModalComponent} from "../../../../repositories/show-git-images/create-branch-modal/create-branch-modal.component"; @Component({ selector: 'app-create-image', @@ -38,12 +39,27 @@ export class CreateClientImageComponent implements OnInit{ loadingGitRepositories: boolean = false; loadingGitImageRepositories: boolean = false; creatingRepository: boolean = false; - gitAction: string = 'create'; monolithicAction: string = 'create'; existingImages: any[] = []; selectedExistingImage: any = null; loadingExistingImages: boolean = false; dataSource = new MatTableDataSource(); + + branches: string[] = []; + selectedBranch: string = ''; + loadingBranches: boolean = false; + + gitData: any = null; + loadingGitData: boolean = false; + destinationBranch: string = ''; + isDestinationBranchEditable: boolean = false; + repositoryNotFound: boolean = false; + newlyCreatedRepository: any = null; + + get hasValidGitData(): boolean { + return this.gitData && this.gitData.repo && this.gitData.branch; + } + columns = [ { columnDef: 'diskNumber', @@ -107,9 +123,19 @@ export class CreateClientImageComponent implements OnInit{ this.clientName = response.name; this.selectedRepository = response.repository; - this.dataSource.data = response.partitions.filter((partition: any) => { + const validPartitions = response.partitions.filter((partition: any) => { return partition.partitionNumber !== 0; }); + + this.dataSource.data = validPartitions; + + const firstValidPartition = validPartitions.find((partition: any) => { + return partition.operativeSystem; + }); + + if (firstValidPartition) { + this.selectedPartition = firstValidPartition; + } } }, (error) => { @@ -141,6 +167,23 @@ export class CreateClientImageComponent implements OnInit{ (response: any) => { this.gitRepositories = response['hydra:member']; this.loadingGitRepositories = false; + + if (this.newlyCreatedRepository) { + + const newRepo = this.gitRepositories.find(repo => + repo.name === this.newlyCreatedRepository.name + ); + + if (newRepo) { + this.selectedGitRepository = newRepo; + this.onGitRepositorySelected(newRepo); + this.toastService.success(`Repositorio "${newRepo.name}" preseleccionado`); + } + + this.newlyCreatedRepository = null; + } + + this.checkRepositoryExists(); }, (error) => { console.error('Error al cargar los repositorios git:', error); @@ -151,7 +194,7 @@ export class CreateClientImageComponent implements OnInit{ loadGitImageRepositories(gitRepository: any) { this.loadingGitImageRepositories = true; - const url = `${this.baseUrl}/git-image-repositories?gitRepository.id=${gitRepository.id}&page=1&itemsPerPage=100`; + const url = `${this.baseUrl}/git-image-repositories?gitRepository.uuid=${gitRepository.uuid}&page=1&itemsPerPage=100`; this.http.get(url).subscribe( (response: any) => { this.gitImageRepositories = response['hydra:member']; @@ -168,47 +211,97 @@ export class CreateClientImageComponent implements OnInit{ this.selectedGitRepository = gitRepository; this.selectedExistingImage = null; this.existingImages = []; + this.selectedBranch = ''; + this.branches = []; + if (gitRepository) { this.loadGitImageRepositories(gitRepository); + this.loadBranches(); } else { this.gitImageRepositories = []; } } - onGitActionSelected(event: any) { - console.log('onGitActionSelected llamado con:', event); - this.gitAction = event.value; - this.selectedExistingImage = null; - this.gitImageName = ''; - - // Si se selecciona 'update' y ya hay un repositorio Git seleccionado, cargar los repositorios de imágenes - if (event.value === 'update' && this.selectedGitRepository) { - this.loadGitImageRepositories(this.selectedGitRepository); + loadBranches(): void { + if (!this.selectedGitRepository) { + return; } - - console.log('Antes del setTimeout'); - // Hacer scroll hacia la sección de partición después de un delay más largo - setTimeout(() => { - console.log('Dentro del setTimeout, llamando a scrollToPartitionSection'); - this.scrollToPartitionSection(); - }, 300); + this.loadingBranches = true; + const url = `${this.baseUrl}/image-repositories/server/git/${this.selectedRepository.uuid}/branches`; + this.http.post(url, { repositoryName: this.selectedGitRepository.name }).subscribe( + data => { + this.branches = data.branches || []; + this.loadingBranches = false; + if (this.branches.length > 0) { + this.selectedBranch = this.branches[0]; + } + }, + error => { + console.error('Error fetching branches', error); + this.toastService.error('Error al cargar las ramas del repositorio'); + this.loadingBranches = false; + } + ); + } + + onBranchChange(): void { + // La rama ha sido seleccionada, no necesitamos hacer nada más + } + + openCreateBranchModal(): void { + if (this.hasValidGitData) { + const dialogRef = this.dialog.open(CreateBranchModalComponent, { + width: '500px', + data: { + commit: this.gitData.branch, + repositoryName: this.gitData.repo, + repositoryUuid: this.selectedRepository.uuid + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.toastService.success('Rama creada correctamente'); + this.destinationBranch = result.branchName || result; + this.loadBranches(); + } + }); + return; + } + + if (!this.selectedGitRepository) { + this.toastService.error('Debe seleccionar un repositorio primero'); + return; + } + + const dialogRef = this.dialog.open(CreateBranchModalComponent, { + width: '500px', + data: { + commit: this.selectedBranch || 'master', + repositoryName: this.selectedGitRepository.name, + repositoryUuid: this.selectedRepository.uuid + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.toastService.success('Rama creada correctamente'); + this.destinationBranch = result.branchName || result; + this.loadBranches(); + } + }); } onMonolithicActionSelected(event: any) { - console.log('onMonolithicActionSelected llamado con:', event); this.monolithicAction = event.value; this.selectedImage = null; this.name = ''; - // Si se selecciona 'update', cargar las imágenes existentes if (event.value === 'update') { this.loadImages(); } - console.log('Antes del setTimeout (monolithic)'); - // Hacer scroll hacia la sección de partición después de un delay más largo setTimeout(() => { - console.log('Dentro del setTimeout (monolithic), llamando a scrollToPartitionSection'); this.scrollToPartitionSection(); }, 300); } @@ -217,8 +310,7 @@ export class CreateClientImageComponent implements OnInit{ if (!this.selectedExistingImage) return; this.loadingExistingImages = true; - // Aquí deberías hacer el GET al endpoint externo - // Por ahora uso un endpoint de ejemplo, ajusta según tu API + const url = `${this.baseUrl}/images?gitImageRepository.id=${this.selectedExistingImage.id}&page=1&itemsPerPage=100`; this.http.get(url).subscribe( @@ -238,14 +330,15 @@ export class CreateClientImageComponent implements OnInit{ this.selectedGitRepository = null; this.selectedExistingImage = null; this.gitImageName = ''; - this.gitAction = 'create'; + this.monolithicAction = 'create'; this.existingImages = []; this.gitRepositories = []; this.gitImageRepositories = []; + this.selectedBranch = ''; + this.branches = []; this.selectedImage = null; this.name = ''; - this.monolithicAction = 'create'; } resetCanonicalName() { @@ -258,16 +351,6 @@ export class CreateClientImageComponent implements OnInit{ return; } - if (this.imageType === 'git') { - if (!this.selectedGitRepository) { - this.toastService.error('Debes seleccionar un repositorio Git'); - return; - } - if (this.gitAction === 'update' && !this.selectedExistingImage) { - this.toastService.error('Debes seleccionar un repositorio de imágenes Git'); - return; - } - } if (this.imageType === 'monolithic') { if (this.monolithicAction === 'create' && !this.name) { @@ -295,24 +378,46 @@ export class CreateClientImageComponent implements OnInit{ if (result !== undefined) { this.loading = true; - let payload: any = { - client: `/clients/${this.clientId}`, - partition: this.selectedPartition['@id'], - source: 'assistant', - type: this.imageType, - queue: result - }; + let endpoint: string; + let payload: any; if (this.imageType === 'git') { - payload.gitRepository = this.selectedGitRepository.name - payload.name = this.selectedGitRepository.name; + const gitRepoName = this.hasValidGitData ? this.gitData.repo : this.selectedGitRepository?.name; + const originBranch = this.hasValidGitData ? this.gitData.branch : this.selectedBranch; - if (this.gitAction === 'create') { - payload.action = 'create'; + if (this.branches.length === 0) { + endpoint = `${this.baseUrl}/images`; + payload = { + client: `/clients/${this.clientId}`, + partition: this.selectedPartition['@id'], + type: this.imageType, + gitRepository: gitRepoName, + name: gitRepoName, + originalBranch: originBranch, + destinationBranch: this.destinationBranch, + action: 'create', + queue: result + }; } else { - payload.action = 'update'; + endpoint = `${this.baseUrl}/git-repositories/update-image`; + payload = { + client: `/clients/${this.clientId}`, + partition: this.selectedPartition['@id'], + gitRepository: gitRepoName, + originalBranch: originBranch, + destinationBranch: this.destinationBranch, + queue: result + }; } } else { + endpoint = `${this.baseUrl}/images`; + payload = { + client: `/clients/${this.clientId}`, + partition: this.selectedPartition['@id'], + type: this.imageType, + queue: result + }; + if (this.monolithicAction === 'create') { payload.name = this.name; payload.action = 'create'; @@ -322,13 +427,13 @@ export class CreateClientImageComponent implements OnInit{ } } - this.http.post(`${this.baseUrl}/images`, payload) + this.http.post(endpoint, payload) .subscribe({ next: (response) => { let actionText = 'creación'; - if (this.imageType === 'git' && this.gitAction === 'update') { + if (this.imageType === 'monolithic' && this.monolithicAction === 'update') { actionText = 'actualización'; - } else if (this.imageType === 'monolithic' && this.monolithicAction === 'update') { + } else if (this.imageType === 'git' && this.branches.length > 0) { actionText = 'actualización'; } this.toastService.success(`Petición de ${actionText} de imagen enviada`); @@ -359,14 +464,28 @@ export class CreateClientImageComponent implements OnInit{ dialogRef.afterClosed().subscribe(result => { this.creatingRepository = false; if (result) { + + + this.newlyCreatedRepository = result; + this.loadGitRepositories(); + setTimeout(() => { - const newRepository = this.gitRepositories.find(repo => repo['@id'] === result['@id']); - if (newRepository) { - this.selectedGitRepository = newRepository; - this.onGitRepositorySelected(newRepository); + if (this.newlyCreatedRepository && !this.selectedGitRepository) { + const newRepo = this.gitRepositories.find(repo => + repo.name === this.newlyCreatedRepository.name + ); + + if (newRepo) { + this.selectedGitRepository = newRepo; + this.onGitRepositorySelected(newRepo); + this.toastService.success(`Repositorio "${newRepo.name}" preseleccionado`); + } + + this.newlyCreatedRepository = null; } - }, 200); + }, 1000); + } else { } }); } @@ -383,8 +502,13 @@ export class CreateClientImageComponent implements OnInit{ this.selectedImage = null; this.name = ''; this.monolithicAction = 'create'; + + if (this.selectedPartition) { + this.loadGitData(this.selectedPartition); + } } else { this.resetGitSelections(); + this.gitData = null; } } @@ -414,5 +538,82 @@ export class CreateClientImageComponent implements OnInit{ set selectedPartition(value: any) { this._selectedPartition = value; + + if (value && this.imageType === 'git') { + this.loadGitData(value); + } else { + this.gitData = null; + } + } + + loadGitData(partition: any): void { + this.loadingGitData = true; + this.gitData = null; + this.repositoryNotFound = false; + + const payload = { + partition: partition['@id'], + client: `/clients/${this.clientId}` + }; + + this.http.post(`${this.baseUrl}/git-repositories/get-git-data`, payload).subscribe({ + next: (response) => { + this.gitData = response; + if (this.gitData && this.gitData.branch) { + this.destinationBranch = this.gitData.branch; + } + this.loadingGitData = false; + + this.checkRepositoryExists(); + + if (this.hasValidGitData) { + this.loadBranchesForGitData(); + } + }, + error: (error) => { + console.error('Error al cargar datos de Git:', error); + this.toastService.error('Error al cargar información de Git'); + this.loadingGitData = false; + } + }); + } + + checkRepositoryExists(): void { + if (this.gitData && this.gitData.repo && this.gitRepositories.length > 0) { + const repoExists = this.gitRepositories.some((repo: any) => + repo.name === this.gitData.repo + ); + this.repositoryNotFound = !repoExists; + } else { + this.repositoryNotFound = false; + } + } + + loadBranchesForGitData(): void { + if (!this.gitData || !this.gitData.repo) { + return; + } + + this.loadingBranches = true; + const url = `${this.baseUrl}/image-repositories/server/git/${this.selectedRepository.uuid}/branches`; + this.http.post(url, { repositoryName: this.gitData.repo }).subscribe( + data => { + this.branches = data.branches || []; + this.loadingBranches = false; + }, + error => { + console.error('Error fetching branches for Git data', error); + this.loadingBranches = false; + } + ); + } + + toggleDestinationBranchEdit(): void { + this.isDestinationBranchEditable = !this.isDestinationBranchEditable; + + if (!this.isDestinationBranchEditable) { + // Opcional: Aquí se pueden agregar validaciones adicionales + console.log('Rama destino guardada:', this.destinationBranch); + } } } 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 8cdb3e0..9c60bbe 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 @@ -519,4 +519,73 @@ table { } } +.commits-table-container { + margin-top: 20px; +} + +::ng-deep .mat-chip.mat-standard-chip.mat-chip-selected.mat-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; + color: white !important; + border-radius: 20px !important; + font-weight: 500 !important; + font-size: 12px !important; + padding: 8px 16px !important; + margin: 2px !important; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3) !important; + transition: all 0.3s ease !important; + border: none !important; +} + +::ng-deep .mat-chip.mat-standard-chip.mat-chip-selected.mat-primary:hover { + transform: translateY(-1px) !important; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important; +} + +::ng-deep .mat-chip.mat-standard-chip.mat-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; + color: white !important; + border-radius: 20px !important; + font-weight: 500 !important; + font-size: 12px !important; + padding: 8px 16px !important; + margin: 2px !important; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3) !important; + transition: all 0.3s ease !important; + border: none !important; +} + +::ng-deep .mat-chip.mat-standard-chip.mat-primary:hover { + transform: translateY(-1px) !important; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important; +} + +::ng-deep .custom-tag-chip.mat-chip.mat-standard-chip.mat-chip-selected.mat-primary { + background: rgba(102, 126, 234, 0.1) !important; + color: #667eea !important; + border: 1px solid rgba(102, 126, 234, 0.3) !important; + border-radius: 8px !important; + font-weight: 500 !important; + font-size: 10px !important; + padding: 2px 6px !important; + margin: 1px !important; + box-shadow: none !important; + transition: all 0.3s ease !important; +} + +::ng-deep .custom-tag-chip.mat-chip.mat-standard-chip.mat-chip-selected.mat-primary:hover { + background: rgba(102, 126, 234, 0.15) !important; + border-color: rgba(102, 126, 234, 0.5) !important; + transform: translateY(-1px) !important; +} + +.no-tags-text { + color: #999; + font-style: italic; + font-size: 12px; + padding: 4px 8px; + background: #f8f9fa; + border-radius: 12px; + border: 1px dashed #dee2e6; +} + 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 442a438..59bf1f5 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 @@ -129,11 +129,18 @@ hourglass_empty + + + Tag + + No + + +
- -
- +
+
@@ -172,7 +182,6 @@
Seleccionar @@ -162,8 +169,11 @@ Tags - {{ tag }} - Sin tags + + {{ tag }} + + Sin tags
-
Seleccione método de deploy 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 707e9e9..14a0c04 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 @@ -109,6 +109,9 @@ export class DeployImageComponent implements OnInit{ selectedCommit: any = null; private initialGitLoad = true; + showOnlyTagged: boolean = false; + filteredCommits: any[] = []; + constructor( private http: HttpClient, private toastService: ToastrService, @@ -387,7 +390,7 @@ export class DeployImageComponent implements OnInit{ } if (this.imageType === 'git') { - if (!this.selectedCommit) { + if (!this.selectedCommit || this.filteredCommits.length === 0) { return false; } } @@ -418,17 +421,28 @@ export class DeployImageComponent implements OnInit{ openScheduleModal(): void { + let scope = this.runScriptContext.type; + let selectedClients = null; + + + if ((!this.runScriptContext || this.runScriptContext.type === 'client' || this.selectedClients.length === 1) && this.selectedClients && this.selectedClients.length > 0) { + scope = 'clients'; + selectedClients = this.selectedClients; + } + const dialogRef = this.dialog.open(CreateTaskComponent, { width: '800px', data: { - scope: this.runScriptContext.type, - organizationalUnit: this.runScriptContext['@id'], - source: 'assistant' + scope: scope, + selectedClients: selectedClients, + organizationalUnit: this.runScriptContext?.['@id'], + source: 'assistant', + runScriptContext: this.runScriptContext } }); dialogRef.afterClosed().subscribe((result: { [x: string]: any; }) => { - if (result) { + if (result !== undefined) { const payload = { method: this.selectedMethod, diskNumber: this.selectedPartition.diskNumber, @@ -519,7 +533,9 @@ export class DeployImageComponent implements OnInit{ this.selectedBranch = ''; this.branches = []; this.commits = []; + this.filteredCommits = []; this.selectedCommit = null; + this.showOnlyTagged = false; this.loadGitBranches(); } @@ -545,6 +561,8 @@ export class DeployImageComponent implements OnInit{ onGitBranchChange() { this.selectedCommit = null; this.commits = []; + this.filteredCommits = []; + this.showOnlyTagged = false; this.loadGitCommits(); } @@ -558,6 +576,12 @@ export class DeployImageComponent implements OnInit{ data => { this.commits = data.commits || []; this.loadingCommits = false; + this.filterCommits(); + if (this.filteredCommits.length > 0) { + this.selectedCommit = this.filteredCommits.reduce((a, b) => + new Date(a.committed_date * 1000) > new Date(b.committed_date * 1000) ? a : b + ); + } }, error => { this.toastService.error('Error al cargar los commits'); @@ -565,4 +589,23 @@ export class DeployImageComponent implements OnInit{ } ); } + + filterCommits() { + if (this.showOnlyTagged) { + this.filteredCommits = this.commits.filter(commit => + commit.tags && commit.tags.length > 0 + ); + } else { + this.filteredCommits = this.commits; + } + } + + onTagFilterChange() { + this.filterCommits(); + if (this.filteredCommits.length > 0 && !this.filteredCommits.includes(this.selectedCommit)) { + this.selectedCommit = this.filteredCommits.reduce((a, b) => + new Date(a.committed_date * 1000) > new Date(b.committed_date * 1000) ? a : b + ); + } + } } diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css index baaa2b0..23a405a 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.css @@ -175,6 +175,22 @@ margin-bottom: 20px; } +.selected-disk-hint { + display: flex; + align-items: center; + gap: 8px; + color: #2e7d32; + font-weight: 500; + font-size: 14px; +} + +.hint-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: #4caf50; +} + /* Opciones del select */ ::ng-deep .disk-option { display: flex; @@ -232,7 +248,9 @@ background: #e8f5e8; border: 1px solid #c8e6c9; border-radius: 12px; - margin-top: 16px; + margin-top: 0; + flex-shrink: 0; + min-width: 250px; } .info-icon { @@ -709,8 +727,454 @@ margin: 5px 0 !important; } -/* ===== ESTADOS DE ADVERTENCIA ===== */ -/* Advertencia (90% a 99% usado) */ +/* ===== LAYOUT PRINCIPAL ===== */ +.partition-layout { + display: flex; + flex-direction: column; + gap: 24px; + margin-top: 20px; +} + +/* ===== BARRA DE PROGRESO DE PARTICIONES ===== */ +.partition-progress-container { + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #e0e0e0; + padding: 24px; + margin-bottom: 20px; +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 16px; +} + +.progress-header h3 { + margin: 0; + color: #333; + font-size: 18px; + font-weight: 600; +} + +.disk-info-summary { + display: flex; + gap: 16px; + flex-wrap: wrap; +} + +.disk-info-summary span { + padding: 6px 12px; + border-radius: 6px; + font-size: 14px; + font-weight: 500; +} + +.total-size { + background: #e3f2fd; + color: #1976d2; +} + +.used-size { + background: #fff3e0; + color: #f57c00; +} + +.free-size { + background: #e8f5e8; + color: #388e3c; +} + +.partition-progress-bar { + margin-bottom: 20px; +} + +.progress-segments { + display: flex; + height: 60px; + border-radius: 8px; + overflow: hidden; + border: 2px solid #e0e0e0; + background: #f5f5f5; +} + +.progress-segment { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-width: 20px; + transition: all 0.3s ease; + cursor: pointer; + border-right: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2); +} + +.progress-segment:hover { + filter: brightness(1.2); + transform: scale(1.02); + box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.4), 0 2px 8px rgba(0, 0, 0, 0.2); + z-index: 2; +} + +.progress-segment:last-child { + border-right: none; +} + +.progress-segment.removed { + opacity: 0.3; + background: #ccc !important; +} + +.progress-segment.free-space { + background: linear-gradient(45deg, #e8f5e8, #c8e6c9); + border-left: 1px solid #e0e0e0; +} + +.segment-label { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + color: white; + font-weight: 600; + font-size: 12px; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); + width: 100%; + height: 100%; +} + +.partition-number { + font-size: 14px; + font-weight: bold; +} + +.partition-percentage { + font-size: 10px; + opacity: 0.9; +} + +.free-label { + font-size: 12px; + font-weight: bold; +} + +.free-percentage { + font-size: 10px; + opacity: 0.9; +} + +/* ===== LEYENDA DE PARTICIONES ===== */ +.partition-legend { + display: flex; + flex-wrap: wrap; + gap: 12px; + padding: 20px; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 12px; + border: 1px solid #e9ecef; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.legend-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + background: white; + border-radius: 8px; + border: 1px solid #e0e0e0; + font-size: 14px; + transition: all 0.3s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.legend-item:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + transform: translateY(-1px); +} + +.legend-item.removed { + opacity: 0.5; + text-decoration: line-through; +} + +.legend-color { + width: 18px; + height: 18px; + border-radius: 4px; + border: 2px solid rgba(255, 255, 255, 0.8); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; +} + +.legend-item:hover .legend-color { + transform: scale(1.1); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.legend-color.free-color { + background: linear-gradient(45deg, #e8f5e8, #c8e6c9); +} + +.legend-text { + font-weight: 600; + color: #333; + font-size: 14px; +} + +.legend-size { + font-size: 12px; + color: #666; + font-weight: 500; + background: #f8f9fa; + padding: 2px 6px; + border-radius: 4px; + border: 1px solid #e9ecef; +} + +/* ===== TABLA MAT-TABLE ===== */ +.table-container { + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid #e0e0e0; + overflow: hidden; +} + +.table-header { + padding: 20px 24px 16px 24px; + border-bottom: 1px solid #e0e0e0; + background: #f8f9fa; +} + +.table-header h3 { + margin: 0; + color: #333; + font-size: 18px; + font-weight: 600; +} + +.partition-mat-table { + width: 100%; + background: white; +} + +.partition-mat-table .mat-header-cell { + background: #f8f9fa; + color: #495057; + font-weight: 600; + font-size: 14px; + padding: 16px 12px; + border-bottom: 2px solid #e9ecef; +} + +.partition-mat-table .mat-cell { + padding: 12px; + border-bottom: 1px solid #f1f3f4; + vertical-align: middle; +} + +.partition-mat-table .mat-row:hover { + background: #f8f9fa; +} + +/* Campos compactos para la tabla */ +.compact-form-field { + width: 100%; + margin: 0; +} + +.compact-form-field .mat-form-field-wrapper { + padding-bottom: 0; + margin: 0; +} + +.compact-form-field .mat-form-field-infix { + padding: 2px 0; + min-height: 20px; + font-size: 13px; +} + +.compact-form-field .mat-form-field-outline { + border-radius: 4px; +} + +.compact-form-field .mat-form-field-outline-start, +.compact-form-field .mat-form-field-outline-end { + border-width: 1px; +} + +.compact-form-field .mat-form-field-outline-gap { + border-width: 1px; +} + +/* Estilos para inputs y selects compactos */ +.compact-form-field input, +.compact-form-field .mat-select { + line-height: 1.2; +} + +.compact-form-field .mat-select-value { + color: #333; +} + +/* Reducir el padding de las celdas de la tabla */ +.partition-mat-table .mat-cell { + padding: 6px 6px; + vertical-align: middle; +} + +.partition-mat-table .mat-header-cell { + padding: 10px 6px; +} + +/* Hacer los inputs más pequeños */ +.compact-form-field .mat-form-field-infix { + padding: 2px 0; + min-height: 20px; +} + +/* Ajustar el tamaño de los selects */ +.compact-form-field .mat-select-trigger { + height: 20px; +} + +/* Ajustar el tamaño de los inputs numéricos */ +.compact-form-field input[type="number"] { + padding: 2px 6px; +} + +/* Reducir el espacio del wrapper del form field */ +.compact-form-field .mat-form-field-wrapper { + padding-bottom: 0; + margin: 0; + line-height: 1.2; +} + +/* Ajustar el espacio del outline */ +.compact-form-field .mat-form-field-outline { + top: 0; + bottom: 0; +} + +.compact-form-field .mat-form-field-outline-start, +.compact-form-field .mat-form-field-outline-end, +.compact-form-field .mat-form-field-outline-gap { + border-width: 1px; +} + +/* Checkbox en la tabla */ +.partition-mat-table .mat-checkbox { + margin: 0; +} + +/* Botón de eliminar */ +.partition-mat-table .mat-icon-button { + width: 32px; + height: 32px; + line-height: 32px; +} + +.partition-mat-table .mat-icon { + font-size: 18px; + width: 18px; + height: 18px; +} + +/* ===== RESPONSIVE ===== */ +@media (max-width: 768px) { + .partition-layout { + gap: 16px; + } + + .progress-header { + flex-direction: column; + align-items: flex-start; + } + + .disk-info-summary { + width: 100%; + justify-content: space-between; + } + + .partition-legend { + flex-direction: column; + gap: 8px; + } + + .legend-item { + justify-content: space-between; + } + + .partition-mat-table { + font-size: 12px; + } + + .compact-form-field { + font-size: 12px; + } +} + +@media (max-width: 480px) { + .progress-segments { + height: 40px; + } + + .segment-label { + font-size: 10px; + } + + .partition-number { + font-size: 12px; + } + + .partition-percentage { + font-size: 8px; + } + + .partition-mat-table .mat-header-cell, + .partition-mat-table .mat-cell { + padding: 8px 6px; + } +} + +.progress-segment { + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: scaleX(0); + } + to { + opacity: 1; + transform: scaleX(1); + } +} + +.legend-item { + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + + .warning { color: #ff9800 !important; } @@ -723,7 +1187,6 @@ color: #ff9800 !important; } -/* Peligro (100% o más usado) */ .danger { color: #f44336 !important; font-weight: bold !important; @@ -751,7 +1214,6 @@ } } -/* ===== INSTRUCCIONES ===== */ .instructions-box { margin-top: 15px; background-color: #f5f5f5; @@ -780,7 +1242,6 @@ line-height: 1.5; } -/* ===== RESPONSIVE ===== */ @media (max-width: 768px) { .header-container { flex-direction: column; @@ -844,6 +1305,73 @@ } } +.partition-validation-indicator { + margin: 16px 0; + padding: 16px 20px; + border-radius: 12px; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.validation-status { + display: flex; + align-items: center; + gap: 12px; + font-size: 14px; + font-weight: 500; + padding: 12px 16px; + border-radius: 8px; + border: 2px solid; +} + +.validation-status.loading { + color: #1976d2; + background: #e3f2fd; + border-color: #bbdefb; +} + +.validation-status.success { + color: #2e7d32; + background: #e8f5e8; + border-color: #4caf50; +} + +.validation-status.error { + color: #d32f2f; + background: #ffebee; + border-color: #f44336; +} + +.validation-icon { + font-size: 20px; + width: 20px; + height: 20px; +} + +.validation-icon.loading { + color: #1976d2; + animation: spin 1s linear infinite; +} + +.validation-icon.success { + color: #2e7d32; +} + +.validation-icon.error { + color: #d32f2f; +} + +.validation-message { + flex: 1; + margin-left: 8px; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + + diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html index 4624d67..74e023b 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.html @@ -16,7 +16,7 @@
- +
@@ -26,7 +26,7 @@
-
@@ -51,7 +51,7 @@
- Selecciona el disco que deseas particionar + Selecciona el disco que deseas particionar + + check_circle + Disco {{ selectedDisk.diskNumber }} seleccionado - {{ (selectedDisk.totalDiskSize / 1024).toFixed(2) }} GB + -
- info -
- Disco seleccionado: {{ selectedDisk.diskNumber }} - Tamaño total: {{ (selectedDisk.totalDiskSize / 1024).toFixed(2) }} GB -
-
-
warning
@@ -207,6 +203,16 @@
+ +
+
+ hourglass_empty + check_circle + error + {{ partitionValidationMessage }} +
+
+ @@ -256,18 +262,21 @@
-
+

Distribución de Particiones

- - +
+ + +
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts index 859f0b3..795b3d1 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/partition-assistant/partition-assistant.component.ts @@ -1,4 +1,4 @@ -import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/core'; +import {Component, EventEmitter, Inject, Input, OnInit, Output, AfterViewInit, OnDestroy, ViewChild, ElementRef, HostListener} from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { ToastrService } from 'ngx-toastr'; import {ActivatedRoute, Router} from "@angular/router"; @@ -8,6 +8,9 @@ import { ConfigService } from '@services/config.service'; import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component"; import {MatDialog} from "@angular/material/dialog"; import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component"; +import { Subject } from 'rxjs'; +import { takeUntil, debounceTime } from 'rxjs/operators'; +import { MatTableDataSource } from '@angular/material/table'; interface Partition { uuid?: string; @@ -28,7 +31,7 @@ interface Partition { templateUrl: './partition-assistant.component.html', styleUrls: ['./partition-assistant.component.css'] }) -export class PartitionAssistantComponent implements OnInit{ +export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDestroy{ baseUrl: string; private apiUrl: string; @Output() dataChange = new EventEmitter(); @@ -47,6 +50,10 @@ export class PartitionAssistantComponent implements OnInit{ runScriptContext: any = null; showInstructions = false; + @ViewChild('chartContainer', { static: false }) chartContainer!: ElementRef; + private destroy$ = new Subject(); + private resizeSubject = new Subject(); + view: [number, number] = [300, 200]; showLegend = true; showLabels = true; @@ -55,6 +62,23 @@ export class PartitionAssistantComponent implements OnInit{ selectedModelClient: any = null; partitionCode: string = ''; generatedInstructions: string = ''; + + // Propiedades para validación de particiones + partitionValidationStatus: 'idle' | 'loading' | 'success' | 'error' = 'idle'; + partitionValidationMessage: string = ''; + private validationDebounceTime = 500; // ms + private validationSubject = new Subject(); + + // Columnas para mat-table + displayedColumns: string[] = ['partitionNumber', 'partitionCode', 'filesystem', 'size', 'percentage', 'format', 'actions']; + + // Paleta de colores para las particiones + private partitionColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', + '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', + '#F8C471', '#82E0AA', '#F1948A', '#85C1E9', '#D7BDE2', + '#A8E6CF', '#FFD3B6', '#FFAAA5', '#DCEDC8', '#FFE0B2' + ]; constructor( private http: HttpClient, @@ -77,10 +101,22 @@ export class PartitionAssistantComponent implements OnInit{ this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null; this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; }); - this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected); + this.selectedClients = this.clientData.filter( + (client: { selected: boolean; status: string }) => client.selected + ); + + if (this.selectedClients.length === 0 && this.clientData.length > 0) { + this.selectedClients = [this.clientData[0]]; + this.clientData[0].selected = true; + } this.selectedModelClient = this.clientData.find((client: { selected: boolean; status: string}) => client.selected) || null; + if (!this.selectedModelClient && this.clientData.length > 0) { + this.selectedModelClient = this.clientData[0]; + this.clientData[0].selected = true; + } + if (this.selectedModelClient) { this.loadPartitions(this.selectedModelClient); } @@ -90,6 +126,55 @@ export class PartitionAssistantComponent implements OnInit{ this.route.queryParams.subscribe(params => { this.runScriptContext = params['runScriptContext'] ? JSON.parse(params['runScriptContext']) : null; }); + + this.resizeSubject.pipe( + takeUntil(this.destroy$), + debounceTime(100) + ).subscribe(() => { + this.resizeChart(); + }); + + this.validationSubject.pipe( + takeUntil(this.destroy$), + debounceTime(this.validationDebounceTime) + ).subscribe(() => { + this.validatePartitionSizes(); + }); + } + + ngAfterViewInit(): void { + setTimeout(() => { + this.resizeChart(); + }, 100); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + @HostListener('window:resize') + onResize(): void { + this.resizeSubject.next(); + } + + private resizeChart(): void { + if (this.chartContainer && this.chartContainer.nativeElement) { + const container = this.chartContainer.nativeElement; + const width = container.offsetWidth; + + if (width > 0) { + const height = Math.max(250, Math.min(450, width * 0.7)); + + if (Math.abs(this.view[0] - width) > 10 || Math.abs(this.view[1] - height) > 10) { + this.view = [width, height]; + + setTimeout(() => { + this.view = [...this.view]; + }, 10); + } + } + } } get selectedDisk():any { @@ -135,7 +220,6 @@ export class PartitionAssistantComponent implements OnInit{ initializeDisks() { this.disks = []; - // Verificar que hay datos válidos if (!this.data || !this.data.partitions || !Array.isArray(this.data.partitions)) { console.warn('No hay datos de particiones válidos'); return; @@ -161,10 +245,10 @@ export class PartitionAssistantComponent implements OnInit{ size: this.convertBytesToGB(partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 512 : partition.size), memoryUsage: partition.memoryUsage, partitionCode: partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 'EFI' : partition.partitionCode, - filesystem: partition.filesystem, + filesystem: partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 'FAT32' : partition.filesystem, sizeBytes: partition.partitionNumber === 1 && this.partitionCode === 'GPT' ? 512 : partition.size, format: false, - color: '#1f1b91', + color: this.getColorForPartition(partition.partitionNumber), percentage: 0, removed: false }); @@ -234,7 +318,14 @@ export class PartitionAssistantComponent implements OnInit{ } updateSelectedClients() { - this.selectedClients = this.clientData.filter((client: { selected: any; }) => client.selected); + this.selectedClients = this.clientData.filter( + (client: { selected: boolean; status: string }) => client.selected + ); + + if (this.selectedClients.length === 0 && this.clientData.length > 0) { + this.selectedClients = [this.clientData[0]]; + this.clientData[0].selected = true; + } } getPartitionsTooltip(client: any): string { @@ -267,12 +358,18 @@ export class PartitionAssistantComponent implements OnInit{ memoryUsage: 0, sizeBytes: 0, format: false, - color: '#' + Math.floor(Math.random() * 16777215).toString(16), + color: this.getNextPartitionColor(), percentage: 0, removed: false }); this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); this.updateDiskChart(disk); + + this.validationSubject.next(); + + setTimeout(() => { + this.resizeChart(); + }, 100); } else { this.toastService.error('No hay suficiente espacio libre en el disco para crear una nueva partición.'); } @@ -297,6 +394,8 @@ export class PartitionAssistantComponent implements OnInit{ partition.percentage = (size / disk.totalDiskSize) * 100; this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); this.updateDiskChart(disk); + + this.validationSubject.next(); } } } @@ -311,6 +410,10 @@ export class PartitionAssistantComponent implements OnInit{ save() { + if (this.selectedClients.length === 0 && this.clientData.length > 0) { + this.updateSelectedClients(); + } + if (!this.selectedDisk) { this.toastService.error('No se ha seleccionado un disco.'); return; @@ -389,6 +492,12 @@ export class PartitionAssistantComponent implements OnInit{ this.updateDiskChart(disk); this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); + + this.validationSubject.next(); + + setTimeout(() => { + this.resizeChart(); + }, 100); } } @@ -401,9 +510,55 @@ export class PartitionAssistantComponent implements OnInit{ } this.updateDiskChart(disk); this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); + + this.validationSubject.next(); } } + validatePartitionSizes(): void { + if (!this.selectedModelClient || !this.selectedDisk) { + return; + } + + this.partitionValidationStatus = 'loading'; + this.partitionValidationMessage = ''; + + const partitions = this.selectedDisk.partitions + .filter((partition: Partition) => !partition.removed) + .map((partition: Partition) => ({ + diskNumber: this.selectedDisk.diskNumber, + partitionNumber: partition.partitionNumber, + size: partition.size, + partitionCode: partition.partitionCode, + filesystem: partition.filesystem + })); + + const payload = { + partitions: partitions + }; + + const url = `${this.baseUrl}${this.selectedModelClient.uuid}/check-partition-sizes`; + + this.http.post(url, payload).subscribe( + (response: any) => { + if (response.res === 1) { + this.partitionValidationStatus = 'success'; + this.partitionValidationMessage = 'Las particiones cumplen con los requisitos del disco.'; + } else if (response.res === 2) { + this.partitionValidationStatus = 'error'; + this.partitionValidationMessage = response.der || 'Las particiones no cumplen con los requisitos del disco.'; + } else { + this.partitionValidationStatus = 'error'; + this.partitionValidationMessage = 'Respuesta inesperada del servidor.'; + } + }, + (error) => { + this.partitionValidationStatus = 'error'; + this.partitionValidationMessage = error.error?.message || 'Error al validar las particiones.'; + } + ); + } + calculateUsedSpace(partitions: Partition[]): number { return partitions .filter(partition => !partition.removed) @@ -429,19 +584,36 @@ export class PartitionAssistantComponent implements OnInit{ } updateDiskChart(disk: any) { - console.log('disk', disk); disk.chartData = this.generateChartData(disk.partitions); disk.used = this.calculateUsedSpace(disk.partitions); disk.percentage = (disk.used / disk.totalDiskSize) * 100; + + setTimeout(() => { + this.resizeChart(); + }, 50); } openScheduleModal(): void { + let scope = this.runScriptContext?.type || 'clients'; + let selectedClients = null; + + if (this.selectedClients.length === 0 && this.clientData.length > 0) { + this.updateSelectedClients(); + } + + if (this.selectedClients && this.selectedClients.length > 0) { + scope = 'clients'; + selectedClients = this.selectedClients; + } + const dialogRef = this.dialog.open(CreateTaskComponent, { width: '800px', data: { - scope: this.runScriptContext.type, - organizationalUnit: this.runScriptContext['@id'], - source: 'assistant' + scope: scope, + selectedClients: selectedClients, + organizationalUnit: this.runScriptContext?.['@id'], + source: 'assistant', + runScriptContext: this.runScriptContext } }); @@ -502,11 +674,16 @@ export class PartitionAssistantComponent implements OnInit{ onDiskSelectionChange() { if (this.selectedDiskNumber) { this.scrollToPartitionTable(); + + this.validationSubject.next(); + + setTimeout(() => { + this.resizeChart(); + }, 150); } } scrollToPartitionTable() { - // Pequeño delay para asegurar que el contenido se haya renderizado setTimeout(() => { const diskInfo = document.getElementById('disk-info'); @@ -536,4 +713,40 @@ export class PartitionAssistantComponent implements OnInit{ console.error('No se encontró el botón execute-button'); } } + + getPartitionsDataSource(): MatTableDataSource { + return new MatTableDataSource(this.selectedDisk?.partitions || []); + } + + getPartitionIndex(partition: Partition): number { + return this.selectedDisk?.partitions.findIndex((p: Partition) => p.uuid === partition.uuid) || -1; + } + + getFreeSpacePercentage(): number { + if (!this.selectedDisk) return 0; + return this.selectedDisk.totalDiskSize > 0 ? + ((this.selectedDisk.totalDiskSize - this.selectedDisk.used) / this.selectedDisk.totalDiskSize) * 100 : 0; + } + + + private getNextPartitionColor(): string { + if (!this.selectedDisk) return this.partitionColors[0]; + + const usedColors = this.selectedDisk.partitions + .filter((p: Partition) => !p.removed) + .map((p: Partition) => p.color); + + for (const color of this.partitionColors) { + if (!usedColors.includes(color)) { + return color; + } + } + + return this.partitionColors[Math.floor(Math.random() * this.partitionColors.length)]; + } + + + private getColorForPartition(partitionNumber: number): string { + return this.partitionColors[(partitionNumber - 1) % this.partitionColors.length]; + } } diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.html index a17794d..7b154c9 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.html @@ -10,11 +10,11 @@
- +
-
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.ts index b1135d9..61297c8 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.ts @@ -29,7 +29,7 @@ export class RunScriptAssistantComponent implements OnInit{ parameters: any = {}; selectedScript: any = null; selectedClients: any[] = []; - allSelected: boolean = true; + allSelected: boolean = false; commandType: string = 'existing'; newScript: string = ''; selection = new SelectionModel(true, []); @@ -56,7 +56,11 @@ export class RunScriptAssistantComponent implements OnInit{ this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null; this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; }); - this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected); + this.selectedClients = this.clientData.filter( + (client: { selected: boolean; status: string }) => client.selected + ); + + this.allSelected = this.clientData.length > 0 && this.clientData.every((client: { selected: boolean }) => client.selected); this.loadScripts() } @@ -117,12 +121,15 @@ export class RunScriptAssistantComponent implements OnInit{ } updateSelectedClients() { - this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected); + this.selectedClients = this.clientData.filter( + (client: { selected: boolean; status: string }) => client.selected + ); } toggleSelectAll() { this.allSelected = !this.allSelected; this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = this.allSelected; }); + this.updateSelectedClients(); } getPartitionsTooltip(client: any): string { @@ -198,21 +205,33 @@ export class RunScriptAssistantComponent implements OnInit{ } openScheduleModal(): void { + let scope = this.runScriptContext.type; + let selectedClients = null; + + + if ((!this.runScriptContext || this.runScriptContext.type === 'client' || this.selectedClients.length === 1) && this.selectedClients && this.selectedClients.length > 0) { + scope = 'clients'; + selectedClients = this.selectedClients; + } + const dialogRef = this.dialog.open(CreateTaskComponent, { width: '800px', data: { - scope: this.runScriptContext.type, - organizationalUnit: this.runScriptContext['@id'], - source: 'assistant' + scope: scope, + selectedClients: selectedClients, + organizationalUnit: this.runScriptContext?.['@id'], + source: 'assistant', + runScriptContext: this.runScriptContext } }); dialogRef.afterClosed().subscribe(result => { + console.log(result); if (result) { this.http.post(`${this.baseUrl}/command-task-scripts`, { - commandTask: result['@id'], + commandTask: result.taskId['@id'], content: this.commandType === 'existing' ? this.scriptContent : this.newScript, - order: 1, + order: result.executionOrder, type: 'run-script', }).subscribe({ next: () => { diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 3e98714..741645d 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -422,6 +422,10 @@ article Logs en tiempo real + + +
+
+ + + + +
\ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/client-logs-modal/client-logs-modal.component.ts b/ogWebconsole/src/app/components/groups/shared/client-logs-modal/client-logs-modal.component.ts new file mode 100644 index 0000000..9e7ebd1 --- /dev/null +++ b/ogWebconsole/src/app/components/groups/shared/client-logs-modal/client-logs-modal.component.ts @@ -0,0 +1,29 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'app-client-logs-modal', + templateUrl: './client-logs-modal.component.html', + styleUrls: ['./client-logs-modal.component.css'] +}) +export class ClientLogsModalComponent { + iframeUrl: string; + grafanaUrl: string; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: { client: any } + ) { + const mac = this.data.client.mac || ''; + this.iframeUrl = `https://localhost:3030/d-solo/opengnsys-clients/filebeat-clients?orgId=1&timezone=browser&var-hostname=$__all&var-mac=${mac}&refresh=5s&panelId=1&__feature.dashboardSceneSolo`; + this.grafanaUrl = `https://localhost:3030/d/opengnsys-clients/filebeat-clients?orgId=1&from=now-5m&to=now&timezone=browser&var-hostname=$__all&var-mac=${mac}&refresh=5s&viewPanel=panel-1`; + } + + closeModal(): void { + this.dialogRef.close(); + } + + openGrafanaInNewTab(): void { + window.open(this.grafanaUrl, '_blank'); + } +} \ No newline at end of file diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/create-branch-modal/create-branch-modal.component.css b/ogWebconsole/src/app/components/repositories/show-git-images/create-branch-modal/create-branch-modal.component.css new file mode 100644 index 0000000..df6972f --- /dev/null +++ b/ogWebconsole/src/app/components/repositories/show-git-images/create-branch-modal/create-branch-modal.component.css @@ -0,0 +1,67 @@ +.dialog-content { + min-width: 400px; + max-width: 600px; +} + +.commit-info { + background-color: #f5f5f5; + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; +} + +.commit-info p { + margin: 5px 0; + font-size: 14px; +} + +.branch-form { + display: flex; + flex-direction: column; + gap: 15px; +} + +.form-field { + width: 100%; +} + +.action-container { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; + padding: 0 24px 24px 24px; +} + +.ordinary-button { + background-color: #f5f5f5; + color: #333; + border: 1px solid #ddd; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.ordinary-button:hover { + background-color: #e0e0e0; +} + +.submit-button { + background-color: #3f51b5; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.submit-button:hover:not(:disabled) { + background-color: #303f9f; +} + +.submit-button:disabled { + background-color: #ccc; + cursor: not-allowed; +} \ No newline at end of file diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/create-branch-modal/create-branch-modal.component.html b/ogWebconsole/src/app/components/repositories/show-git-images/create-branch-modal/create-branch-modal.component.html new file mode 100644 index 0000000..cb0f7cf --- /dev/null +++ b/ogWebconsole/src/app/components/repositories/show-git-images/create-branch-modal/create-branch-modal.component.html @@ -0,0 +1,31 @@ + + +

Crear Rama desde Commit

+ + +
+

Commit ID: {{ data.commit?.hexsha }}

+

Mensaje: {{ data.commit?.message }}

+
+ + + + Nombre de la rama + + + El nombre de la rama es obligatorio + + + El nombre de la rama solo puede contener letras, números, puntos, guiones y guiones bajos + + Ejemplo: feature-nueva-funcionalidad, hotfix-bug-123, release-v2.0 + + +
+ +
+ + +
\ No newline at end of file diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/create-branch-modal/create-branch-modal.component.ts b/ogWebconsole/src/app/components/repositories/show-git-images/create-branch-modal/create-branch-modal.component.ts new file mode 100644 index 0000000..ce8f72f --- /dev/null +++ b/ogWebconsole/src/app/components/repositories/show-git-images/create-branch-modal/create-branch-modal.component.ts @@ -0,0 +1,61 @@ +import { Component, Inject } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { HttpClient } from '@angular/common/http'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { ToastrService } from 'ngx-toastr'; +import { ConfigService } from '@services/config.service'; + +@Component({ + selector: 'app-create-branch-modal', + templateUrl: './create-branch-modal.component.html', + styleUrl: './create-branch-modal.component.css' +}) +export class CreateBranchModalComponent { + branchForm: FormGroup; + loading: boolean = false; + baseUrl: string; + + constructor( + private fb: FormBuilder, + private http: HttpClient, + public dialogRef: MatDialogRef, + private toastService: ToastrService, + private configService: ConfigService, + @Inject(MAT_DIALOG_DATA) public data: { commit: any, repositoryName: string, repositoryUuid: string } + ) { + this.baseUrl = this.configService.apiUrl; + this.branchForm = this.fb.group({ + name: ['', [Validators.required, Validators.pattern(/^[a-zA-Z0-9._-]+$/)]], + }); + } + + createBranch(): void { + if (this.branchForm.valid) { + this.loading = true; + const payload = { + commit: this.data.commit?.hexsha || 'master', + name: this.branchForm.value.name, + repository: this.data.repositoryName + }; + + const url = `${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/create-branch`; + + this.http.post(url, payload).subscribe({ + next: (response) => { + this.toastService.success('Rama creada correctamente'); + this.dialogRef.close(response); + }, + error: (error) => { + this.toastService.error(error.error?.message || 'Error al crear la rama'); + this.loading = false; + } + }); + } else { + this.toastService.error('Por favor, complete todos los campos requeridos'); + } + } + + close(): void { + this.dialogRef.close(); + } +} \ No newline at end of file diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.css b/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.css new file mode 100644 index 0000000..db1d027 --- /dev/null +++ b/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.css @@ -0,0 +1,78 @@ +.dialog-content { + min-width: 400px; + max-width: 500px; + padding: 0 24px 24px 24px; +} + +h2[mat-dialog-title] { + padding: 24px 24px 0 24px; + margin: 0; +} + +.commit-info { + background-color: #f5f5f5; + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; + border-left: 4px solid #3f51b5; +} + +.commit-info p { + margin: 5px 0; + font-size: 14px; +} + +.commit-info strong { + color: #3f51b5; +} + +.tag-form { + display: flex; + flex-direction: column; + gap: 15px; +} + +.form-field { + width: 100%; +} + +.action-container { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; + padding: 0 24px 24px 24px; +} + +.ordinary-button { + background-color: #f5f5f5; + color: #333; + border: 1px solid #ddd; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.ordinary-button:hover { + background-color: #e0e0e0; +} + +.submit-button { + background-color: #3f51b5; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.submit-button:hover:not(:disabled) { + background-color: #303f9f; +} + +.submit-button:disabled { + background-color: #ccc; + cursor: not-allowed; +} \ No newline at end of file diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.html b/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.html new file mode 100644 index 0000000..df954ad --- /dev/null +++ b/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.html @@ -0,0 +1,40 @@ + + +

Crear Tag para Commit

+ + +
+

Commit ID: {{ data.commit.hexsha }}

+

Mensaje: {{ data.commit.message }}

+
+ +
+ + Nombre del tag + + + El nombre del tag es obligatorio + + + El nombre del tag solo puede contener letras, números, puntos, guiones y guiones bajos + + Ejemplo: v1.0.0, release-2024-01, hotfix-bug-123 + + + + Mensaje del tag + + + El mensaje del tag es obligatorio + + Descripción del tag (ej: "Release estable de la versión 1.0") + +
+
+ +
+ + +
\ No newline at end of file diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.spec.ts b/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.spec.ts new file mode 100644 index 0000000..488da1e --- /dev/null +++ b/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.spec.ts @@ -0,0 +1,79 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CreateTagModalComponent } from './create-tag-modal.component'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { HttpClient } from '@angular/common/http'; +import { ToastrService } from 'ngx-toastr'; +import { ToastrModule } from 'ngx-toastr'; +import { ConfigService } from '@services/config.service'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateModule } from '@ngx-translate/core'; +import { LoadingComponent } from '../../../../shared/loading/loading.component'; + +describe('CreateTagModalComponent', () => { + let component: CreateTagModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + const mockToastrService = { + success: jasmine.createSpy('success'), + error: jasmine.createSpy('error'), + warning: jasmine.createSpy('warning'), + info: jasmine.createSpy('info') + }; + + const mockConfigService = { + apiUrl: 'http://mock-api-url' + }; + + await TestBed.configureTestingModule({ + declarations: [CreateTagModalComponent, LoadingComponent], + imports: [ + MatDialogModule, + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + NoopAnimationsModule, + TranslateModule.forRoot(), + ToastrModule.forRoot() + ], + providers: [ + FormBuilder, + HttpClient, + provideHttpClient(), + provideHttpClientTesting(), + { provide: ToastrService, useValue: mockToastrService }, + { provide: ConfigService, useValue: mockConfigService }, + { + provide: MatDialogRef, + useValue: {} + }, + { + provide: MAT_DIALOG_DATA, + useValue: { + commit: { + hexsha: 'test-commit-id', + message: 'Test commit message' + }, + repositoryName: 'test-repo' + } + } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CreateTagModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.ts b/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.ts new file mode 100644 index 0000000..3ed3f55 --- /dev/null +++ b/ogWebconsole/src/app/components/repositories/show-git-images/create-tag-modal/create-tag-modal.component.ts @@ -0,0 +1,63 @@ +import { Component, Inject } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { HttpClient } from '@angular/common/http'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { ToastrService } from 'ngx-toastr'; +import { ConfigService } from '@services/config.service'; + +@Component({ + selector: 'app-create-tag-modal', + templateUrl: './create-tag-modal.component.html', + styleUrl: './create-tag-modal.component.css' +}) +export class CreateTagModalComponent { + tagForm: FormGroup; + loading: boolean = false; + baseUrl: string; + + constructor( + private fb: FormBuilder, + private http: HttpClient, + public dialogRef: MatDialogRef, + private toastService: ToastrService, + private configService: ConfigService, + @Inject(MAT_DIALOG_DATA) public data: { commit: any, repositoryName: string, repositoryUuid: string } + ) { + this.baseUrl = this.configService.apiUrl; + this.tagForm = this.fb.group({ + name: ['', [Validators.required, Validators.pattern(/^[a-zA-Z0-9._-]+$/)]], + message: ['', Validators.required] + }); + } + + createTag(): void { + if (this.tagForm.valid) { + this.loading = true; + const payload = { + commit: this.data.commit.hexsha, + name: this.tagForm.value.name, + message: this.tagForm.value.message, + repository: this.data.repositoryName + }; + + const url = `${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/create-tag`; + + this.http.post(url, payload).subscribe({ + next: (response) => { + this.toastService.success('Tag creado correctamente'); + this.dialogRef.close(response); + }, + error: (error) => { + this.toastService.error(error.error?.message || 'Error al crear el tag'); + this.loading = false; + } + }); + } else { + this.toastService.error('Por favor, complete todos los campos requeridos'); + } + } + + close(): void { + this.dialogRef.close(); + } +} \ No newline at end of file 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 2b7612a..2f53ce2 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 @@ -95,6 +95,14 @@ + + + +
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 099e379..407c318 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 @@ -1,4 +1,4 @@ -import {Component, Inject, Input, isDevMode, OnInit} from '@angular/core'; +import {Component, Inject, isDevMode, OnInit} from '@angular/core'; import {MatTableDataSource} from "@angular/material/table"; import {DatePipe} from "@angular/common"; import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog"; @@ -7,13 +7,9 @@ import {ToastrService} from "ngx-toastr"; import {JoyrideService} from "ngx-joyride"; import {ConfigService} from "@services/config.service"; import {Router} from "@angular/router"; -import {Observable} from "rxjs"; import {ServerInfoDialogComponent} from "../../ogdhcp/server-info-dialog/server-info-dialog.component"; -import {ImportImageComponent} from "../import-image/import-image.component"; -import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component"; -import {ExportImageComponent} from "../../images/export-image/export-image.component"; -import {BackupImageComponent} from "../backup-image/backup-image.component"; -import {EditImageComponent} from "../edit-image/edit-image.component"; +import {CreateTagModalComponent} from "./create-tag-modal/create-tag-modal.component"; +import {CreateBranchModalComponent} from "./create-branch-modal/create-branch-modal.component"; @Component({ selector: 'app-show-git-commits', @@ -212,6 +208,12 @@ export class ShowGitCommitsComponent implements OnInit{ this.toastService.success('Commit ID copiado al portapapeles'); }); break; + case 'create-tag': + this.openCreateTagDialog(commit); + break; + case 'create-branch': + this.openCreateBranchDialog(commit); + break; default: console.error('Acción no soportada:', action); break; @@ -253,6 +255,43 @@ export class ShowGitCommitsComponent implements OnInit{ }); } + openCreateTagDialog(commit: any) { + const dialogRef = this.dialog.open(CreateTagModalComponent, { + width: '500px', + data: { + commit: commit, + repositoryName: this.selectedRepository, + repositoryUuid: this.data.repositoryUuid + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + // Recargar los datos para mostrar el nuevo tag + this.loadData(); + } + }); + } + + openCreateBranchDialog(commit: any) { + const dialogRef = this.dialog.open(CreateBranchModalComponent, { + width: '500px', + data: { + commit: commit, + repositoryName: this.selectedRepository, + repositoryUuid: this.data.repositoryUuid + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + // Recargar los datos para mostrar la nueva rama + this.loadBranches(); + this.loadData(); + } + }); + } + goToPage(commit: any) { window.open(`http://localhost:3100/oggit/${this.selectedRepository}/commit/${commit.hexsha}`, '_blank'); } diff --git a/ogWebconsole/src/app/layout/header/header.component.ts b/ogWebconsole/src/app/layout/header/header.component.ts index f3e6c81..6f8ed9d 100644 --- a/ogWebconsole/src/app/layout/header/header.component.ts +++ b/ogWebconsole/src/app/layout/header/header.component.ts @@ -42,8 +42,8 @@ export class HeaderComponent implements OnInit { showGlobalStatus() { this.dialog.open(GlobalStatusComponent, { - width: '65vw', - height: '80vh', + width: '80vw', + height: '85vh', }) } diff --git a/ogWebconsole/src/app/shared/queue-confirmation-modal/queue-confirmation-modal.component.ts b/ogWebconsole/src/app/shared/queue-confirmation-modal/queue-confirmation-modal.component.ts index 2e6571b..722873c 100644 --- a/ogWebconsole/src/app/shared/queue-confirmation-modal/queue-confirmation-modal.component.ts +++ b/ogWebconsole/src/app/shared/queue-confirmation-modal/queue-confirmation-modal.component.ts @@ -15,7 +15,7 @@ export class QueueConfirmationModalComponent { ) {} onNoClick(): void { - this.dialogRef.close(false); + this.dialogRef.close(); } onYesClick(): void {