+
+ code
+ Configuración de script
-
0 && selectedScript.parameters">
-
Ingrese los parámetros:
-
-
- {{ paramName }}
-
-
+
+
+
+ Nuevo Script
+
+
+ Script Guardado
+
+
+
+
+
+
+
+ Ingrese el script
+
+
+ Guardar Script
+
+
+
+
+ Seleccione script a ejecutar
+
+ {{ script.name }}
+
+
+
+
+
+
+
+
0 && selectedScript.parameters">
+
Ingrese los parámetros:
+
+
+ {{ paramName }}
+
+
+
-
+
+
+
+
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.spec.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.spec.ts
index d4531e6..348494d 100644
--- a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.spec.ts
+++ b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.spec.ts
@@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RunScriptAssistantComponent } from './run-script-assistant.component';
import { DeployImageComponent } from "../deploy-image/deploy-image.component";
import { LoadingComponent } from "../../../../../shared/loading/loading.component";
+import { ScrollToTopComponent } from "../../../../../shared/scroll-to-top/scroll-to-top.component";
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from "@angular/material/dialog";
import { MatFormFieldModule } from "@angular/material/form-field";
@@ -28,6 +29,8 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import {MatIconModule} from "@angular/material/icon";
import {MatCardModule} from "@angular/material/card";
import {MatButtonToggleModule} from "@angular/material/button-toggle";
+import { MatChipsModule } from "@angular/material/chips";
+import { MatTooltipModule } from "@angular/material/tooltip";
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http);
@@ -44,7 +47,7 @@ describe('RunScriptAssistantComponent', () => {
};
await TestBed.configureTestingModule({
- declarations: [RunScriptAssistantComponent, DeployImageComponent, LoadingComponent],
+ declarations: [RunScriptAssistantComponent, DeployImageComponent, LoadingComponent, ScrollToTopComponent],
imports: [
ReactiveFormsModule,
FormsModule,
@@ -63,6 +66,8 @@ describe('RunScriptAssistantComponent', () => {
MatIconModule,
MatCardModule,
MatButtonToggleModule,
+ MatChipsModule,
+ MatTooltipModule,
ToastrModule.forRoot(),
HttpClientTestingModule,
TranslateModule.forRoot({
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 2909ac5..b1135d9 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
@@ -7,6 +7,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { SaveScriptComponent } from "./save-script/save-script.component";
import { MatDialog } from "@angular/material/dialog";
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
+import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
@Component({
selector: 'app-run-script-assistant',
@@ -53,15 +54,9 @@ export class RunScriptAssistantComponent 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.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; });
- this.selectedClients = this.clientData.filter(
- (client: { status: string }) => client.status === 'og-live'
- );
+ this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
this.loadScripts()
}
@@ -122,18 +117,12 @@ export class RunScriptAssistantComponent implements OnInit{
}
updateSelectedClients() {
- this.selectedClients = this.clientData.filter(
- (client: { selected: boolean; status: string }) => client.selected && client.status === "og-live"
- );
+ 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 }) => {
- if (client.status === "og-live") {
- client.selected = this.allSelected;
- }
- });
+ this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = this.allSelected; });
}
getPartitionsTooltip(client: any): string {
@@ -179,23 +168,33 @@ export class RunScriptAssistantComponent implements OnInit{
}
save(): void {
- this.loading = true;
+ const dialogRef = this.dialog.open(QueueConfirmationModalComponent, {
+ width: '400px',
+ disableClose: true,
+ hasBackdrop: true
+ });
- this.http.post(`${this.baseUrl}/commands/run-script`, {
- clients: this.selectedClients.map((client: any) => client.uuid),
- script: this.commandType === 'existing' ? this.scriptContent : this.newScript,
- }).subscribe(
- response => {
- this.toastService.success('Script ejecutado correctamente');
- this.dataChange.emit();
- this.router.navigate(['/commands-logs']);
- },
- error => {
- this.toastService.error('Error al ejecutar el script');
+ dialogRef.afterClosed().subscribe(result => {
+ if (result !== undefined) {
+ this.loading = true;
+ this.http.post(`${this.baseUrl}/commands/run-script`, {
+ clients: this.selectedClients.map((client: any) => client.uuid),
+ script: this.commandType === 'existing' ? this.scriptContent : this.newScript,
+ queue: result
+ }).subscribe(
+ response => {
+ this.toastService.success('Script ejecutado correctamente');
+ this.dataChange.emit();
+ this.router.navigate(['/commands-logs']);
+ this.loading = false;
+ },
+ error => {
+ this.toastService.error('Error al ejecutar el script');
+ this.loading = false;
+ }
+ );
}
- );
-
- this.loading = false;
+ });
}
openScheduleModal(): void {
diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css
index f4a5383..722ad3e 100644
--- a/ogWebconsole/src/app/components/groups/groups.component.css
+++ b/ogWebconsole/src/app/components/groups/groups.component.css
@@ -20,6 +20,8 @@
width: 100%;
height: 100%;
overflow: hidden;
+ min-height: 0;
+ will-change: auto;
}
.clients-mat-divider {
@@ -46,7 +48,7 @@
padding: 1rem !important;
box-sizing: border-box !important;
min-height: 250px !important;
- overflow-y: auto !important;
+ overflow: hidden !important;
}
.filters-panel,
@@ -56,6 +58,7 @@
padding: 0.5rem !important;
box-sizing: border-box !important;
margin-bottom: 0 !important;
+ max-height: 300px !important;
}
.filters-container {
@@ -85,18 +88,20 @@
}
.cards-view {
- max-height: none !important;
+ max-height: calc(100vh - 400px) !important;
}
.clients-table {
- max-height: unset !important;
- overflow: unset !important;
- display: table !important;
- flex-direction: unset !important;
+ max-height: none !important;
+ overflow: auto !important;
+ display: flex !important;
+ flex-direction: column !important;
+ flex: 1 !important;
}
.clients-container {
padding: 0em 1em 0em 1em !important;
+ overflow: auto !important;
}
}
@@ -162,7 +167,9 @@
.clients-view {
flex-grow: 1;
- overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ scroll-behavior: smooth;
}
.cards-view {
@@ -184,16 +191,23 @@
.list-view {
overflow-x: auto;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ scroll-behavior: smooth;
}
.clients-table {
- max-height: calc(100vh - 330px);
+ flex: 1;
overflow: auto;
display: flex;
flex-direction: column;
width: 100%;
table-layout: auto;
border-collapse: collapse;
+ will-change: scroll-position;
+ scroll-behavior: smooth;
+ -webkit-overflow-scrolling: touch;
}
.clients-table th,
@@ -223,11 +237,13 @@
width: 100%;
margin-left: 1rem;
background-color: #fafafa;
+ margin-bottom: 1rem;
}
.list-paginator {
width: 100%;
background-color: #f3f3f3;
+ margin-bottom: 1rem;
}
.actions mat-icon {
@@ -305,7 +321,6 @@ mat-tree mat-tree-node:hover mat-icon {
}
mat-tree mat-tree-node mat-icon.node-icon {
- color: #757575;
margin-right: 10px;
}
@@ -382,19 +397,26 @@ mat-tree mat-tree-node.disabled:hover {
.clients-container {
flex: 8;
box-sizing: border-box;
- overflow: hidden;
+ overflow: auto;
display: flex;
flex-direction: column;
padding: 0rem 1rem 0rem 0.5rem;
+ background: #fafbfc;
+ border-radius: 12px;
+ margin-left: 0.5rem;
+ min-height: 0;
}
.filters-and-tree-container {
- flex: 2;
display: flex;
flex-direction: column;
- flex-shrink: 1;
- width: 100%;
+ gap: 0.5rem;
+ flex: 2;
+ min-width: 400px;
+ max-width: 100%;
box-sizing: border-box;
+ width: 100%;
+ padding: 0 0.5rem;
}
.filters-container {
@@ -418,8 +440,321 @@ mat-tree mat-tree-node.disabled:hover {
.tree-container {
flex-grow: 1;
overflow-y: auto;
- max-height: calc(100% - var(--filters-panel-height));
margin-bottom: 1rem;
+ background: white;
+ border-radius: 12px;
+ border: 1px solid #e9ecef;
+ width: 100%;
+ min-width: 0;
+ max-height: calc(100vh - 400px);
+ min-height: 200px;
+ will-change: scroll-position;
+ scroll-behavior: smooth;
+ -webkit-overflow-scrolling: touch;
+}
+
+.tree-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 24px;
+ border-bottom: 2px solid #e9ecef;
+ background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
+ border-radius: 12px 12px 0 0;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.tree-title {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin: 0;
+ font-size: 1.2rem;
+ font-weight: 700;
+ color: #2c3e50;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.tree-title mat-icon {
+ color: #3f51b5;
+ font-size: 28px;
+ width: 28px;
+ height: 28px;
+ background: rgba(63, 81, 181, 0.1);
+ border-radius: 50%;
+ padding: 4px;
+}
+
+.tree-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.tree-actions button {
+ color: #6c757d;
+ transition: all 0.3s ease;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+}
+
+.tree-actions button:hover {
+ color: #3f51b5;
+ background-color: rgba(63, 81, 181, 0.1);
+ transform: scale(1.1);
+ box-shadow: 0 2px 8px rgba(63, 81, 181, 0.2);
+}
+
+.modern-tree {
+ background: transparent;
+ padding: 12px 0;
+ border-radius: 0 0 12px 12px;
+}
+
+.tree-node {
+ margin: 2px 8px;
+ border-radius: 8px;
+ transition: all 0.3s ease;
+ position: relative;
+ border-left: 3px solid transparent;
+}
+
+.tree-node:hover {
+ background-color: rgba(102, 126, 234, 0.08);
+ transform: translateX(4px);
+ border-left-color: #667eea;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.node-content {
+ display: flex;
+ align-items: center;
+ padding: 10px 16px;
+ gap: 12px;
+ width: 100%;
+ min-height: 52px;
+}
+
+.expand-button {
+ min-width: 36px;
+ color: #6c757d;
+ transition: all 0.3s ease;
+ border-radius: 50%;
+}
+
+.expand-button:hover:not(.disabled-toggle) {
+ color: #667eea;
+ background-color: rgba(102, 126, 234, 0.1);
+ transform: scale(1.1);
+}
+
+.expand-icon {
+ transition: transform 0.3s ease;
+ font-size: 20px;
+}
+
+.tree-node:hover .expand-icon {
+ transform: scale(1.2);
+}
+
+.node-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+.node-main {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.node-icon {
+ font-size: 22px;
+ width: 22px;
+ height: 22px;
+ transition: all 0.3s ease;
+ border-radius: 50%;
+ padding: 4px;
+ background: rgba(255, 255, 255, 0.8);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.tree-node:hover .node-icon {
+ transform: scale(1.1);
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+}
+
+.node-name {
+ font-weight: 500;
+ color: #2c3e50;
+ font-size: 0.95rem;
+ transition: color 0.2s ease;
+}
+
+.tree-node:hover .node-name {
+ color: #1a237e;
+ font-weight: 600;
+}
+
+.node-details {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ font-size: 0.8rem;
+ color: #6c757d;
+}
+
+.node-ip {
+ font-family: 'Courier New', monospace;
+ color: #495057;
+ font-weight: 500;
+}
+
+.node-count {
+ background: rgba(102, 126, 234, 0.1);
+ color: #667eea;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ display: inline-block;
+ margin-top: 2px;
+}
+
+.node-status {
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.node-status.status-off {
+ background: linear-gradient(135deg, #dc3545, #c82333);
+ color: white;
+ box-shadow: 0 1px 3px rgba(220, 53, 69, 0.3);
+}
+
+.node-status.status-og-live {
+ background: linear-gradient(135deg, #28a745, #20c997);
+ color: white;
+ box-shadow: 0 1px 3px rgba(40, 167, 69, 0.3);
+}
+
+.node-status.status-linux {
+ background: linear-gradient(135deg, #17a2b8, #138496);
+ color: white;
+ box-shadow: 0 1px 3px rgba(23, 162, 184, 0.3);
+}
+
+.node-status.status-windows {
+ background: linear-gradient(135deg, #007bff, #0056b3);
+ color: white;
+ box-shadow: 0 1px 3px rgba(0, 123, 255, 0.3);
+}
+
+.node-status.status-busy {
+ background: linear-gradient(135deg, #ffc107, #e0a800);
+ color: #212529;
+ box-shadow: 0 1px 3px rgba(255, 193, 7, 0.3);
+}
+
+.node-status.status-disconnected {
+ background: linear-gradient(135deg, #6c757d, #545b62);
+ color: white;
+ box-shadow: 0 1px 3px rgba(108, 117, 125, 0.3);
+}
+
+.node-status.status-initializing {
+ background: linear-gradient(135deg, #fd7e14, #e55a00);
+ color: white;
+ box-shadow: 0 1px 3px rgba(253, 126, 20, 0.3);
+}
+
+.node-actions {
+ opacity: 0;
+ transition: opacity 0.2s ease;
+}
+
+.tree-node:hover .node-actions {
+ opacity: 1;
+}
+
+.menu-button {
+ min-width: 32px;
+ color: #6c757d;
+ transition: all 0.2s ease;
+}
+
+.menu-button:hover {
+ color: #667eea;
+ background-color: rgba(102, 126, 234, 0.1);
+}
+
+.selected-node {
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.15), rgba(102, 126, 234, 0.08));
+ border-left-color: #667eea;
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
+}
+
+.selected-node .node-name {
+ color: #1a237e;
+ font-weight: 600;
+}
+
+.selected-node .node-icon {
+ background: rgba(102, 126, 234, 0.1);
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+}
+
+/* Animaciones */
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.tree-node {
+ animation: slideIn 0.3s ease-out;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .tree-header {
+ padding: 12px 16px;
+ }
+
+ .tree-title {
+ font-size: 1rem;
+ }
+
+ .node-content {
+ padding: 8px 12px;
+ min-height: 48px;
+ }
+
+ .node-icon {
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+ }
+
+ .node-name {
+ font-size: 0.9rem;
+ }
+
+ .node-details {
+ font-size: 0.75rem;
+ }
}
.client-item {
@@ -428,24 +763,26 @@ mat-tree mat-tree-node.disabled:hover {
}
.client-card {
- background-color: #fff;
- border: 1px solid #ccc;
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
+ border: 1.5px solid #e1e5e9cc;
border-radius: 8px;
overflow: hidden;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
width: 100%;
- max-width: 300px;
+ max-width: 260px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
- transition: transform 0.3s ease, box-shadow 0.3s ease;
+ transition: all 0.2s ease;
position: relative;
+ padding: 6px 4px;
}
.client-card:hover {
- transform: translateY(-5px);
- box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+ background: linear-gradient(135deg, #ffffff 0%, #f0f4ff 100%);
}
.client-details {
@@ -454,6 +791,8 @@ mat-tree mat-tree-node.disabled:hover {
flex-direction: column;
justify-content: space-between;
margin-top: 4px;
+ width: 100%;
+ gap: 4px;
}
.type-view-text {
@@ -465,19 +804,76 @@ mat-tree mat-tree-node.disabled:hover {
justify-content: center;
align-items: center;
gap: 1px;
- margin-top: 10px;
+ margin-top: 4px;
+ padding: 3px;
+ border-radius: 4px;
+ width: 100%;
}
-.client-status-container {
- display: flex;
- align-items: center;
- gap: 5px;
+.action-icons button {
+ transition: all 0.15s ease;
+}
+
+.action-icons button:hover {
+ transform: scale(1.05);
+ background: rgba(63, 81, 181, 0.08);
+}
+
+.client-image {
+ width: 32px;
+ height: 32px;
+ margin: 4px 0;
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
+ transition: transform 0.2s ease;
+}
+
+/* Iconos más grandes solo en vista de tarjetas */
+.cards-view .client-image {
+ width: 42px;
+ height: 42px;
+ margin: 5px 0;
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15));
+}
+
+.client-card:hover .client-image {
+ transform: scale(1.05);
+}
+
+.cards-view .client-card:hover .client-image {
+ transform: scale(1.1);
+}
+
+.client-name {
+ display: block;
+ font-weight: 600;
+ font-size: 0.85rem;
+ margin-bottom: 2px;
+ padding: 0 4px;
+ color: #2c3e50;
+ line-height: 1.2;
+ word-break: break-word;
}
.client-ip {
display: block;
- font-size: 0.9em;
- color: #666;
+ font-size: 0.7rem;
+ color: #6c757d;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ padding: 1px 4px;
+ border-radius: 3px;
+ margin: 1px 0;
+ word-break: break-all;
+}
+
+.client-mac {
+ display: block;
+ font-size: 0.6rem;
+ color: #6c757d;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ padding: 1px 3px;
+ border-radius: 3px;
+ margin: 1px 0;
+ word-break: break-all;
}
.clients-list {
@@ -487,8 +883,13 @@ mat-tree mat-tree-node.disabled:hover {
}
.sync-spinner {
- margin-left: 1em;
- margin-right: 1em;
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ z-index: 2;
+ background: rgba(255, 255, 255, 0.9);
+ border-radius: 50%;
+ padding: 1px;
}
.mat-elevation-z8 {
@@ -561,9 +962,783 @@ mat-button-toggle-group {
.clients-grid {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
- gap: 16px;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: 10px;
padding: 0.5rem 1rem 1rem 1rem;
width: 100%;
box-sizing: border-box;
}
+
+/* Estilo para hacer el backdrop no clickeable */
+::ng-deep .non-clickable-backdrop {
+ pointer-events: none !important;
+}
+
+/* Estados de los PCs - Colores exactos de los SVG */
+.status-off .client-card {
+ border-color: #b0b0b0cc;
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+}
+
+.status-off .client-image {
+ filter: drop-shadow(0 2px 4px rgba(102, 102, 102, 0.3));
+}
+
+.status-off:hover .client-card {
+ border-color: #a0a0a0cc;
+ background: linear-gradient(135deg, #f8f9fa 0%, #dee2e6 100%);
+}
+
+.status-busy .client-card {
+ border-color: #ea332388;
+ background: linear-gradient(135deg, #fff5f5 0%, #ffe6e6 100%);
+}
+
+.status-busy .client-image {
+ filter: drop-shadow(0 2px 4px rgba(234, 51, 35, 0.3));
+}
+
+.status-busy:hover .client-card {
+ border-color: #d6338488;
+ background: linear-gradient(135deg, #fff5f5 0%, #ffebeb 100%);
+}
+
+.status-disconnected .client-card {
+ border-color: #ff000088;
+ background: linear-gradient(135deg, #fff5f5 0%, #ffe6e6 100%);
+}
+
+.status-disconnected .client-image {
+ filter: drop-shadow(0 2px 4px rgba(255, 0, 0, 0.3));
+}
+
+.status-disconnected:hover .client-card {
+ border-color: #dc354588;
+ background: linear-gradient(135deg, #fff5f5 0%, #ffebeb 100%);
+}
+
+.status-initializing .client-card {
+ border-color: #75fbfd88;
+ background: linear-gradient(135deg, #f0f9ff 0%, #e6f3ff 100%);
+}
+
+.status-initializing .client-image {
+ filter: drop-shadow(0 2px 4px rgba(117, 251, 253, 0.3));
+}
+
+.status-initializing:hover .client-card {
+ border-color: #17a2b888;
+ background: linear-gradient(135deg, #f0f9ff 0%, #d1ecf1 100%);
+}
+
+.status-og-live .client-card {
+ border-color: #ffff5588;
+ background: linear-gradient(135deg, #fffbf0 0%, #fff3cd 100%);
+}
+
+.status-og-live .client-image {
+ filter: drop-shadow(0 2px 4px rgba(255, 255, 85, 0.3));
+}
+
+.status-og-live:hover .client-card {
+ border-color: #ffc10788;
+ background: linear-gradient(135deg, #fffbf0 0%, #ffeaa7 100%);
+}
+
+.status-linux .client-card {
+ border-color: #ea33f788;
+ background: linear-gradient(135deg, #f8f0ff 0%, #f0e6ff 100%);
+}
+
+.status-linux .client-image {
+ filter: drop-shadow(0 2px 4px rgba(234, 51, 247, 0.3));
+}
+
+.status-linux:hover .client-card {
+ border-color: #6f42c188;
+ background: linear-gradient(135deg, #f8f0ff 0%, #e6d9ff 100%);
+}
+
+.status-linux-session .client-card {
+ border-color: #ea33f788;
+ background: linear-gradient(135deg, #f8f0ff 0%, #f0e6ff 100%);
+}
+
+.status-linux-session .client-image {
+ filter: drop-shadow(0 2px 4px rgba(234, 51, 247, 0.3));
+}
+
+.status-linux-session:hover .client-card {
+ border-color: #6f42c188;
+ background: linear-gradient(135deg, #f8f0ff 0%, #e6d9ff 100%);
+}
+
+.status-windows .client-card {
+ border-color: #0000f588;
+ background: linear-gradient(135deg, #f0f8ff 0%, #e6f3ff 100%);
+}
+
+.status-windows .client-image {
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 245, 0.3));
+}
+
+.status-windows:hover .client-card {
+ border-color: #007bff88;
+ background: linear-gradient(135deg, #f0f8ff 0%, #cce7ff 100%);
+}
+
+.status-windows-session .client-card {
+ border-color: #0000f588;
+ background: linear-gradient(135deg, #f0f8ff 0%, #e6f3ff 100%);
+}
+
+.status-windows-session .client-image {
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 245, 0.3));
+}
+
+.status-windows-session:hover .client-card {
+ border-color: #007bff88;
+ background: linear-gradient(135deg, #f0f8ff 0%, #cce7ff 100%);
+}
+
+.status-macos .client-card {
+ border-color: #f19e3988;
+ background: linear-gradient(135deg, #fff8f0 0%, #ffe6cc 100%);
+}
+
+.status-macos .client-image {
+ filter: drop-shadow(0 2px 4px rgba(241, 158, 57, 0.3));
+}
+
+.status-macos:hover .client-card {
+ border-color: #fd7e1488;
+ background: linear-gradient(135deg, #fff8f0 0%, #ffd280 100%);
+}
+
+/* Estilos para el checkbox */
+.client-card mat-checkbox {
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ z-index: 2;
+}
+
+/* Responsive adjustments - manteniendo tamaño compacto */
+@media (max-width: 768px) {
+ .clients-grid {
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: 8px;
+ padding: 0.5rem 0.75rem 0.75rem 0.75rem;
+ }
+
+ .client-card {
+ padding: 5px 3px;
+ }
+
+ .cards-view .client-image {
+ width: 36px;
+ height: 36px;
+ }
+
+ .client-name {
+ font-size: 0.75rem;
+ }
+
+ .client-ip {
+ font-size: 0.6rem;
+ }
+
+ .client-mac {
+ font-size: 0.5rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .clients-grid {
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+ gap: 6px;
+ padding: 0.5rem;
+ }
+
+ .client-card {
+ padding: 4px 2px;
+ }
+
+ .cards-view .client-image {
+ width: 32px;
+ height: 32px;
+ }
+}
+
+/* Gradiente solo en la celda de status (primera columna) */
+.status-off.mat-row .mat-column-status {
+ background: linear-gradient(to right, #fff 20%, #f2f4f7 100%) !important;
+}
+.status-busy.mat-row .mat-column-status {
+ background: linear-gradient(to right, #fff 20%, #ffeaea 100%) !important;
+}
+.status-disconnected.mat-row .mat-column-status {
+ background: linear-gradient(to right, #fff 20%, #ffdede 100%) !important;
+}
+.status-initializing.mat-row .mat-column-status {
+ background: linear-gradient(to right, #fff 20%, #e3fafd 100%) !important;
+}
+.status-og-live.mat-row .mat-column-status {
+ background: linear-gradient(to right, #fff 20%, #fffbcf 100%) !important;
+}
+.status-linux.mat-row .mat-column-status {
+ background: linear-gradient(to right, #fff 20%, #f7eafd 100%) !important;
+}
+.status-linux-session.mat-row .mat-column-status {
+ background: linear-gradient(to right, #fff 20%, #f1e3fd 100%) !important;
+}
+.status-windows.mat-row .mat-column-status {
+ background: linear-gradient(to right, #fff 20%, #e3eafd 100%) !important;
+}
+.status-windows-session.mat-row .mat-column-status {
+ background: linear-gradient(to right, #fff 20%, #e0e7ff 100%) !important;
+}
+.status-macos.mat-row .mat-column-status {
+ background: linear-gradient(to right, #fff 20%, #fff2de 100%) !important;
+}
+
+/** Chip de firmware más bonito **/
+.firmware-chip {
+ display: inline-flex;
+ align-items: center;
+ background: #e0e0e0;
+ color: #333;
+ border-radius: 6px;
+ border: 1.5px solid #bdbdbd;
+ font-weight: 600;
+ font-size: 0.95em;
+ padding: 0 12px;
+ height: 28px;
+ box-shadow: none;
+ letter-spacing: 0.5px;
+}
+.firmware-chip mat-icon {
+ margin-left: 4px;
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+}
+
+/* Estadísticas */
+.stats-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ margin: 1rem 0;
+ padding: 0 1rem;
+}
+
+.stat-card {
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
+ border: 1px solid #e9ecef;
+ border-radius: 12px;
+ padding: 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ position: relative;
+ overflow: hidden;
+}
+
+.stat-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ transform: scaleX(0);
+ transition: transform 0.3s ease;
+}
+
+.stat-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
+}
+
+.stat-card:hover::before {
+ transform: scaleX(1);
+}
+
+.stat-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 48px;
+ height: 48px;
+ border-radius: 12px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ font-size: 24px;
+ transition: all 0.3s ease;
+}
+
+.stat-card:hover .stat-icon {
+ transform: scale(1.1);
+}
+
+.stat-icon.online {
+ background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
+}
+
+.stat-icon.offline {
+ background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
+}
+
+.stat-icon.busy {
+ background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
+}
+
+.stat-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.stat-number {
+ font-size: 1.75rem;
+ font-weight: 700;
+ color: #2c3e50;
+ line-height: 1;
+}
+
+.stat-label {
+ font-size: 0.875rem;
+ color: #6c757d;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+/* Tabla mejorada */
+.table-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 20px;
+ background: #f8f9fa;
+ border-bottom: 1px solid #dee2e6;
+}
+
+.table-info {
+ color: #6c757d;
+ font-size: 0.9rem;
+}
+
+.table-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.column-header {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.sort-button {
+ opacity: 0.5;
+ transition: opacity 0.3s ease;
+}
+
+.sort-button:hover {
+ opacity: 1;
+}
+
+.sort-button.active {
+ opacity: 1;
+ color: #667eea;
+}
+
+/* Celdas mejoradas */
+.client-cell {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.client-name {
+ font-weight: 500;
+ color: #212529;
+}
+
+.client-ip {
+ font-size: 0.85rem;
+ color: #212529;
+}
+
+.client-mac {
+ font-size: 0.75rem;
+ color: #6c757d;
+}
+
+.oglive-cell {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.oglive-kernel {
+ font-size: 0.85rem;
+ color: #212529;
+}
+
+.oglive-date {
+ font-size: 0.7rem;
+ color: #6c757d;
+ font-style: italic;
+}
+
+/* Botones de acción */
+.action-buttons {
+ display: flex;
+ gap: 5px;
+ justify-content: center;
+}
+
+/* Filas seleccionadas */
+.selected-row {
+ background-color: rgba(102, 126, 234, 0.1) !important;
+}
+
+.mat-row:hover {
+ background-color: rgba(102, 126, 234, 0.05);
+ cursor: pointer;
+}
+
+/* Responsive para nuevas funcionalidades */
+@media (max-width: 768px) {
+ .stats-container {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0.75rem;
+ padding: 0 0.5rem;
+ }
+
+ .stat-card {
+ padding: 1rem;
+ gap: 0.75rem;
+ }
+
+ .stat-icon {
+ width: 40px;
+ height: 40px;
+ font-size: 20px;
+ }
+
+ .stat-number {
+ font-size: 1.5rem;
+ }
+
+ .stat-label {
+ font-size: 0.75rem;
+ }
+
+ .table-header {
+ flex-direction: column;
+ gap: 10px;
+ text-align: center;
+ }
+
+ .action-buttons {
+ flex-direction: column;
+ gap: 2px;
+ }
+}
+
+@media (max-width: 480px) {
+ .stats-container {
+ grid-template-columns: 1fr;
+ gap: 0.5rem;
+ }
+
+ .stat-card {
+ padding: 0.75rem;
+ }
+
+ .stat-icon {
+ width: 36px;
+ height: 36px;
+ font-size: 18px;
+ }
+
+ .stat-number {
+ font-size: 1.25rem;
+ }
+
+ .stat-label {
+ font-size: 0.7rem;
+ }
+}
+
+/* Animaciones */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.stats-container, .table-header {
+ animation: fadeIn 0.5s ease-out;
+}
+
+/* Separadores visuales */
+.tree-node:not(:last-child) {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+ margin-bottom: 2px;
+}
+
+.tree-node:hover:not(:last-child) {
+ border-bottom-color: rgba(102, 126, 234, 0.2);
+}
+
+/* Dashboard de Estadísticas */
+.dashboard-stats {
+ margin: 1rem 0 1.5rem 0;
+ padding: 0 1rem;
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ max-width: 100%;
+}
+
+.stat-card {
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
+ border: 1px solid #e9ecef;
+ border-radius: 12px;
+ padding: 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ position: relative;
+ overflow: hidden;
+}
+
+.stat-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.stat-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
+ border-color: #667eea;
+}
+
+.stat-card:hover::before {
+ opacity: 1;
+}
+
+.stat-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ color: white;
+ font-size: 28px;
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+ transition: all 0.3s ease;
+}
+
+.stat-card:hover .stat-icon {
+ transform: scale(1.1);
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
+}
+
+.stat-icon.online {
+ background: linear-gradient(135deg, #28a745, #20c997);
+ box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
+}
+
+.stat-icon.offline {
+ background: linear-gradient(135deg, #dc3545, #c82333);
+ box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
+}
+
+.stat-icon.busy {
+ background: linear-gradient(135deg, #ffc107, #e0a800);
+ box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
+}
+
+.stat-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.stat-number {
+ font-size: 2rem;
+ font-weight: 700;
+ color: #2c3e50;
+ line-height: 1;
+ margin: 0;
+}
+
+.stat-label {
+ font-size: 0.9rem;
+ color: #6c757d;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin: 0;
+}
+
+/* Responsive para el dashboard */
+@media (max-width: 1200px) {
+ .stats-grid {
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 0.75rem;
+ }
+
+ .stat-card {
+ padding: 1.25rem;
+ }
+
+ .stat-icon {
+ width: 50px;
+ height: 50px;
+ font-size: 24px;
+ }
+
+ .stat-number {
+ font-size: 1.75rem;
+ }
+}
+
+@media (max-width: 768px) {
+ .dashboard-stats {
+ margin: 0.75rem 0 1rem 0;
+ padding: 0 0.5rem;
+ }
+
+ .stats-grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0.5rem;
+ }
+
+ .stat-card {
+ padding: 1rem;
+ flex-direction: column;
+ text-align: center;
+ gap: 0.75rem;
+ }
+
+ .stat-icon {
+ width: 45px;
+ height: 45px;
+ font-size: 22px;
+ }
+
+ .stat-number {
+ font-size: 1.5rem;
+ }
+
+ .stat-label {
+ font-size: 0.8rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .stats-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* Estilos para truncate en tabla */
+.truncate-cell {
+ max-width: 150px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.truncate-cell-wide {
+ max-width: 180px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.truncate-cell-medium {
+ max-width: 140px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.truncate-cell-narrow {
+ max-width: 100px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Para vista de tarjetas */
+.client-card .client-name {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: block;
+}
+
+/* Responsive para truncate */
+@media (max-width: 1200px) {
+ .truncate-cell {
+ max-width: 120px;
+ }
+
+ .truncate-cell-wide {
+ max-width: 150px;
+ }
+
+ .truncate-cell-medium {
+ max-width: 110px;
+ }
+
+ .truncate-cell-narrow {
+ max-width: 80px;
+ }
+}
+
+@media (max-width: 768px) {
+ .truncate-cell {
+ max-width: 100px;
+ }
+
+ .truncate-cell-wide {
+ max-width: 120px;
+ }
+
+ .truncate-cell-medium {
+ max-width: 90px;
+ }
+
+ .truncate-cell-narrow {
+ max-width: 70px;
+ }
+}
+
diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html
index bf30523..15345e3 100644
--- a/ogWebconsole/src/app/components/groups/groups.component.html
+++ b/ogWebconsole/src/app/components/groups/groups.component.html
@@ -1,3 +1,8 @@
+
+
+
@@ -205,6 +267,10 @@
storage
{{ 'partitions' | translate }}
+
+ pending_actions
+ {{ 'colaAcciones' | translate }}
+
0)" [runScriptContext]="selectedNode?.name || ''"
@@ -254,6 +320,45 @@
text="{{ 'clientsViewStepText' | translate }}">
+
+
+
+ computer
+
+
+
{{ totalStats.total }}
+
{{ 'totalClients' | translate }}
+
+
+
+
+ wifi_off
+
+
+
{{ getStatusCount('off') }}
+
{{ 'offline' | translate }}
+
+
+
+
+ wifi
+
+
+
{{ getStatusCount('og-live') + getStatusCount('linux') + getStatusCount('windows') }}
+
{{ 'online' | translate }}
+
+
+
+
+ hourglass_empty
+
+
+
{{ getStatusCount('busy') }}
+
{{ 'busy' | translate }}
+
+
+
+
@@ -262,7 +367,7 @@
[indeterminate]="selection.hasValue() && !isAllSelected()">
-
+
@@ -271,9 +376,9 @@
alt="Client Icon" class="client-image" />
-
{{ client.name }}
+
{{ client.name }}
{{ client.ip }}
-
{{ client.mac }}
+
{{ client.mac }}
list_alt
{{ 'procedimientosCliente' | translate }}
+
+ pending_actions
+ {{ 'colaAcciones' | translate }}
+
delete
{{ 'delete' | translate }}
@@ -327,10 +436,24 @@
-
+
+
+
-
+
- {{ 'status' | translate }}
+
+
+ {{ 'status' | translate }}
+
+ {{ getSortIcon('status') }}
+
+
+
@@ -359,72 +489,101 @@
- {{ 'name' | translate }}
+
+
+ {{ 'name' | translate }}
+
+ {{ getSortIcon('name') }}
+
+
+
- {{ client.name }}
+
+ {{ client.name }}
+
- IP
+
+
+ IP
+
+ {{ getSortIcon('ip') }}
+
+
+
-
-
{{ client.ip }}
-
{{ client.mac }}
+
+ {{ client.ip }}
+ {{ client.mac }}
- {{ 'firmwareType' | translate }}
+
+
+ {{ 'firmwareType' | translate }}
+
+ {{ getSortIcon('firmwareType') }}
+
+
+
-
+
{{ client.firmwareType }}
- OG Live
+ OG Live
-
-
{{ client.ogLive?.kernel }}
-
{{ client.ogLive?.date | date }}
+
+ {{ client.ogLive?.kernel }}
+ {{ client.ogLive?.date | date }}
+
- {{ 'maintenance' | translate }}
+ {{ 'maintenance' | translate }}
{{ client.maintenance }}
- {{ 'subnet' | translate }}
+ {{ 'subnet' | translate }}
{{ client.subnet }}
- {{ 'pxeTemplate' | translate }}
- {{ client.pxeTemplate?.name }}
+ {{ 'pxeTemplate' | translate }}
+ {{ client.pxeTemplate?.name }}
- {{ 'parent' | translate }}
- {{ client.parentName }}
+ {{ 'parent' | translate }}
+ {{ client.parentName }}
- {{ 'actions' | translate }}
+ {{ 'actions' | translate }}
- 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
- mat-icon-button [matMenuTriggerFor]="clientMenu" color="primary">
- more_vert
-
- 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
- [runScriptContext]="getRunScriptContext([client])">
-
+
edit
@@ -442,6 +601,10 @@
list_alt
{{ 'procedimientosCliente' | translate }}
+
+ pending_actions
+ {{ 'colaAcciones' | translate }}
+
delete
{{ 'delete' | translate }}
@@ -449,9 +612,11 @@
-
-
+
+
{
let component: GroupsComponent;
@@ -39,7 +40,7 @@ describe('GroupsComponent', () => {
};
await TestBed.configureTestingModule({
- declarations: [GroupsComponent, ExecuteCommandComponent, LoadingComponent],
+ declarations: [GroupsComponent, ExecuteCommandComponent, LoadingComponent, ModalOverlayComponent],
imports: [
HttpClientTestingModule,
ToastrModule.forRoot(),
diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts
index c301173..a0eea6c 100644
--- a/ogWebconsole/src/app/components/groups/groups.component.ts
+++ b/ogWebconsole/src/app/components/groups/groups.component.ts
@@ -15,7 +15,6 @@ import { ShowOrganizationalUnitComponent } from './shared/organizational-units/s
import { LegendComponent } from './shared/legend/legend.component';
import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component';
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
-import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { PageEvent } from '@angular/material/paginator';
import { CreateMultipleClientComponent } from "./shared/clients/create-multiple-client/create-multiple-client.component";
@@ -31,6 +30,7 @@ import { PartitionTypeOrganizatorComponent } from './shared/partition-type-organ
import { ClientTaskLogsComponent } from '../task-logs/client-task-logs/client-task-logs.component';
import {ChangeParentComponent} from "./shared/change-parent/change-parent.component";
import { AuthService } from '@services/auth.service';
+import { ClientPendingTasksComponent } from '../task-logs/client-pending-tasks/client-pending-tasks.component';
enum NodeType {
OrganizationalUnit = 'organizational-unit',
@@ -79,6 +79,29 @@ export class GroupsComponent implements OnInit, OnDestroy {
arrayClients: any[] = [];
filters: { [key: string]: string } = {};
private clientFilterSubject = new Subject();
+ loading = false;
+
+ // Nuevas propiedades para funcionalidades mejoradas
+ selectedClient: any = null;
+ sortBy: string = 'name';
+ sortDirection: 'asc' | 'desc' = 'asc';
+ currentSortColumn: string = 'name';
+
+ // Estadísticas totales
+ totalStats: {
+ total: number;
+ off: number;
+ online: number;
+ busy: number;
+ } = {
+ total: 0,
+ off: 0,
+ online: 0,
+ busy: 0
+ };
+
+ // Tipos de firmware disponibles
+ firmwareTypes: string[] = [];
protected status = [
{ value: 'off', name: 'Apagado' },
@@ -95,16 +118,6 @@ export class GroupsComponent implements OnInit, OnDestroy {
displayedColumns: string[] = ['select', 'status', 'ip', 'firmwareType', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
- private _sort!: MatSort;
-
- @ViewChild(MatSort)
- set matSort(ms: MatSort) {
- this._sort = ms;
- if (this.selectedClients) {
- this.selectedClients.sort = this._sort;
- }
- }
-
private subscriptions: Subscription = new Subscription();
constructor(
@@ -404,8 +417,14 @@ export class GroupsComponent implements OnInit, OnDestroy {
public fetchClientsForNode(node: any, selectedClientsBeforeEdit: string[] = []): void {
const params = new HttpParams({ fromObject: this.filters });
+ // Agregar parámetros de ordenamiento al backend
+ let backendParams = { ...this.filters };
+ if (this.sortBy) {
+ backendParams['order[' + this.sortBy + ']'] = this.sortDirection;
+ }
+
this.isLoadingClients = true;
- this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params }).subscribe({
+ this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: backendParams }).subscribe({
next: (response: any) => {
this.selectedClients.data = response['hydra:member'];
if (this.selectedNode) {
@@ -423,6 +442,9 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.selection.select(client);
}
});
+
+ // Calcular estadísticas después de cargar los clientes
+ this.calculateLocalStats();
},
error: () => {
this.isLoadingClients = false;
@@ -438,25 +460,35 @@ export class GroupsComponent implements OnInit, OnDestroy {
}
addOU(event: MouseEvent, parent: TreeNode | null = null): void {
+ this.loading = true;
event.stopPropagation();
const dialogRef = this.dialog.open(ManageOrganizationalUnitComponent, {
data: { parent },
width: '900px',
+ disableClose: true,
+ hasBackdrop: true,
+ backdropClass: 'non-clickable-backdrop',
});
dialogRef.afterClosed().subscribe((newUnit) => {
if (newUnit) {
this.refreshData(newUnit.uuid);
}
+ this.loading = false;
});
+
}
addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
+ this.loading = true;
event.stopPropagation();
const targetNode = organizationalUnit || this.selectedNode;
const dialogRef = this.dialog.open(ManageClientComponent, {
data: { organizationalUnit: targetNode },
width: '900px',
+ disableClose: true,
+ hasBackdrop: true,
+ backdropClass: 'non-clickable-backdrop',
});
dialogRef.afterClosed().subscribe((result) => {
@@ -469,17 +501,22 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.refreshData(parentNode.uuid);
}
}
+ this.loading = false;
});
}
addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
+ this.loading = true;
event.stopPropagation();
const targetNode = organizationalUnit || this.selectedNode;
const dialogRef = this.dialog.open(CreateMultipleClientComponent, {
data: { organizationalUnit: targetNode },
width: '900px',
+ disableClose: true,
+ hasBackdrop: true,
+ backdropClass: 'non-clickable-backdrop',
});
dialogRef.afterClosed().subscribe((result) => {
if (result?.success) {
@@ -494,29 +531,33 @@ export class GroupsComponent implements OnInit, OnDestroy {
console.error('No se encontró el nodo padre después de la creación masiva.');
}
}
+ this.loading = false;
});
}
onEditNode(event: MouseEvent, node: TreeNode | null): void {
event.stopPropagation();
+ this.loading = true;
const uuid = node ? this.extractUuid(node['@id']) : null;
if (!uuid) return;
const dialogRef = node?.type !== NodeType.Client
- ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
- : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' });
+ ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' })
+ : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' });
dialogRef.afterClosed().subscribe((result) => {
if (result?.success) {
this.refreshData(node?.id);
}
this.menuTriggers.forEach(trigger => trigger.closeMenu());
+ this.loading = false;
});
}
onDeleteClick(event: MouseEvent, entity: TreeNode | Client | null): void {
event.stopPropagation();
+ this.loading = true;
if (!entity) return;
const uuid = entity['@id'] ? this.extractUuid(entity['@id']) : null;
@@ -533,6 +574,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
if (result === true) {
this.deleteEntityorClient(uuid, type);
}
+ this.loading = false;
});
}
@@ -570,16 +612,18 @@ export class GroupsComponent implements OnInit, OnDestroy {
onEditClick(event: MouseEvent, type: string, uuid: string): void {
event.stopPropagation();
+ this.loading = true;
const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid);
const dialogRef = type !== NodeType.Client
- ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
- : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' });
+ ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' })
+ : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' });
dialogRef.afterClosed().subscribe((result) => {
if (result?.success) {
this.refreshData(this.selectedNode?.id, selectedClientsBeforeEdit);
}
this.menuTriggers.forEach(trigger => trigger.closeMenu());
+ this.loading = false;
});
}
@@ -592,6 +636,9 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.dialog.open(ClassroomViewDialogComponent, {
width: '90vw',
data: { clients: response['hydra:member'] },
+ disableClose: true,
+ hasBackdrop: true,
+ backdropClass: 'non-clickable-backdrop',
});
},
(error) => {
@@ -603,35 +650,46 @@ export class GroupsComponent implements OnInit, OnDestroy {
executeCommand(command: Command, selectedNode: TreeNode | null): void {
-
+ this.loading = true;
if (!selectedNode) {
this.toastr.error('No hay un nodo seleccionado.');
return;
} else {
this.toastr.success(`Ejecutando comando: ${command.name} en ${selectedNode.name}`);
}
+ this.loading = false;
}
onShowClientDetail(event: MouseEvent, client: Client): void {
event.stopPropagation();
- this.dialog.open(ClientDetailsComponent, {
+ this.loading = true;
+ const dialogRef = this.dialog.open(ClientDetailsComponent, {
width: '70vw',
height: '90vh',
data: { clientData: client },
+ disableClose: true,
+ hasBackdrop: true,
+ backdropClass: 'non-clickable-backdrop',
})
+
+ dialogRef.afterClosed().subscribe((result) => {
+ this.loading = false;
+ });
}
onShowDetailsClick(event: MouseEvent, data: TreeNode | null): void {
event.stopPropagation();
+ this.loading = true;
if (data && data.type !== NodeType.Client) {
- this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '800px' });
+ this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '800px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' });
} else {
if (data) {
this.router.navigate(['clients', this.extractUuid(data['@id'])], { state: { clientData: data } });
}
}
+ this.loading = false;
}
@@ -873,6 +931,9 @@ export class GroupsComponent implements OnInit, OnDestroy {
const dialogRef = this.dialog.open(ChangeParentComponent, {
data: { clients: this.selection.selected },
width: '700px',
+ disableClose: true,
+ hasBackdrop: true,
+ backdropClass: 'non-clickable-backdrop',
});
dialogRef.afterClosed().subscribe((result) => {
@@ -883,11 +944,269 @@ export class GroupsComponent implements OnInit, OnDestroy {
}
openClientTaskLogs(event: MouseEvent, client: Client): void {
+ this.loading = true;
event.stopPropagation();
- this.dialog.open(ClientTaskLogsComponent, {
+ const dialogRef = this.dialog.open(ClientTaskLogsComponent, {
width: '1200px',
- data: { client }
+ data: { client },
+ disableClose: true,
+ hasBackdrop: true,
+ backdropClass: 'non-clickable-backdrop',
})
+
+ dialogRef.afterClosed().subscribe((result) => {
+ this.loading = false;
+ });
+ }
+
+ openClientPendingTasks(event: MouseEvent, client: Client): void {
+ this.loading = true;
+ event.stopPropagation();
+
+ const dialogRef = this.dialog.open(ClientPendingTasksComponent, {
+ width: '1200px',
+ data: { client },
+ disableClose: true,
+ hasBackdrop: true,
+ backdropClass: 'non-clickable-backdrop',
+ })
+
+ dialogRef.afterClosed().subscribe((result) => {
+ this.loading = false;
+ });
+ }
+
+ openOUPendingTasks(event: MouseEvent, node: any): void {
+ event.stopPropagation();
+ this.loading = true;
+
+ this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=1&itemsPerPage=10000`).subscribe({
+ next: (response) => {
+ const allClients = response['hydra:member'] || [];
+
+ if (allClients.length === 0) {
+ this.toastr.warning('Esta unidad organizativa no tiene clientes');
+ return;
+ }
+
+ const ouClientData = {
+ name: node.name,
+ id: node.id,
+ uuid: node.uuid,
+ type: 'organizational-unit',
+ clients: allClients
+ };
+
+ const dialogRef = this.dialog.open(ClientPendingTasksComponent, {
+ width: '1200px',
+ data: { client: ouClientData, isOrganizationalUnit: true },
+ disableClose: true,
+ hasBackdrop: true,
+ backdropClass: 'non-clickable-backdrop',
+ });
+
+ dialogRef.afterClosed().subscribe((result) => {
+ this.loading = false;
+ });
+ },
+ error: (error) => {
+ console.error('Error al obtener los clientes de la unidad organizativa:', error);
+ this.toastr.error('Error al cargar los clientes de la unidad organizativa');
+ this.loading = false;
+ }
+ });
+ }
+
+ // Métodos para paginación
+ getPaginationFrom(): number {
+ return (this.page * this.itemsPerPage) + 1;
+ }
+
+ getPaginationTo(): number {
+ return Math.min((this.page + 1) * this.itemsPerPage, this.length);
+ }
+
+ getPaginationTotal(): number {
+ return this.length;
+ }
+
+ refreshClientData(): void {
+ this.fetchClientsForNode(this.selectedNode);
+ this.toastr.success('Datos actualizados', 'Éxito');
+ }
+
+ exportToCSV(): void {
+ const headers = ['Nombre', 'IP', 'MAC', 'Estado', 'Firmware', 'Subnet', 'Parent'];
+ const csvData = this.arrayClients.map(client => [
+ client.name,
+ client.ip || '',
+ client.mac || '',
+ client.status || '',
+ client.firmwareType || '',
+ client.subnet || '',
+ client.parentName || ''
+ ]);
+
+ const csvContent = [headers, ...csvData]
+ .map(row => row.map(cell => `"${cell}"`).join(','))
+ .join('\n');
+
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ const url = URL.createObjectURL(blob);
+ link.setAttribute('href', url);
+ link.setAttribute('download', `clients_${new Date().toISOString().split('T')[0]}.csv`);
+ link.style.visibility = 'hidden';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ this.toastr.success('Archivo CSV exportado correctamente', 'Éxito');
+ }
+
+ private calculateLocalStats(): void {
+ const clients = this.arrayClients;
+ this.totalStats = {
+ total: clients.length,
+ off: clients.filter(client => client.status === 'off').length,
+ online: clients.filter(client => ['og-live', 'linux', 'windows', 'linux-session', 'windows-session'].includes(client.status)).length,
+ busy: clients.filter(client => client.status === 'busy').length
+ };
+
+ // Actualizar tipos de firmware disponibles
+ this.firmwareTypes = [...new Set(clients.map(client => client.firmwareType).filter(Boolean))];
+ }
+
+ // Métodos para funcionalidades mejoradas
+
+ selectClient(client: any): void {
+ this.selectedClient = client;
+ }
+
+ sortColumn(columnDef: string): void {
+ if (this.currentSortColumn === columnDef) {
+ this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
+ } else {
+ this.currentSortColumn = columnDef;
+ this.sortDirection = 'asc';
+ }
+ this.sortBy = columnDef;
+ this.onSortChange();
+ }
+
+ getSortIcon(columnDef: string): string {
+ if (this.currentSortColumn !== columnDef) {
+ return 'unfold_more';
+ }
+ return this.sortDirection === 'asc' ? 'expand_less' : 'expand_more';
+ }
+
+ onSortChange(): void {
+ // Hacer nueva llamada al backend con el ordenamiento actualizado
+ this.fetchClientsForNode(this.selectedNode);
+ }
+
+ getStatusCount(status: string): number {
+ switch(status) {
+ case 'off':
+ return this.totalStats.off;
+ case 'online':
+ return this.totalStats.online;
+ case 'busy':
+ return this.totalStats.busy;
+ default:
+ return this.arrayClients.filter(client => client.status === status).length;
+ }
+ }
+
+ // Métodos para el árbol mejorado
+
+ expandAll(): void {
+ this.treeControl.expandAll();
+ }
+
+ collapseAll(): void {
+ this.treeControl.collapseAll();
+ }
+
+ getNodeTypeTooltip(nodeType: string): string {
+ switch (nodeType) {
+ case 'organizational-unit':
+ return 'Unidad Organizacional - Estructura principal de la organización';
+ case 'classrooms-group':
+ return 'Grupo de Aulas - Conjunto de aulas relacionadas';
+ case 'classroom':
+ return 'Aula - Espacio físico con equipos informáticos';
+ case 'clients-group':
+ return 'Grupo de Equipos - Conjunto de equipos informáticos';
+ case 'client':
+ return 'Equipo Informático - Computadora o dispositivo individual';
+ case 'group':
+ return 'Grupo - Agrupación lógica de elementos';
+ default:
+ return 'Elemento del árbol organizacional';
+ }
+ }
+
+ getNodeCountLabel(count: number): string {
+ if (count === 1) return 'elemento';
+ return 'elementos';
+ }
+
+ getStatusLabel(status: string): string {
+ const statusLabels: { [key: string]: string } = {
+ 'off': 'Apagado',
+ 'og-live': 'OG Live',
+ 'linux': 'Linux',
+ 'linux-session': 'Linux Session',
+ 'windows': 'Windows',
+ 'windows-session': 'Windows Session',
+ 'busy': 'Ocupado',
+ 'mac': 'Mac',
+ 'disconnected': 'Desconectado',
+ 'initializing': 'Inicializando'
+ };
+ return statusLabels[status] || status;
+ }
+
+ // Funciones para el dashboard de estadísticas
+ getTotalOrganizationalUnits(): number {
+ let total = 0;
+ const countOrganizationalUnits = (nodes: TreeNode[]) => {
+ nodes.forEach(node => {
+ if (node.type === 'organizational-unit') {
+ total += 1;
+ }
+ if (node.children) {
+ countOrganizationalUnits(node.children);
+ }
+ });
+ };
+ countOrganizationalUnits(this.originalTreeData);
+ return total;
+ }
+
+ getTotalClassrooms(): number {
+ let total = 0;
+ const countClassrooms = (nodes: TreeNode[]) => {
+ nodes.forEach(node => {
+ if (node.type === 'classroom') {
+ total += 1;
+ }
+ if (node.children) {
+ countClassrooms(node.children);
+ }
+ });
+ };
+ countClassrooms(this.originalTreeData);
+ return total;
+ }
+
+ // Función para actualizar estadísticas cuando cambian los datos
+ private updateDashboardStats(): void {
+ // Las estadísticas de equipos ya se calculan en calculateLocalStats()
+ // Solo necesitamos asegurar que se actualicen cuando cambian los datos
+ this.calculateLocalStats();
}
}
diff --git a/ogWebconsole/src/app/components/groups/shared/client-view/client-view.component.ts b/ogWebconsole/src/app/components/groups/shared/client-view/client-view.component.ts
index 1b7944a..a17d9b6 100644
--- a/ogWebconsole/src/app/components/groups/shared/client-view/client-view.component.ts
+++ b/ogWebconsole/src/app/components/groups/shared/client-view/client-view.component.ts
@@ -35,7 +35,7 @@ export class ClientViewComponent {
{ property: 'Fecha de creación', value: this.data.client.createdAt },
{ property: 'NTP', value: this.data.client.organizationalUnit?.networkSettings?.ntp || '' },
{ property: 'Modo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pMode || '' },
- { property: 'Tiempo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' },
+ ...(this.data.client.organizationalUnit?.networkSettings?.p2pMode === 'seeder' ? [{ property: 'Tiempo p2p (minutos)', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' }] : []),
{ property: 'IP multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastIp || '' },
{ property: 'Modo multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastMode || '' },
{ property: 'Puerto multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastPort || '' },
@@ -51,7 +51,7 @@ export class ClientViewComponent {
{ property: 'Router', value: this.data.client.organizationalUnit?.networkSettings?.router || '' },
{ property: 'NTP', value: this.data.client.organizationalUnit?.networkSettings?.ntp || '' },
{ property: 'Modo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pMode || '' },
- { property: 'Tiempo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' },
+ ...(this.data.client.organizationalUnit?.networkSettings?.p2pMode === 'seeder' ? [{ property: 'Tiempo p2p (minutos)', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' }] : []),
{ property: 'IP multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastIp || '' },
{ property: 'Modo multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastMode || '' },
{ property: 'Puerto multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastPort || '' },
diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html
index 697533b..ec3190c 100644
--- a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html
+++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html
@@ -134,8 +134,8 @@
-
- {{ 'p2pTimeLabel' | translate }}
+
+ {{ 'p2pTimeLabel' | translate }} (minutos)
diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts
index 792db32..7d573b9 100644
--- a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts
+++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts
@@ -271,6 +271,13 @@ export class ManageOrganizationalUnitComponent implements OnInit {
onSubmit() {
if (this.generalFormGroup.valid && this.additionalInfoFormGroup.valid && this.networkSettingsFormGroup.valid) {
const parentValue = this.generalFormGroup.value.parent;
+
+ // Preparar networkSettings con lógica condicional para p2pTime
+ const networkSettings = { ...this.networkSettingsFormGroup.value };
+ if (networkSettings.p2pMode !== 'seeder') {
+ networkSettings.p2pTime = null;
+ }
+
const formData = {
name: this.generalFormGroup.value.name,
excludeParentChanges: this.generalFormGroup.value.excludeParentChanges,
@@ -279,7 +286,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
comments: this.additionalInfoFormGroup.value.comments,
remoteCalendar: this.generalFormGroup.value.remoteCalendar,
type: this.generalFormGroup.value.type,
- networkSettings: this.networkSettingsFormGroup.value,
+ networkSettings: networkSettings,
location: this.classroomInfoFormGroup.value.location,
projector: this.classroomInfoFormGroup.value.projector,
board: this.classroomInfoFormGroup.value.board,
diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component.ts b/ogWebconsole/src/app/components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component.ts
index 417f0d7..ee02949 100644
--- a/ogWebconsole/src/app/components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component.ts
+++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component.ts
@@ -92,7 +92,7 @@ export class ShowOrganizationalUnitComponent implements OnInit {
{ property: 'Router', value: this.ou.networkSettings.router },
{ property: 'NTP', value: this.ou.networkSettings.ntp },
{ property: 'Modo P2P', value: this.ou.networkSettings.p2pMode },
- { property: 'Tiempo P2P', value: this.ou.networkSettings.p2pTime },
+ ...(this.ou.networkSettings.p2pMode === 'seeder' ? [{ property: 'Tiempo P2P (minutos)', value: this.ou.networkSettings.p2pTime }] : []),
{ property: 'Mcast IP', value: this.ou.networkSettings.mcastIp },
{ property: 'Mcast Speed', value: this.ou.networkSettings.mcastSpeed },
{ property: 'Mcast Port', value: this.ou.networkSettings.mcastPort },
diff --git a/ogWebconsole/src/app/components/login/login.component.ts b/ogWebconsole/src/app/components/login/login.component.ts
index 4a58364..c5738be 100644
--- a/ogWebconsole/src/app/components/login/login.component.ts
+++ b/ogWebconsole/src/app/components/login/login.component.ts
@@ -65,7 +65,7 @@ export class LoginComponent {
this.openSnackBar(false, 'Bienvenido ' + this.auth.username);
this.router.navigateByUrl('/groups');
this.dialog.open(GlobalStatusComponent, {
- width: '45vw',
+ width: '65vw',
height: '80vh',
});
}
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..ac6d234 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,94 @@ 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;
+}
+
+.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;
+}
+
+.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 @@
help
- Gestionar imágenes git en {{data.repositoryName}}
+ Commits de Git en {{data.repositoryName}}
Ver Información
- Sincronizar base de datos
-
- {{ 'importImageButton' | translate }}
-
-
-
- {{ '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
+
+
+
+
{{ 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
-
-
- visibility
-
+
+
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..099e379 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,50 @@ baseUrl: string;
alertMessage: string | null = null;
repository: any = {};
datePipe: DatePipe = new DatePipe('es-ES');
+ branches: string[] = [];
+ selectedBranch: string = '';
+ loadingBranches: boolean = false;
+ 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 +87,129 @@ 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;
+ 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();
}
- 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 +221,40 @@ 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) {
+ window.open(`http://localhost:3100/oggit/${this.selectedRepository}/commit/${commit.hexsha}`, '_blank');
}
onNoClick(): void {
diff --git a/ogWebconsole/src/app/components/task-logs/client-pending-tasks/client-pending-tasks.component.css b/ogWebconsole/src/app/components/task-logs/client-pending-tasks/client-pending-tasks.component.css
new file mode 100644
index 0000000..308009a
--- /dev/null
+++ b/ogWebconsole/src/app/components/task-logs/client-pending-tasks/client-pending-tasks.component.css
@@ -0,0 +1,143 @@
+.modal-content {
+ max-height: 85vh;
+ overflow-y: auto;
+ padding: 1rem;
+ }
+
+ .header-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 10px;
+ border-bottom: 1px solid #ddd;
+ }
+
+ .header-container-title {
+ flex-grow: 1;
+ text-align: left;
+ margin-left: 1em;
+ }
+
+ .header-actions {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ }
+
+ .header-actions button {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ }
+
+ .calendar-button-row {
+ display: flex;
+ gap: 15px;
+ }
+
+ .lists-container {
+ padding: 16px;
+ }
+
+ .imagesLists-container {
+ flex: 1;
+ }
+
+ .card.unidad-card {
+ height: 100%;
+ box-sizing: border-box;
+ }
+
+ table {
+ width: 100%;
+ }
+
+ .search-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 1.5rem 0rem 0.5rem 0rem;
+ box-sizing: border-box;
+ }
+
+ .search-boolean {
+ flex: 1;
+ padding: 5px;
+ }
+
+ .search-select {
+ flex: 2;
+ padding: 5px;
+ }
+
+ .search-date {
+ flex: 1;
+ padding: 5px;
+ }
+
+ .mat-elevation-z8 {
+ box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
+ }
+
+ .progress-container {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+
+ /* Ajuste para el botón de cancelar en la barra de progreso */
+ .progress-container .cancel-button {
+ margin-left: auto;
+ flex-shrink: 0;
+ }
+
+ .paginator-container {
+ display: flex;
+ justify-content: end;
+ margin-bottom: 30px;
+ }
+
+ .chip-failed {
+ background-color: #e87979 !important;
+ color: white;
+ }
+
+ .chip-success {
+ background-color: #46c446 !important;
+ color: white;
+ }
+
+ .chip-pending {
+ background-color: #bebdbd !important;
+ color: black;
+ }
+
+ .chip-in-progress {
+ background-color: #f5a623 !important;
+ color: white;
+ }
+
+ .status-progress-flex {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ button.cancel-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 5px;
+ }
+
+ .cancel-button {
+ color: red;
+ background-color: transparent;
+ border: none;
+ padding: 0;
+ }
+
+ .cancel-button mat-icon {
+ color: red;
+ }
+
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/task-logs/client-pending-tasks/client-pending-tasks.component.html b/ogWebconsole/src/app/components/task-logs/client-pending-tasks/client-pending-tasks.component.html
new file mode 100644
index 0000000..23da054
--- /dev/null
+++ b/ogWebconsole/src/app/components/task-logs/client-pending-tasks/client-pending-tasks.component.html
@@ -0,0 +1,158 @@
+
+
+
+
+
+ {{ 'commandSelectStepText' | translate }}
+
+
+ {{ translateCommand(command.name) }}
+
+
+
+ close
+
+
+
+
+ Desde
+
+
+
+
+
+
+ Hasta
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ column.header }}
+
+
+
+
+
+
+ {{trace.progress}}%
+
+ cancel
+
+
+
+
+
+
+ {{
+ trace.status === 'pending' ? 'Pendiente' :
+ trace.status
+ }}
+
+
+ cancel
+
+
+
+
+
+
+
+ {{ translateCommand(trace.command) }}
+ {{ trace.jobId }}
+
+
+
+
+
+ {{ trace.client?.name }}
+ {{ trace.client?.ip }}
+
+
+
+
+
+
+ {{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}}
+
+
+
+
+
+ {{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}}
+
+
+
+ {{ column.cell(trace) }}
+
+
+
+
+
+
+
+ {{ 'informationLabel' | translate }}
+
+
+
+
+ mode_comment
+
+
+
+
+
+
+ info
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'closeButton' | translate }}
+
+
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/task-logs/client-pending-tasks/client-pending-tasks.component.ts b/ogWebconsole/src/app/components/task-logs/client-pending-tasks/client-pending-tasks.component.ts
new file mode 100644
index 0000000..e176810
--- /dev/null
+++ b/ogWebconsole/src/app/components/task-logs/client-pending-tasks/client-pending-tasks.component.ts
@@ -0,0 +1,328 @@
+import { Component, OnInit, Inject, ChangeDetectorRef } from '@angular/core';
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
+import { DatePipe } from '@angular/common';
+import { ConfigService } from '@services/config.service';
+import { ToastrService } from 'ngx-toastr';
+import { TranslationService } from '@services/translation.service';
+import { OutputDialogComponent } from '../output-dialog/output-dialog.component';
+import { InputDialogComponent } from '../input-dialog/input-dialog.component';
+import { JoyrideService } from 'ngx-joyride';
+import { FormControl } from '@angular/forms';
+import { Observable } from 'rxjs';
+import { COMMAND_TYPES } from 'src/app/shared/constants/command-types';
+import { DeleteModalComponent } from 'src/app/shared/delete_modal/delete-modal/delete-modal.component';
+
+@Component({
+ selector: 'app-client-pending-tasks',
+ templateUrl: './client-pending-tasks.component.html',
+ styleUrls: ['./client-pending-tasks.component.css']
+})
+export class ClientPendingTasksComponent implements OnInit {
+ baseUrl: string;
+ mercureUrl: string;
+ traces: any[] = [];
+ length: number = 0;
+ itemsPerPage: number = 20;
+ page: number = 0;
+ loading: boolean = true;
+ pageSizeOptions: number[] = [10, 20, 30, 50];
+ datePipe: DatePipe = new DatePipe('es-ES');
+ filters: { [key: string]: any } = {};
+ filteredCommands!: Observable;
+ commandControl = new FormControl();
+ columns = [
+ {
+ columnDef: 'id',
+ header: 'ID',
+ cell: (trace: any) => `${trace.id}`,
+ },
+ {
+ columnDef: 'command',
+ header: 'Comando',
+ cell: (trace: any) => trace.command
+ },
+ {
+ columnDef: 'status',
+ header: 'Estado',
+ cell: (trace: any) => trace.status
+ },
+ {
+ columnDef: 'executedAt',
+ header: 'Ejecución',
+ cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'),
+ },
+ {
+ columnDef: 'finishedAt',
+ header: 'Finalización',
+ cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
+ },
+ ];
+ displayedColumns = [...this.columns.map(column => column.columnDef), 'information'];
+
+ filteredCommands2 = Object.keys(COMMAND_TYPES).map(key => ({
+ name: key,
+ value: key,
+ label: COMMAND_TYPES[key]
+ }));
+
+ today = new Date();
+
+ constructor(
+ private http: HttpClient,
+ @Inject(MAT_DIALOG_DATA) public data: { client: any, isOrganizationalUnit?: boolean },
+ private joyrideService: JoyrideService,
+ private dialog: MatDialog,
+ private cdr: ChangeDetectorRef,
+ private configService: ConfigService,
+ private toastService: ToastrService,
+ private translationService: TranslationService,
+ public dialogRef: MatDialogRef
+ ) {
+ this.baseUrl = this.configService.apiUrl;
+ this.mercureUrl = this.configService.mercureUrl;
+ }
+
+ ngOnInit(): void {
+ // Si es una unidad organizativa, agregar columna de cliente
+ if (this.data.isOrganizationalUnit) {
+ this.columns.splice(2, 0, {
+ columnDef: 'client',
+ header: 'Cliente',
+ cell: (trace: any) => trace.client?.name || 'N/A'
+ });
+ this.displayedColumns = [...this.columns.map(column => column.columnDef), 'information'];
+ }
+
+ this.loadTraces();
+
+ const eventSource = new EventSource(`${this.mercureUrl}?topic=`
+ + encodeURIComponent(`traces`));
+
+ eventSource.onmessage = (event) => {
+ const data = JSON.parse(event.data);
+ if (data && data['@id']) {
+ this.updateTracesStatus(data['@id'], data.status);
+ }
+ }
+ }
+
+ private updateTracesStatus(clientUuid: string, newStatus: string): void {
+ const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
+ if (traceIndex !== -1) {
+ const updatedTraces = [...this.traces];
+
+ updatedTraces[traceIndex] = {
+ ...updatedTraces[traceIndex],
+ status: newStatus
+ };
+
+ this.traces = updatedTraces;
+ this.cdr.detectChanges();
+ }
+ }
+
+ loadTraces(): void {
+ this.loading = true;
+
+ let params = new HttpParams()
+ .set('status', 'pending')
+ .set('page', (this.page + 1).toString())
+ .set('itemsPerPage', this.itemsPerPage.toString());
+
+ // Si es una unidad organizativa, obtener las trazas de todos sus clientes
+ if (this.data.isOrganizationalUnit && this.data.client?.clients) {
+ const clientIds = this.data.client.clients.map((client: any) => client.id);
+ if (clientIds.length > 0) {
+ // Agregar cada ID de cliente como un parámetro separado
+ clientIds.forEach((id: number) => {
+ params = params.append('client.id[]', id.toString());
+ });
+ } else {
+ this.traces = [];
+ this.length = 0;
+ this.loading = false;
+ return;
+ }
+ } else {
+ // Cliente individual
+ const clientId = this.data.client?.id;
+ if (!clientId) {
+ this.loading = false;
+ return;
+ }
+ params = params.set('client.id', clientId.toString());
+ }
+
+ const url = `${this.baseUrl}/traces`;
+
+ console.log('URL con parámetros:', url, params.toString());
+
+ this.http.get(url, { params }).subscribe(
+ (data) => {
+ this.traces = data['hydra:member'];
+ this.length = data['hydra:totalItems'];
+ this.loading = false;
+ },
+ (error) => {
+ console.error('Error fetching traces', error);
+ this.loading = false;
+ }
+ );
+ }
+
+ onOptionCommandSelected(selectedCommand: any): void {
+ this.filters['command'] = selectedCommand.name;
+ this.loadTraces();
+ }
+
+ onOptionStatusSelected(selectedStatus: any): void {
+ this.filters['status'] = selectedStatus;
+ this.loadTraces();
+ }
+
+ openInputModal(inputData: any): void {
+ this.dialog.open(InputDialogComponent, {
+ width: '70vw',
+ height: '60vh',
+ data: { input: inputData }
+ });
+ }
+
+ cancelTrace(trace: any): void {
+ if (trace.status !== 'pending' && trace.status !== 'in-progress') {
+ this.toastService.warning('Solo se pueden cancelar trazas pendientes o en ejecución', 'Advertencia');
+ return;
+ }
+
+ this.dialog.open(DeleteModalComponent, {
+ width: '300px',
+ data: { name: trace.jobId },
+ }).afterClosed().subscribe((result) => {
+ if (result) {
+ if (trace.status === 'in-progress') {
+ this.http.post(`${this.baseUrl}/traces/${trace['@id']}/kill-job`, {
+ jobId: trace.jobId
+ }).subscribe({
+ next: () => {
+ this.toastService.success('Tarea cancelada correctamente');
+ this.loadTraces();
+ },
+ error: (error) => {
+ this.toastService.error(error.error['hydra:description'] || 'Error al cancelar la tarea');
+ console.error('Error cancelling in-progress trace:', error);
+ }
+ });
+ } else {
+ this.http.post(`${this.baseUrl}/traces/server/${trace.uuid}/cancel`, {}).subscribe({
+ next: () => {
+ this.toastService.success('Tarea cancelada correctamente');
+ this.loadTraces();
+ },
+ error: (error) => {
+ this.toastService.error(error.error['hydra:description'] || 'Error al cancelar la tarea');
+ console.error('Error cancelling pending trace:', error);
+ }
+ });
+ }
+ }
+ });
+ }
+
+ resetFilters(clientSearchCommandInput: any, clientSearchStatusInput: any) {
+ clientSearchCommandInput.value = '';
+ clientSearchStatusInput.value = '';
+ this.loadTraces();
+ }
+
+ openOutputModal(outputData: any): void {
+ this.dialog.open(OutputDialogComponent, {
+ width: '500px',
+ data: { input: outputData }
+ });
+ }
+
+ onPageChange(event: any): void {
+ this.page = event.pageIndex;
+ this.itemsPerPage = event.pageSize;
+ this.length = event.length;
+ this.loadTraces();
+ }
+
+ translateCommand(command: string): string {
+ return this.translationService.getCommandTranslation(command);
+ }
+
+ clearCommandFilter(event: Event, clientSearchCommandInput: any): void {
+ clientSearchCommandInput.value = '';
+ this.loadTraces();
+ }
+
+ clearStatusFilter(event: Event, clientSearchStatusInput: any): void {
+ clientSearchStatusInput.value = '';
+ this.loadTraces();
+ }
+
+ onDateFilterChange(): void {
+ this.loadTraces();
+ }
+
+ iniciarTour(): void {
+ this.joyrideService.startTour({
+ steps: [
+ 'tracesTitleStep',
+ 'resetFiltersStep',
+ 'filtersStep',
+ 'tracesProgressStep',
+ 'tracesInfoStep',
+ 'paginationStep'
+ ],
+ showPrevButton: true,
+ themeColor: '#3f51b5'
+ });
+ }
+
+ close(): void {
+ this.dialogRef.close();
+ }
+
+ clearAllActions(): void {
+ if (this.traces.length === 0) {
+ this.toastService.warning('No hay acciones para limpiar');
+ return;
+ }
+
+ // Mostrar confirmación antes de proceder
+ this.dialog.open(DeleteModalComponent, {
+ width: '400px',
+ data: {
+ name: `Todas las acciones mostradas (${this.traces.length} acciones)`,
+ message: '¿Estás seguro de que quieres cancelar todas las acciones mostradas?'
+ },
+ }).afterClosed().subscribe((result) => {
+ if (result) {
+ this.loading = true;
+
+ // Enviar array de traces en el body
+ const tracesToCancel = this.traces.map((trace: any) => trace['@id']);
+
+ this.http.post(`${this.baseUrl}/traces/cancel-multiple`, {
+ traces: tracesToCancel
+ }).subscribe({
+ next: () => {
+ this.toastService.success(`Se han cancelado ${this.traces.length} acciones correctamente`);
+ this.loadTraces(); // Recargar las trazas
+ },
+ error: (error) => {
+ console.error('Error al cancelar las acciones:', error);
+ this.toastService.error('Error al cancelar las acciones');
+ this.loadTraces(); // Recargar las trazas para mostrar el estado actual
+ },
+ complete: () => {
+ this.loading = false;
+ }
+ });
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.css b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.css
index 4a3ab81..4520ce9 100644
--- a/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.css
+++ b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.css
@@ -10,6 +10,15 @@
align-items: center;
padding: 10px 10px;
border-bottom: 1px solid #ddd;
+ background: white;
+ color: #333;
+ border-radius: 8px 8px 0 0;
+}
+
+.header-right {
+ display: flex;
+ gap: 8px;
+ align-items: center;
}
.header-container-title {
@@ -18,6 +27,133 @@
margin-left: 1em;
}
+.header-container-title h2 {
+ margin: 0;
+ font-weight: 500;
+}
+
+.header-actions {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.action-button {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 8px 16px;
+ border-radius: 20px;
+ border: 1px solid #ddd;
+ cursor: pointer;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ background: white;
+ color: #333;
+}
+
+.action-button:hover {
+ background: #f8f9fa;
+ transform: translateY(-1px);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.action-button.secondary {
+ background: #f8f9fa;
+ border-color: #adb5bd;
+}
+
+.action-button.secondary:hover {
+ background: #e9ecef;
+}
+
+/* Estadísticas */
+.stats-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 15px;
+ margin: 20px 0;
+ padding: 0 10px;
+}
+
+.stat-card {
+ background: white;
+ border-radius: 12px;
+ padding: 20px;
+ text-align: center;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ border-left: 4px solid #667eea;
+}
+
+.stat-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
+}
+
+.stat-number {
+ font-size: 2rem;
+ font-weight: bold;
+ color: #667eea;
+ margin-bottom: 5px;
+}
+
+.stat-label {
+ font-size: 0.9rem;
+ color: #666;
+ font-weight: 500;
+}
+
+/* Filtros mejorados */
+.filters-section {
+ background: #f8f9fa;
+ border-radius: 8px;
+ margin: 20px 0;
+ padding: 20px;
+ border: 1px solid #e9ecef;
+}
+
+.filters-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.filters-header h3 {
+ margin: 0;
+ color: #495057;
+ font-weight: 500;
+}
+
+.search-container {
+ display: flex;
+ flex-direction: row;
+ gap: 15px;
+ transition: all 0.3s ease;
+ margin: 1.5rem 0rem 0.5rem 0rem;
+ box-sizing: border-box;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.search-container.expanded {
+ gap: 20px;
+}
+
+.filter-row {
+ display: flex;
+ gap: 15px;
+ align-items: center;
+ width: 100%;
+}
+
+.advanced-filters {
+ border-top: 1px solid #dee2e6;
+ padding-top: 15px;
+ margin-top: 10px;
+}
+
.calendar-button-row {
display: flex;
gap: 15px;
@@ -40,12 +176,9 @@ table {
width: 100%;
}
-.search-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin: 1.5rem 0rem 0.5rem 0rem;
- box-sizing: border-box;
+.search-string {
+ flex: 1;
+ padding: 5px;
}
.search-boolean {
@@ -64,39 +197,142 @@ table {
}
.mat-elevation-z8 {
- box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+/* Tabla mejorada */
+.table-container {
+ background: white;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.table-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 20px;
+ background: #f8f9fa;
+ border-bottom: 1px solid #dee2e6;
+}
+
+.table-info {
+ color: #6c757d;
+ font-size: 0.9rem;
+}
+
+.table-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.column-header {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.sort-button {
+ opacity: 0.5;
+ transition: opacity 0.3s ease;
+}
+
+.sort-button:hover {
+ opacity: 1;
+}
+
+.sort-button.active {
+ opacity: 1;
+ color: #667eea;
+}
+
+/* Celdas mejoradas */
+.command-cell, .client-cell, .date-cell {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.command-name, .client-name {
+ font-weight: 500;
+ color: #212529;
+}
+
+.command-id, .client-ip {
+ font-size: 0.75rem;
+ color: #6c757d;
+}
+
+.date-time {
+ font-size: 0.85rem;
+ color: #212529;
+}
+
+.date-relative {
+ font-size: 0.7rem;
+ color: #6c757d;
+ font-style: italic;
}
.progress-container {
display: flex;
align-items: center;
gap: 10px;
+ width: 100%;
+}
+
+.progress-text {
+ font-size: 0.8rem;
+ font-weight: 500;
+ color: #667eea;
+ min-width: 35px;
+}
+
+/* Ajuste para el botón de cancelar en la barra de progreso */
+.progress-container .cancel-button {
+ margin-left: auto;
+ flex-shrink: 0;
}
.paginator-container {
display: flex;
justify-content: end;
- margin-bottom: 30px;
+ margin: 20px 0;
+ padding: 0 10px;
}
+/* Chips de estado mejorados */
.chip-failed {
- background-color: #e87979 !important;
- color: white;
+ background-color: #ff6b6b !important;
+ color: white !important;
+ font-weight: 500;
}
.chip-success {
- background-color: #46c446 !important;
- color: white;
+ background-color: #51cf66 !important;
+ color: white !important;
+ font-weight: 500;
}
.chip-pending {
- background-color: #bebdbd !important;
- color: black;
+ background-color: #74c0fc !important;
+ color: white !important;
+ font-weight: 500;
}
.chip-in-progress {
- background-color: #f5a623 !important;
- color: white;
+ background-color: #ffd43b !important;
+ color: #212529 !important;
+ font-weight: 500;
+}
+
+.chip-cancelled {
+ background-color: #adb5bd !important;
+ color: white !important;
+ font-weight: 500;
}
.status-progress-flex {
@@ -105,6 +341,48 @@ table {
gap: 8px;
}
+/* Opciones de estado */
+.status-option {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.status-indicator {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+}
+
+.status-indicator.failed { background-color: #dc3545; }
+.status-indicator.success { background-color: #28a745; }
+.status-indicator.pending { background-color: #17a2b8; }
+.status-indicator.in-progress { background-color: #ffc107; }
+.status-indicator.cancelled { background-color: #6c757d; }
+
+/* Opciones de cliente */
+.client-option {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.client-name {
+ font-weight: 500;
+}
+
+.client-details {
+ font-size: 0.8rem;
+ color: #6c757d;
+}
+
+/* Botones de acción */
+.action-buttons {
+ display: flex;
+ gap: 5px;
+ justify-content: center;
+}
+
button.cancel-button {
display: flex;
align-items: center;
@@ -113,12 +391,120 @@ button.cancel-button {
}
.cancel-button {
- color: red;
+ color: #dc3545;
background-color: transparent;
border: none;
padding: 0;
+ transition: all 0.3s ease;
+}
+
+.cancel-button:hover {
+ background-color: rgba(220, 53, 69, 0.1);
+ border-radius: 50%;
}
.cancel-button mat-icon {
- color: red;
+ color: #dc3545;
+}
+
+/* Filas seleccionadas */
+.selected-row {
+ background-color: rgba(102, 126, 234, 0.1) !important;
+}
+
+.mat-row:hover {
+ background-color: rgba(102, 126, 234, 0.05);
+ cursor: pointer;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .header-container {
+ flex-direction: column;
+ gap: 15px;
+ text-align: center;
+ background: white;
+ color: #333;
+ }
+
+ .header-actions {
+ width: 100%;
+ justify-content: center;
+ }
+
+ .stats-container {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .filter-row {
+ grid-template-columns: 1fr;
+ }
+
+ .table-header {
+ flex-direction: column;
+ gap: 10px;
+ text-align: center;
+ }
+
+ .action-buttons {
+ flex-direction: column;
+ gap: 2px;
+ }
+}
+
+/* Animaciones */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.stats-container, .filters-section, .table-container {
+ animation: fadeIn 0.5s ease-out;
+}
+
+/* Estados de carga */
+.loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+/* Botón de cerrar en footer */
+.footer-actions {
+ display: flex;
+ justify-content: flex-end;
+ padding: 20px 0;
+ border-top: 1px solid #e9ecef;
+ margin-top: 20px;
+}
+
+.footer-actions button {
+ min-width: 120px;
+ padding: 10px 24px;
+ font-weight: 500;
+ border-radius: 8px;
+ transition: all 0.3s ease;
+ background-color: white;
+ color: #333;
+ border: 1px solid #ddd;
+}
+
+.footer-actions button:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+ background-color: #f8f9fa;
+}
+
+.action-container {
+ display: flex;
+ justify-content: flex-end;
+ gap: 1em;
+ padding: 1.5em;
}
diff --git a/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.html b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.html
index 843bd76..ab1e27b 100644
--- a/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.html
+++ b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.html
@@ -1,4 +1,4 @@
-
+
-
+
@@ -103,8 +107,8 @@
trace.status
}}
-
+
cancel
@@ -176,4 +180,8 @@
(page)="onPageChange($event)">
-
\ No newline at end of file
+
+
+
+ {{ 'closeButton' | translate }}
+
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.ts b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.ts
index 3b28e20..551a731 100644
--- a/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.ts
+++ b/ogWebconsole/src/app/components/task-logs/client-task-logs/client-task-logs.component.ts
@@ -175,21 +175,41 @@ export class ClientTaskLogsComponent implements OnInit {
}
cancelTrace(trace: any): void {
+ if (trace.status !== 'pending' && trace.status !== 'in-progress') {
+ this.toastService.warning('Solo se pueden cancelar trazas pendientes o en ejecución', 'Advertencia');
+ return;
+ }
+
this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: trace.jobId },
}).afterClosed().subscribe((result) => {
if (result) {
- this.http.post(`${this.baseUrl}/traces/server/${trace.uuid}/cancel`, {}).subscribe({
- next: () => {
- this.toastService.success('Transmision de imagen cancelada');
- this.loadTraces();
- },
- error: (error) => {
- this.toastService.error(error.error['hydra:description']);
- console.error(error.error['hydra:description']);
- }
- });
+ if (trace.status === 'in-progress') {
+ this.http.post(`${this.baseUrl}/traces/${trace['@id']}/cancel`, {
+ job_id: trace.jobId
+ }).subscribe({
+ next: () => {
+ this.toastService.success('Tarea cancelada correctamente');
+ this.loadTraces();
+ },
+ error: (error) => {
+ this.toastService.error(error.error['hydra:description'] || 'Error al cancelar la tarea');
+ console.error('Error cancelling in-progress trace:', error);
+ }
+ });
+ } else {
+ this.http.post(`${this.baseUrl}/traces/server/${trace.uuid}/cancel`, {}).subscribe({
+ next: () => {
+ this.toastService.success('Tarea cancelada correctamente');
+ this.loadTraces();
+ },
+ error: (error) => {
+ this.toastService.error(error.error['hydra:description'] || 'Error al cancelar la tarea');
+ console.error('Error cancelling pending trace:', error);
+ }
+ });
+ }
}
});
}
@@ -311,4 +331,8 @@ export class ClientTaskLogsComponent implements OnInit {
themeColor: '#3f51b5'
});
}
+
+ closeDialog(): void {
+ this.dialog.closeAll();
+ }
}
diff --git a/ogWebconsole/src/app/components/task-logs/task-logs.component.css b/ogWebconsole/src/app/components/task-logs/task-logs.component.css
index 195127a..127ad84 100644
--- a/ogWebconsole/src/app/components/task-logs/task-logs.component.css
+++ b/ogWebconsole/src/app/components/task-logs/task-logs.component.css
@@ -4,6 +4,9 @@
align-items: center;
padding: 10px 10px;
border-bottom: 1px solid #ddd;
+ background: white;
+ color: #333;
+ border-radius: 8px 8px 0 0;
}
.header-container-title {
@@ -12,6 +15,182 @@
margin-left: 1em;
}
+.header-container-title h2 {
+ margin: 0;
+ font-weight: 500;
+}
+
+.header-actions {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.action-button {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 8px 16px;
+ border-radius: 20px;
+ border: 1px solid #ddd;
+ cursor: pointer;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ background: white;
+ color: #333;
+}
+
+.action-button:hover {
+ background: #f8f9fa;
+ transform: translateY(-1px);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.action-button.secondary {
+ background: #f8f9fa;
+ border-color: #adb5bd;
+}
+
+.action-button.secondary:hover {
+ background: #e9ecef;
+}
+
+.stats-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 15px;
+ margin: 20px 0;
+ padding: 0 10px;
+}
+
+.stat-card {
+ background: white;
+ border-radius: 12px;
+ padding: 20px;
+ text-align: center;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ border-left: 4px solid #667eea;
+}
+
+.stat-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
+}
+
+.stat-total {
+ border-left-color: #667eea;
+ background: #667eea;
+ color: white;
+}
+
+.stat-total .stat-number,
+.stat-total .stat-label {
+ color: white;
+}
+
+.stat-today {
+ border-left-color: #17a2b8;
+ background: #17a2b8;
+ color: white;
+}
+
+.stat-today .stat-number,
+.stat-today .stat-label {
+ color: white;
+}
+
+.stat-success {
+ border-left-color: #28a745;
+ background: #28a745;
+ color: white;
+}
+
+.stat-success .stat-number,
+.stat-success .stat-label {
+ color: white;
+}
+
+.stat-failed {
+ border-left-color: #dc3545;
+ background: #dc3545;
+ color: white;
+}
+
+.stat-failed .stat-number,
+.stat-failed .stat-label {
+ color: white;
+}
+
+.stat-in-progress {
+ border-left-color: #ffc107;
+ background: #ffc107;
+ color: #212529;
+}
+
+.stat-in-progress .stat-number,
+.stat-in-progress .stat-label {
+ color: #212529;
+}
+
+.stat-number {
+ font-size: 2rem;
+ font-weight: bold;
+ color: #667eea;
+ margin-bottom: 5px;
+}
+
+.stat-label {
+ font-size: 0.9rem;
+ color: #666;
+ font-weight: 500;
+}
+
+.filters-section {
+ background: #f8f9fa;
+ border-radius: 8px;
+ margin: 20px 0;
+ padding: 20px;
+ border: 1px solid #e9ecef;
+}
+
+.filters-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.filters-header h3 {
+ margin: 0;
+ color: #495057;
+ font-weight: 500;
+}
+
+.search-container {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ transition: all 0.3s ease;
+}
+
+.search-container.expanded {
+ gap: 20px;
+}
+
+.filter-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 15px;
+ align-items: end;
+}
+
+.advanced-filters {
+ border-top: 1px solid #dee2e6;
+ padding-top: 15px;
+ margin-top: 10px;
+}
+
.calendar-button-row {
display: flex;
gap: 15px;
@@ -34,14 +213,6 @@ table {
width: 100%;
}
-.search-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin: 1.5rem 0rem 1.5rem 0rem;
- box-sizing: border-box;
-}
-
.search-string {
flex: 1;
padding: 5px;
@@ -57,40 +228,145 @@ table {
padding: 5px;
}
+.search-date {
+ flex: 1;
+ padding: 5px;
+}
+
.mat-elevation-z8 {
- box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.table-container {
+ background: white;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.table-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 20px;
+ background: #f8f9fa;
+ border-bottom: 1px solid #dee2e6;
+}
+
+.table-info {
+ color: #6c757d;
+ font-size: 0.9rem;
+}
+
+.table-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.column-header {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.sort-button {
+ opacity: 0.5;
+ transition: opacity 0.3s ease;
+}
+
+.sort-button:hover {
+ opacity: 1;
+}
+
+.sort-button.active {
+ opacity: 1;
+ color: #667eea;
+}
+
+.command-cell, .client-cell, .date-cell {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.command-name, .client-name {
+ font-weight: 500;
+ color: #212529;
+}
+
+.command-id, .client-ip {
+ font-size: 0.75rem;
+ color: #6c757d;
+}
+
+.date-time {
+ font-size: 0.85rem;
+ color: #212529;
+}
+
+.date-relative {
+ font-size: 0.7rem;
+ color: #6c757d;
+ font-style: italic;
}
.progress-container {
display: flex;
align-items: center;
gap: 10px;
+ width: 100%;
+}
+
+.progress-text {
+ font-size: 0.8rem;
+ font-weight: 500;
+ color: #667eea;
+ min-width: 35px;
+}
+
+/* Ajuste para el botón de cancelar en la barra de progreso */
+.progress-container .cancel-button {
+ margin-left: auto;
+ flex-shrink: 0;
}
.paginator-container {
display: flex;
justify-content: end;
- margin-bottom: 30px;
+ margin: 20px 0;
+ padding: 0 10px;
}
.chip-failed {
- background-color: #e87979 !important;
- color: white;
+ background-color: #ff6b6b !important;
+ color: white !important;
+ font-weight: 500;
}
.chip-success {
- background-color: #46c446 !important;
- color: white;
+ background-color: #51cf66 !important;
+ color: white !important;
+ font-weight: 500;
}
.chip-pending {
- background-color: #bebdbd !important;
- color: black;
+ background-color: #74c0fc !important;
+ color: white !important;
+ font-weight: 500;
}
.chip-in-progress {
- background-color: #f5a623 !important;
- color: white;
+ background-color: #ffd43b !important;
+ color: #212529 !important;
+ font-weight: 500;
+}
+
+.chip-cancelled {
+ background-color: #adb5bd !important;
+ color: white !important;
+ font-weight: 500;
}
.status-progress-flex {
@@ -99,6 +375,45 @@ table {
gap: 8px;
}
+.status-option {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.status-indicator {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+}
+
+.status-indicator.failed { background-color: #dc3545; }
+.status-indicator.success { background-color: #28a745; }
+.status-indicator.pending { background-color: #17a2b8; }
+.status-indicator.in-progress { background-color: #ffc107; }
+.status-indicator.cancelled { background-color: #6c757d; }
+
+.client-option {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.client-name {
+ font-weight: 500;
+}
+
+.client-details {
+ font-size: 0.8rem;
+ color: #6c757d;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 5px;
+ justify-content: center;
+}
+
button.cancel-button {
display: flex;
align-items: center;
@@ -107,12 +422,83 @@ button.cancel-button {
}
.cancel-button {
- color: red;
+ color: #dc3545;
background-color: transparent;
border: none;
padding: 0;
+ transition: all 0.3s ease;
+}
+
+.cancel-button:hover {
+ background-color: rgba(220, 53, 69, 0.1);
+ border-radius: 50%;
}
.cancel-button mat-icon {
- color: red;
+ color: #dc3545;
+}
+
+.selected-row {
+ background-color: rgba(102, 126, 234, 0.1) !important;
+}
+
+.mat-row:hover {
+ background-color: rgba(102, 126, 234, 0.05);
+ cursor: pointer;
+}
+
+@media (max-width: 768px) {
+ .header-container {
+ flex-direction: column;
+ gap: 15px;
+ text-align: center;
+ background: white;
+ color: #333;
+ }
+
+ .header-actions {
+ width: 100%;
+ justify-content: center;
+ }
+
+ .stats-container {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .filter-row {
+ grid-template-columns: 1fr;
+ }
+
+ .table-header {
+ flex-direction: column;
+ gap: 10px;
+ text-align: center;
+ }
+
+ .action-buttons {
+ flex-direction: column;
+ gap: 2px;
+ }
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.stats-container, .filters-section, .table-container {
+ animation: fadeIn 0.5s ease-out;
+}
+
+.loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(255, 255, 255, 0.8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
}
diff --git a/ogWebconsole/src/app/components/task-logs/task-logs.component.html b/ogWebconsole/src/app/components/task-logs/task-logs.component.html
index eeb2930..39ca165 100644
--- a/ogWebconsole/src/app/components/task-logs/task-logs.component.html
+++ b/ogWebconsole/src/app/components/task-logs/task-logs.component.html
@@ -1,5 +1,5 @@
-
+
-
-
-
-
-
-
- {{ client.name }}
-
- {{ client.ip }} — {{ client.mac }}
-
-
-
+
+
+
{{ totalStats.total }}
+
{{ 'totalTraces' | translate }}
+
+
+
{{ getStatusCount('today') }}
+
{{ 'todayTraces' | translate }}
+
+
+
{{ getStatusCount('success') }}
+
{{ 'successful' | translate }}
+
+
+
{{ getStatusCount('failed') }}
+
{{ 'failed' | translate }}
+
+
+
{{ getStatusCount('in-progress') }}
+
{{ 'inProgress' | translate }}
+
+
-
-
- close
+
+
-
- {{ 'commandSelectStepText' | translate }}
-
-
- {{ translateCommand(command.name) }}
-
-
-
- close
-
-
+
+
+
+
+
+
+
+ {{ client.name }}
+ {{ client.ip }} — {{ client.mac }}
+
+
+
+
+ close
+
+ {{ 'enterClientName' | translate }}
+
+
+ {{ 'commandSelectStepText' | translate }}
+
+
+ {{ translateCommand(command.name) }}
+
+
+
+ close
+
+
-
- Estado
-
- Fallido
- Pendiente de ejecutar
- Ejecutando
- Completado con éxito
- Cancelado
-
-
- close
-
-
+
+ {{ 'status' | translate }}
+
+
+
+
+ {{ 'failed' | translate }}
+
+
+
+
+
+ {{ 'pending' | translate }}
+
+
+
+
+
+ {{ 'inProgress' | translate }}
+
+
+
+
+
+ {{ 'success' | translate }}
+
+
+
+
+
+ {{ 'cancelled' | translate }}
+
+
+
+
+ close
+
+
+
-
- Desde
-
-
-
-
+
+
+ {{ 'fromDate' | translate }}
+
+
+
+
-
- Hasta
-
-
-
-
+
+ {{ 'toDate' | translate }}
+
+
+
+
+
+ {{ 'sortBy' | translate }}
+
+ {{ 'executionDate' | translate }}
+ {{ 'status' | translate }}
+ {{ 'command' | translate }}
+ {{ 'client' | translate }}
+
+
+
+
-
+
+
+
- {{ column.header }}
+
+
+ {{ column.header }}
+
+ {{ getSortIcon(column.columnDef) }}
+
+
+
@@ -102,7 +192,11 @@
- {{trace.progress}}%
+ {{trace.progress}}%
+
+ cancel
+
@@ -116,16 +210,16 @@
'chip-cancelled': trace.status === 'cancelled'
}">
{{
- trace.status === 'failed' ? 'Error' :
- trace.status === 'in-progress' ? 'En ejecución' :
- trace.status === 'success' ? 'Completado' :
- trace.status === 'pending' ? 'Pendiente' :
- trace.status === 'cancelled' ? 'Cancelado' :
+ trace.status === 'failed' ? ('failed' | translate) :
+ trace.status === 'in-progress' ? ('inProgress' | translate) :
+ trace.status === 'success' ? ('successful' | translate) :
+ trace.status === 'pending' ? ('pending' | translate) :
+ trace.status === 'cancelled' ? ('cancelled' | translate) :
trace.status
}}
-
+
cancel
@@ -133,29 +227,30 @@
-
-
{{ translateCommand(trace.command) }}
-
{{ trace.jobId }}
+
+ {{ translateCommand(trace.command) }}
+ {{ trace.jobId }}
-
-
{{ trace.client?.name }}
-
{{ trace.client?.ip }}
+
+ {{ trace.client?.name }}
+ {{ trace.client?.ip }}
-
-
-
{{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}}
+
+ {{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}}
+ {{ getRelativeTime(trace.executedAt) }}
-
-
{{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}}
+
+ {{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}}
+ {{ getRelativeTime(trace.finishedAt) }}
@@ -170,26 +265,31 @@
{{ 'informationLabel' | translate }}
-
-
-
- mode_comment
-
-
-
-
-
-
- info
-
-
-
+
+
+
+ mode_comment
+
+
+
+
+ info
+
+
+
+ cancel
+
+
-
+
diff --git a/ogWebconsole/src/app/components/task-logs/task-logs.component.ts b/ogWebconsole/src/app/components/task-logs/task-logs.component.ts
index 18e441b..5386f4d 100644
--- a/ogWebconsole/src/app/components/task-logs/task-logs.component.ts
+++ b/ogWebconsole/src/app/components/task-logs/task-logs.component.ts
@@ -1,4 +1,4 @@
-import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { ChangeDetectorRef, Component, OnInit, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, forkJoin } from 'rxjs';
import { FormControl } from '@angular/forms';
@@ -20,7 +20,7 @@ import { COMMAND_TYPES } from '../../shared/constants/command-types';
templateUrl: './task-logs.component.html',
styleUrls: ['./task-logs.component.css']
})
-export class TaskLogsComponent implements OnInit {
+export class TaskLogsComponent implements OnInit, OnDestroy {
baseUrl: string;
mercureUrl: string;
traces: any[] = [];
@@ -38,6 +38,30 @@ export class TaskLogsComponent implements OnInit {
bufferValue = 0;
today = new Date();
+ showAdvancedFilters: boolean = false;
+ selectedTrace: any = null;
+ sortBy: string = 'executedAt';
+ sortDirection: 'asc' | 'desc' = 'desc';
+ currentSortColumn: string = 'executedAt';
+
+ totalStats: {
+ total: number;
+ success: number;
+ failed: number;
+ pending: number;
+ inProgress: number;
+ cancelled: number;
+ today: number;
+ } = {
+ total: 0,
+ success: 0,
+ failed: 0,
+ pending: 0,
+ inProgress: 0,
+ cancelled: 0,
+ today: 0
+ };
+
filteredCommands2 = Object.keys(COMMAND_TYPES).map(key => ({
name: key,
value: key,
@@ -49,31 +73,37 @@ export class TaskLogsComponent implements OnInit {
columnDef: 'id',
header: 'ID',
cell: (trace: any) => `${trace.id}`,
+ sortable: true
},
{
columnDef: 'command',
header: 'Comando',
- cell: (trace: any) => trace.command
+ cell: (trace: any) => trace.command,
+ sortable: true
},
{
columnDef: 'client',
header: 'Cliente',
- cell: (trace: any) => trace.client?.name
+ cell: (trace: any) => trace.client?.name,
+ sortable: true
},
{
columnDef: 'status',
header: 'Estado',
- cell: (trace: any) => trace.status
+ cell: (trace: any) => trace.status,
+ sortable: true
},
{
columnDef: 'executedAt',
header: 'Ejecución',
cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'),
+ sortable: true
},
{
columnDef: 'finishedAt',
header: 'Finalización',
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
+ sortable: true
},
];
displayedColumns = [...this.columns.map(column => column.columnDef), 'information'];
@@ -100,6 +130,7 @@ export class TaskLogsComponent implements OnInit {
this.loadTraces();
this.loadCommands();
this.loadClients();
+ this.loadTotalStats();
this.filteredCommands = this.commandControl.valueChanges.pipe(
startWith(''),
map(value => (typeof value === 'string' ? value : value?.name)),
@@ -122,6 +153,169 @@ export class TaskLogsComponent implements OnInit {
}
}
+ ngOnDestroy(): void {
+ }
+
+ toggleFilters(): void {
+ this.showAdvancedFilters = !this.showAdvancedFilters;
+ }
+
+ refreshData(): void {
+ this.loadTraces();
+ this.toastService.success('Datos actualizados', 'Éxito');
+ }
+
+ selectTrace(trace: any): void {
+ this.selectedTrace = trace;
+ }
+
+ sortColumn(columnDef: string): void {
+ if (this.currentSortColumn === columnDef) {
+ this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
+ } else {
+ this.currentSortColumn = columnDef;
+ this.sortDirection = 'desc';
+ }
+ this.sortBy = columnDef;
+ this.onSortChange();
+ }
+
+ getSortIcon(columnDef: string): string {
+ if (this.currentSortColumn !== columnDef) {
+ return 'unfold_more';
+ }
+ return this.sortDirection === 'asc' ? 'expand_less' : 'expand_more';
+ }
+
+ onSortChange(): void {
+ this.loadTraces();
+ }
+
+ getStatusCount(status: string): number {
+ switch(status) {
+ case 'success':
+ return this.totalStats.success;
+ case 'failed':
+ return this.totalStats.failed;
+ case 'pending':
+ return this.totalStats.pending;
+ case 'in-progress':
+ return this.totalStats.inProgress;
+ case 'cancelled':
+ return this.totalStats.cancelled;
+ case 'today':
+ return this.totalStats.today;
+ default:
+ return 0;
+ }
+ }
+
+ getTodayTracesCount(): number {
+ const today = new Date();
+ const todayString = this.datePipe.transform(today, 'yyyy-MM-dd');
+ return this.traces.filter(trace =>
+ trace.executedAt && trace.executedAt.startsWith(todayString)
+ ).length;
+ }
+
+ getRelativeTime(date: string): string {
+ if (!date) return '';
+
+ const now = new Date();
+ const traceDate = new Date(date);
+ const diffInSeconds = Math.floor((now.getTime() - traceDate.getTime()) / 1000);
+
+ if (diffInSeconds < 60) {
+ return 'hace un momento';
+ } else if (diffInSeconds < 3600) {
+ const minutes = Math.floor(diffInSeconds / 60);
+ return `hace ${minutes} minuto${minutes > 1 ? 's' : ''}`;
+ } else if (diffInSeconds < 86400) {
+ const hours = Math.floor(diffInSeconds / 3600);
+ return `hace ${hours} hora${hours > 1 ? 's' : ''}`;
+ } else {
+ const days = Math.floor(diffInSeconds / 86400);
+ return `hace ${days} día${days > 1 ? 's' : ''}`;
+ }
+ }
+
+ exportToCSV(): void {
+ const headers = ['ID', 'Comando', 'Cliente', 'Estado', 'Fecha Ejecución', 'Fecha Finalización', 'Job ID'];
+ const csvData = this.traces.map(trace => [
+ trace.id,
+ this.translateCommand(trace.command),
+ trace.client?.name || '',
+ trace.status,
+ this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'),
+ this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
+ trace.jobId || ''
+ ]);
+
+ const csvContent = [headers, ...csvData]
+ .map(row => row.map(cell => `"${cell}"`).join(','))
+ .join('\n');
+
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ const url = URL.createObjectURL(blob);
+ link.setAttribute('href', url);
+ link.setAttribute('download', `traces_${new Date().toISOString().split('T')[0]}.csv`);
+ link.style.visibility = 'hidden';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ this.toastService.success('Archivo CSV exportado correctamente', 'Éxito');
+ }
+
+ cancelTrace(trace: any): void {
+ if (trace.status !== 'pending' && trace.status !== 'in-progress') {
+ this.toastService.warning('Solo se pueden cancelar trazas pendientes o en ejecución', 'Advertencia');
+ return;
+ }
+
+ const dialogRef = this.dialog.open(DeleteModalComponent, {
+ width: '400px',
+ data: {
+ title: 'Cancelar Traza',
+ message: `¿Estás seguro de que quieres cancelar la traza #${trace.id}?`,
+ confirmText: 'Cancelar',
+ cancelText: 'No cancelar'
+ }
+ });
+
+ dialogRef.afterClosed().subscribe(result => {
+ if (result) {
+ if (trace.status === 'in-progress') {
+ this.http.post(`${this.baseUrl}${trace['@id']}/kill-job`, {
+ jobId: trace.jobId
+ }).subscribe(
+ () => {
+ this.toastService.success('Traza cancelada correctamente', 'Éxito');
+ this.loadTraces();
+ this.loadTotalStats();
+ },
+ (error) => {
+ this.toastService.error('Error al cancelar la traza', 'Error');
+ }
+ );
+ } else {
+ this.http.post(`${this.baseUrl}/traces/cancel-multiple`, {traces: [trace['@id']]}).subscribe(
+ () => {
+ this.toastService.success('Traza cancelada correctamente', 'Éxito');
+ this.loadTraces();
+ this.loadTotalStats();
+ },
+ (error) => {
+ console.error('Error cancelling pending trace:', error);
+ this.toastService.error('Error al cancelar la traza', 'Error');
+ }
+ );
+ }
+ }
+ });
+ }
+
private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void {
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
if (traceIndex !== -1) {
@@ -135,14 +329,9 @@ export class TaskLogsComponent implements OnInit {
this.traces = updatedTraces;
this.cdr.detectChanges();
-
- console.log(`Estado actualizado para la traza ${clientUuid}: ${newStatus}`);
- } else {
- console.warn(`Traza con UUID ${clientUuid} no encontrado en la lista.`);
}
}
-
private _filterClients(value: string): any[] {
const filterValue = value.toLowerCase();
@@ -153,7 +342,6 @@ export class TaskLogsComponent implements OnInit {
);
}
-
private _filterCommands(name: string): any[] {
const filterValue = name.toLowerCase();
return this.commands.filter(command => command.name.toLowerCase().includes(filterValue));
@@ -193,30 +381,11 @@ export class TaskLogsComponent implements OnInit {
});
}
- cancelTrace(trace: any): void {
- this.dialog.open(DeleteModalComponent, {
- width: '300px',
- data: { name: trace.jobId },
- }).afterClosed().subscribe((result) => {
- if (result) {
- this.http.post(`${this.baseUrl}/traces/server/${trace.uuid}/cancel`, {}).subscribe({
- next: () => {
- this.toastService.success('Transmision de imagen cancelada');
- this.loadTraces();
- },
- error: (error) => {
- this.toastService.error(error.error['hydra:description']);
- console.error(error.error['hydra:description']);
- }
- });
- }
- });
- }
-
loadTraces(): void {
this.loading = true;
const url = `${this.baseUrl}/traces?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`;
const params: any = { ...this.filters };
+
if (params['status'] === undefined) {
delete params['status'];
}
@@ -230,12 +399,19 @@ export class TaskLogsComponent implements OnInit {
delete params['endDate'];
}
+ if (this.sortBy) {
+ params['order[' + this.sortBy + ']'] = this.sortDirection;
+ }
+
this.http.get
(url, { params }).subscribe(
(data) => {
this.traces = data['hydra:member'];
this.length = data['hydra:totalItems'];
this.groupedTraces = this.groupByCommandId(this.traces);
this.loading = false;
+ if (Object.keys(this.filters).length === 0) {
+ this.loadTotalStats();
+ }
},
(error) => {
console.error('Error fetching traces', error);
@@ -272,6 +448,52 @@ export class TaskLogsComponent implements OnInit {
);
}
+ loadTotalStats(): void {
+ this.calculateLocalStats();
+ }
+
+ private calculateLocalStats(): void {
+ const statuses = ['success', 'failed', 'pending', 'in-progress', 'cancelled'];
+ const requests = statuses.map(status =>
+ this.http.get(`${this.baseUrl}/traces?status=${status}&page=1&itemsPerPage=1`)
+ );
+
+ const totalRequest = this.http.get(`${this.baseUrl}/traces?page=1&itemsPerPage=1`);
+
+ const todayString = this.datePipe.transform(new Date(), 'yyyy-MM-dd');
+ const todayRequest = this.http.get(`${this.baseUrl}/traces?executedAt[after]=${todayString}&page=1&itemsPerPage=1`);
+
+ forkJoin([totalRequest, ...requests, todayRequest]).subscribe(
+ (responses) => {
+ const totalData = responses[0];
+ const statusData = responses.slice(1, 6);
+ const todayData = responses[6];
+
+ this.totalStats = {
+ total: totalData['hydra:totalItems'],
+ success: statusData[0]['hydra:totalItems'],
+ failed: statusData[1]['hydra:totalItems'],
+ pending: statusData[2]['hydra:totalItems'],
+ inProgress: statusData[3]['hydra:totalItems'],
+ cancelled: statusData[4]['hydra:totalItems'],
+ today: todayData['hydra:totalItems']
+ };
+ },
+ error => {
+ console.error('Error fetching stats by status:', error);
+ const todayString = this.datePipe.transform(new Date(), 'yyyy-MM-dd');
+ this.totalStats = {
+ total: this.length,
+ success: this.traces.filter(trace => trace.status === 'success').length,
+ failed: this.traces.filter(trace => trace.status === 'failed').length,
+ pending: this.traces.filter(trace => trace.status === 'pending').length,
+ inProgress: this.traces.filter(trace => trace.status === 'in-progress').length,
+ cancelled: this.traces.filter(trace => trace.status === 'cancelled').length,
+ today: this.traces.filter(trace => trace.executedAt && trace.executedAt.startsWith(todayString)).length
+ };
+ }
+ );
+ }
resetFilters(clientSearchCommandInput: any, clientSearchStatusInput: any, clientSearchClientInput: any) {
this.loading = true;
@@ -361,4 +583,16 @@ export class TaskLogsComponent implements OnInit {
});
}
+ // Métodos para paginación
+ getPaginationFrom(): number {
+ return (this.page * this.itemsPerPage) + 1;
+ }
+
+ getPaginationTo(): number {
+ return Math.min((this.page + 1) * this.itemsPerPage, this.length);
+ }
+
+ getPaginationTotal(): number {
+ return this.length;
+ }
}
diff --git a/ogWebconsole/src/app/layout/header/header.component.css b/ogWebconsole/src/app/layout/header/header.component.css
index 72b60ae..7e7c1dd 100644
--- a/ogWebconsole/src/app/layout/header/header.component.css
+++ b/ogWebconsole/src/app/layout/header/header.component.css
@@ -2,8 +2,52 @@ mat-toolbar {
/*height: 7vh;*/
min-height: 65px;
min-width: 375px;
- background-color: #e2e8f0;
+ background: rgba(226, 232, 240, 0.8);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
color: black;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 16px;
+ padding: 0 16px;
+}
+
+/* Estilos específicos para el botón del sidebar */
+.navbar-icon {
+ color: #3f51b5;
+ font-size: 24px;
+ width: 24px;
+ height: 24px;
+}
+
+/* Asegurar que el botón del sidebar sea visible */
+mat-toolbar button[mat-icon-button] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ transition: all 0.3s ease;
+ color: #3f51b5;
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+ margin-right: 8px;
+}
+
+mat-toolbar button[mat-icon-button]:hover {
+ background-color: rgba(63, 81, 181, 0.1);
+ transform: scale(1.05);
+ box-shadow: 0 2px 8px rgba(63, 81, 181, 0.2);
}
.navbar-actions-row {
@@ -11,6 +55,7 @@ mat-toolbar {
justify-content: end;
align-items: center;
flex-grow: 1;
+ gap: 8px;
}
.navbar-buttons-row {
@@ -71,6 +116,7 @@ mat-toolbar {
display: flex;
justify-content: center;
align-items: center;
+ gap: 8px;
}
.trace-button .mat-icon {
@@ -81,3 +127,17 @@ mat-toolbar {
margin-right: 2vh;
}
}
+
+.menu-toggle-right {
+ position: absolute;
+ right: 16px;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 1100;
+}
+
+@media (max-width: 576px) {
+ .menu-toggle-right {
+ right: 8px;
+ }
+}
diff --git a/ogWebconsole/src/app/layout/header/header.component.html b/ogWebconsole/src/app/layout/header/header.component.html
index 28d1c25..353e3da 100644
--- a/ogWebconsole/src/app/layout/header/header.component.html
+++ b/ogWebconsole/src/app/layout/header/header.component.html
@@ -3,12 +3,11 @@
matTooltipShowDelay="1000">
-
- menu
-
-
+
+ menu
+
notifications
@@ -37,6 +36,10 @@
+
+ menu
+
notifications
diff --git a/ogWebconsole/src/app/layout/header/header.component.ts b/ogWebconsole/src/app/layout/header/header.component.ts
index c009d8a..20ef4e2 100644
--- a/ogWebconsole/src/app/layout/header/header.component.ts
+++ b/ogWebconsole/src/app/layout/header/header.component.ts
@@ -42,7 +42,7 @@ export class HeaderComponent implements OnInit {
showGlobalStatus() {
this.dialog.open(GlobalStatusComponent, {
- width: '45vw',
+ width: '5vw',
height: '80vh',
})
}
diff --git a/ogWebconsole/src/app/layout/main-layout/main-layout.component.css b/ogWebconsole/src/app/layout/main-layout/main-layout.component.css
index f952919..76054e4 100644
--- a/ogWebconsole/src/app/layout/main-layout/main-layout.component.css
+++ b/ogWebconsole/src/app/layout/main-layout/main-layout.component.css
@@ -6,6 +6,8 @@ html, body {
.container {
height: 100%;
+ padding-top: 65px; /* Asegurar que el contenido no se superponga con el header fijo */
+ box-sizing: border-box;
}
.sidebar {
@@ -28,3 +30,10 @@ html, body {
.mat-list-item:hover {
background-color: #2a2a40;
}
+
+/* Asegurar que el contenido principal tenga el espacio correcto */
+.content {
+ padding-top: 0; /* El padding ya está en el container */
+ height: calc(100vh - 65px); /* Altura total menos el header */
+ overflow: auto;
+}
diff --git a/ogWebconsole/src/app/services/translation.service.ts b/ogWebconsole/src/app/services/translation.service.ts
index b57aa88..b63b440 100644
--- a/ogWebconsole/src/app/services/translation.service.ts
+++ b/ogWebconsole/src/app/services/translation.service.ts
@@ -30,6 +30,18 @@ export class TranslationService {
return COMMAND_TYPES['partition-and-format'][this.currentLang];
case 'run-script':
return COMMAND_TYPES['run-script'][this.currentLang];
+ case 'login':
+ return COMMAND_TYPES.login[this.currentLang];
+ case 'software-inventory':
+ return COMMAND_TYPES['software-inventory'][this.currentLang];
+ case 'hardware-inventory':
+ return COMMAND_TYPES['hardware-inventory'][this.currentLang];
+ case 'rename-image':
+ return COMMAND_TYPES['rename-image'][this.currentLang];
+ case 'transfer-image':
+ return COMMAND_TYPES['transfer-image'][this.currentLang];
+ case 'kill-job':
+ return COMMAND_TYPES['kill-job'][this.currentLang];
default:
return command;
}
diff --git a/ogWebconsole/src/app/shared/constants/command-types.ts b/ogWebconsole/src/app/shared/constants/command-types.ts
index ef81cb5..1d52dbe 100644
--- a/ogWebconsole/src/app/shared/constants/command-types.ts
+++ b/ogWebconsole/src/app/shared/constants/command-types.ts
@@ -42,5 +42,35 @@ export const COMMAND_TYPES: any = {
'run-script': {
en: 'Run Script',
es: 'Ejecutar Script'
+ },
+
+ login: {
+ en: 'Login',
+ es: 'Iniciar sesión'
+ },
+
+ 'software-inventory': {
+ en: 'Software Inventory',
+ es: 'Inventario de Software'
+ },
+
+ 'hardware-inventory': {
+ en: 'Hardware Inventory',
+ es: 'Inventario de Hardware'
+ },
+
+ 'rename-image': {
+ en: 'Rename Image',
+ es: 'Renombrar Imagen'
+ },
+
+ 'transfer-image': {
+ en: 'Transfer Image',
+ es: 'Transferir Imagen'
+ },
+
+ 'kill-job': {
+ en: 'Cancel Task',
+ es: 'Cancelar Tarea'
}
};
diff --git a/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.css b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.css
new file mode 100644
index 0000000..d666f43
--- /dev/null
+++ b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.css
@@ -0,0 +1,35 @@
+.custom-icon {
+ font-size: 48px;
+ width: 48px;
+ height: 48px;
+ color: white;
+}
+
+/* Animaciones adicionales */
+.modal-overlay-blur {
+ animation: fadeIn 0.3s ease-in-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.spinner-container {
+ animation: slideIn 0.3s ease-out;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateY(-20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.html b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.html
new file mode 100644
index 0000000..4c5975b
--- /dev/null
+++ b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.html
@@ -0,0 +1,7 @@
+
+
+
+
{{ customIcon }}
+
{{ message }}
+
+
\ No newline at end of file
diff --git a/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.ts b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.ts
new file mode 100644
index 0000000..d855799
--- /dev/null
+++ b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.ts
@@ -0,0 +1,14 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'app-modal-overlay',
+ templateUrl: './modal-overlay.component.html',
+ styleUrls: ['./modal-overlay.component.css']
+})
+export class ModalOverlayComponent {
+ @Input() isVisible: boolean = false;
+ @Input() message: string = 'Procesando...';
+ @Input() variant: 'default' | 'success' | 'warning' | 'error' = 'default';
+ @Input() showSpinner: boolean = true;
+ @Input() customIcon?: string;
+}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/shared/queue-confirmation-modal/queue-confirmation-modal.component.css b/ogWebconsole/src/app/shared/queue-confirmation-modal/queue-confirmation-modal.component.css
new file mode 100644
index 0000000..835ba25
--- /dev/null
+++ b/ogWebconsole/src/app/shared/queue-confirmation-modal/queue-confirmation-modal.component.css
@@ -0,0 +1,54 @@
+mat-dialog-content {
+ font-size: 16px;
+ margin-bottom: 20px;
+ color: #555;
+}
+
+.action-container {
+ display: flex;
+ justify-content: flex-end;
+ gap: 1em;
+ padding: 0.5em 1.5em 1.5em 1.5em;
+}
+
+.action-button {
+ background-color: #2196f3;
+ color: white;
+ padding: 8px 18px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 16px;
+ transition: transform 0.3s ease;
+ font-family: Roboto, "Helvetica Neue", sans-serif;
+}
+
+.action-button:hover:not(:disabled) {
+ background-color: #3f51b5;
+}
+
+.action-button:disabled {
+ background-color: #ced0df;
+ cursor: not-allowed;
+}
+
+.ordinary-button {
+ background-color: #f5f5f5;
+ color: #333;
+ padding: 8px 18px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 16px;
+ transition: transform 0.3s ease;
+ font-family: Roboto, "Helvetica Neue", sans-serif;
+}
+
+.ordinary-button:hover:not(:disabled) {
+ background-color: #e0e0e0;
+}
+
+mat-checkbox {
+ margin-top: 15px;
+ display: block;
+}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/shared/queue-confirmation-modal/queue-confirmation-modal.component.html b/ogWebconsole/src/app/shared/queue-confirmation-modal/queue-confirmation-modal.component.html
new file mode 100644
index 0000000..d199d55
--- /dev/null
+++ b/ogWebconsole/src/app/shared/queue-confirmation-modal/queue-confirmation-modal.component.html
@@ -0,0 +1,9 @@
+
Confirmación
+
+
¿Quieres que se encolen las acciones para cuyos PCs no se pueda ejecutar?
+
Encolar acciones
+
+
+ Cancelar
+ Confirmar
+
\ No newline at end of file
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
new file mode 100644
index 0000000..2e6571b
--- /dev/null
+++ b/ogWebconsole/src/app/shared/queue-confirmation-modal/queue-confirmation-modal.component.ts
@@ -0,0 +1,24 @@
+import { Component, Inject } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+
+@Component({
+ selector: 'app-queue-confirmation-modal',
+ templateUrl: './queue-confirmation-modal.component.html',
+ styleUrl: './queue-confirmation-modal.component.css'
+})
+export class QueueConfirmationModalComponent {
+ shouldQueue: boolean = false;
+
+ constructor(
+ public dialogRef: MatDialogRef
,
+ @Inject(MAT_DIALOG_DATA) public data: any
+ ) {}
+
+ onNoClick(): void {
+ this.dialogRef.close(false);
+ }
+
+ onYesClick(): void {
+ this.dialogRef.close(this.shouldQueue);
+ }
+}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/shared/scroll-to-top/scroll-to-top.component.css b/ogWebconsole/src/app/shared/scroll-to-top/scroll-to-top.component.css
new file mode 100644
index 0000000..386b9d9
--- /dev/null
+++ b/ogWebconsole/src/app/shared/scroll-to-top/scroll-to-top.component.css
@@ -0,0 +1,74 @@
+/* Posición por defecto (bottom-right) */
+.scroll-to-top-button-bottom-right {
+ position: fixed;
+ bottom: 30px;
+ right: 30px;
+ z-index: 1000;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.scroll-to-top-button-bottom-left {
+ 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-top-right {
+ position: fixed;
+ top: 30px;
+ right: 30px;
+ z-index: 1000;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.scroll-to-top-button-top-left {
+ position: fixed;
+ top: 30px;
+ left: 30px;
+ z-index: 1000;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+/* Efectos hover para todas las posiciones */
+.scroll-to-top-button-bottom-right:hover,
+.scroll-to-top-button-bottom-left:hover,
+.scroll-to-top-button-top-right:hover,
+.scroll-to-top-button-top-left:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
+}
+
+/* Responsive para dispositivos móviles */
+@media (max-width: 768px) {
+ .scroll-to-top-button-bottom-right,
+ .scroll-to-top-button-bottom-left {
+ bottom: 20px;
+ }
+
+ .scroll-to-top-button-bottom-right {
+ right: 20px;
+ }
+
+ .scroll-to-top-button-bottom-left {
+ left: 20px;
+ }
+
+ .scroll-to-top-button-top-right,
+ .scroll-to-top-button-top-left {
+ top: 20px;
+ }
+
+ .scroll-to-top-button-top-right {
+ right: 20px;
+ }
+
+ .scroll-to-top-button-top-left {
+ left: 20px;
+ }
+}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/shared/scroll-to-top/scroll-to-top.component.html b/ogWebconsole/src/app/shared/scroll-to-top/scroll-to-top.component.html
new file mode 100644
index 0000000..cb505ff
--- /dev/null
+++ b/ogWebconsole/src/app/shared/scroll-to-top/scroll-to-top.component.html
@@ -0,0 +1,9 @@
+
+ keyboard_arrow_up
+
\ No newline at end of file
diff --git a/ogWebconsole/src/app/shared/scroll-to-top/scroll-to-top.component.ts b/ogWebconsole/src/app/shared/scroll-to-top/scroll-to-top.component.ts
new file mode 100644
index 0000000..0daaf99
--- /dev/null
+++ b/ogWebconsole/src/app/shared/scroll-to-top/scroll-to-top.component.ts
@@ -0,0 +1,61 @@
+import { Component, OnInit, OnDestroy, Input } from '@angular/core';
+
+@Component({
+ selector: 'app-scroll-to-top',
+ templateUrl: './scroll-to-top.component.html',
+ styleUrls: ['./scroll-to-top.component.css']
+})
+export class ScrollToTopComponent implements OnInit, OnDestroy {
+ @Input() threshold: number = 300;
+ @Input() targetElement: string = '.header-container';
+ @Input() position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' = 'bottom-right';
+ @Input() showTooltip: boolean = true;
+ @Input() tooltipText: string = 'Volver arriba';
+ @Input() tooltipPosition: 'left' | 'right' | 'above' | 'below' = 'left';
+
+ showScrollToTop: boolean = false;
+ private scrollListener: (() => void) | undefined;
+
+ ngOnInit(): void {
+ this.setupScrollListener();
+ }
+
+ ngOnDestroy(): void {
+ if (this.scrollListener) {
+ window.removeEventListener('scroll', this.scrollListener);
+ }
+ }
+
+ private setupScrollListener(): void {
+ this.scrollListener = () => {
+ this.showScrollToTop = window.scrollY > this.threshold;
+ };
+ window.addEventListener('scroll', this.scrollListener);
+
+ this.scrollListener();
+ }
+
+ scrollToTop(): void {
+ try {
+ const targetElement = document.querySelector(this.targetElement);
+ if (targetElement) {
+ targetElement.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start'
+ });
+ } else {
+ window.scrollTo({
+ top: 0,
+ left: 0,
+ behavior: 'smooth'
+ });
+ }
+ } catch (error) {
+ window.scrollTo(0, 0);
+ }
+ }
+
+ getPositionClass(): string {
+ return `scroll-to-top-button-${this.position}`;
+ }
+}
\ No newline at end of file
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);
+}