diff --git a/ogWebconsole/src/app/app-routing.module.ts b/ogWebconsole/src/app/app-routing.module.ts
index c2e3a06..cbd353a 100644
--- a/ogWebconsole/src/app/app-routing.module.ts
+++ b/ogWebconsole/src/app/app-routing.module.ts
@@ -40,6 +40,9 @@ import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
import {MenusComponent} from "./components/menus/menus.component";
import {OgDhcpSubnetsComponent} from "./components/ogdhcp/og-dhcp-subnets.component";
import {StatusComponent} from "./components/ogdhcp/status/status.component";
+import {
+ RunScriptAssistantComponent
+} from "./components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component";
const routes: Routes = [
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
{ path: '', component: MainLayoutComponent,
@@ -63,6 +66,7 @@ const routes: Routes = [
{ path: 'calendars', component: CalendarComponent },
{ path: 'clients/deploy-image', component: DeployImageComponent },
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent },
+ { path: 'clients/run-script', component: RunScriptAssistantComponent },
{ path: 'clients/:id', component: ClientMainViewComponent },
{ path: 'clients/:id/create-image', component: CreateClientImageComponent },
{ path: 'repositories', component: RepositoriesComponent },
diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts
index 3e5a1e6..8b55fb8 100644
--- a/ogWebconsole/src/app/app.module.ts
+++ b/ogWebconsole/src/app/app.module.ts
@@ -137,6 +137,7 @@ import { GlobalStatusComponent } from './components/global-status/global-status.
import { ShowImagesComponent } from './components/repositories/show-images/show-images.component';
import { StatusTabComponent } from './components/global-status/status-tab/status-tab.component';
import { ConvertImageToVirtualComponent } from './components/repositories/convert-image-to-virtual/convert-image-to-virtual.component';
+import { RunScriptAssistantComponent } from './components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
@@ -233,7 +234,8 @@ registerLocaleData(localeEs, 'es-ES');
GlobalStatusComponent,
ShowImagesComponent,
StatusTabComponent,
- ConvertImageToVirtualComponent
+ ConvertImageToVirtualComponent,
+ RunScriptAssistantComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.css b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.css
new file mode 100644
index 0000000..2ceeeee
--- /dev/null
+++ b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.css
@@ -0,0 +1,214 @@
+
+.divider {
+ margin: 20px 0;
+}
+
+table {
+ width: 100%;
+ margin-top: 50px;
+}
+
+.search-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px;
+ box-sizing: border-box;
+}
+
+.deploy-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px;
+ gap: 10px;
+}
+
+.script-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+ padding: 20px;
+ background-color: #eaeff6;
+ border-radius: 12px;
+ margin-top: 20px;
+}
+
+.script-content {
+ flex: 2;
+ min-width: 60%;
+}
+
+.script-params {
+ flex: 1;
+ min-width: 35%;
+}
+
+@media (max-width: 768px) {
+ .script-container {
+ flex-direction: column;
+ }
+
+ .script-content, .script-params {
+ min-width: 100%;
+ }
+}
+
+.select-container {
+ margin-top: 20px;
+ align-items: center;
+ padding: 20px;
+ box-sizing: border-box;
+}
+
+.input-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ margin-top: 20px;
+}
+
+.input-field {
+ flex: 1 1 calc(33.33% - 16px);
+ min-width: 250px;
+}
+
+.script-preview {
+ background-color: #f4f4f4;
+ border: 1px solid #ccc;
+ padding: 10px;
+ border-radius: 5px;
+ font-family: monospace;
+ white-space: pre-wrap;
+ min-height: 50px;
+}
+
+.full-width {
+ width: 100%;
+ margin-bottom: 16px;
+}
+
+.search-string {
+ flex: 2;
+ padding: 5px;
+}
+
+.search-boolean {
+ flex: 1;
+ padding: 5px;
+}
+
+.header-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 10px;
+ border-bottom: 1px solid #ddd;
+}
+
+.mat-elevation-z8 {
+ box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
+}
+
+.paginator-container {
+ display: flex;
+ justify-content: end;
+ margin-bottom: 30px;
+}
+
+.clients-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: 8px;
+}
+
+.client-item {
+ position: relative;
+}
+
+.client-card {
+ background: #ffffff;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ position: relative;
+ padding: 8px;
+ text-align: center;
+ cursor: pointer;
+ transition: background-color 0.3s, transform 0.2s;
+
+ &:hover {
+ background-color: #f0f0f0;
+ transform: scale(1.02);
+ }
+}
+
+.client-details {
+ margin-top: 4px;
+}
+
+.client-name {
+ display: block;
+ font-size: 1.2em;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 5px;
+}
+
+.client-ip {
+ display: block;
+ font-size: 0.9em;
+ color: #666;
+}
+
+.header-container-title {
+ flex-grow: 1;
+ text-align: left;
+ padding-left: 1em;
+}
+
+.button-row {
+ display: flex;
+ padding-right: 1em;
+}
+
+.client-card {
+ background: #ffffff;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ position: relative;
+ padding: 8px;
+ text-align: center;
+ cursor: pointer;
+ transition: background-color 0.3s, transform 0.2s;
+
+ &:hover {
+ background-color: #f0f0f0;
+ transform: scale(1.02);
+ }
+}
+
+::ng-deep .custom-tooltip {
+ white-space: pre-line !important;
+ max-width: 200px;
+ background: rgba(0, 0, 0, 0.8);
+ color: white;
+ padding: 8px;
+ border-radius: 4px;
+}
+
+.selected-client {
+ background-color: #a0c2e5 !important;
+ color: white !important;
+}
+
+.button-row {
+ display: flex;
+ padding-right: 1em;
+}
+
+.disabled-client {
+ pointer-events: none;
+ opacity: 0.5;
+}
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.html
new file mode 100644
index 0000000..caa81e7
--- /dev/null
+++ b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.html
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+ Clientes
+ Listado de clientes donde se ejectutará el script
+
+
+
+
+
+
+
+
+
+
![Client Icon]()
+
+
+ {{ client.name }}
+ {{ client.ip }}
+ {{ client.mac }}
+
+
+
+
+
+
+
+
+
+
+
+
+ Seleccione script a ejecutar
+
+ {{ script.name }}
+
+
+
+
+
+
+
0">
+
Ingrese los valores de los parámetros:
+
+
+ Parámetro {{ i + 1 }}
+
+
+
+
+
+
+
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
new file mode 100644
index 0000000..18e51ea
--- /dev/null
+++ b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.spec.ts
@@ -0,0 +1,81 @@
+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 {FormBuilder, FormsModule, ReactiveFormsModule} from "@angular/forms";
+import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
+import {MatFormFieldModule} from "@angular/material/form-field";
+import {MatInputModule} from "@angular/material/input";
+import {MatCheckboxModule} from "@angular/material/checkbox";
+import {MatExpansionModule} from "@angular/material/expansion";
+import {MatButtonModule} from "@angular/material/button";
+import {MatTableModule} from "@angular/material/table";
+import {MatDividerModule} from "@angular/material/divider";
+import {MatRadioModule} from "@angular/material/radio";
+import {MatSelectModule} from "@angular/material/select";
+import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
+import {ToastrModule, ToastrService} from "ngx-toastr";
+import {TranslateModule} from "@ngx-translate/core";
+import {provideHttpClient} from "@angular/common/http";
+import {provideHttpClientTesting} from "@angular/common/http/testing";
+import {provideRouter} from "@angular/router";
+import {ConfigService} from "@services/config.service";
+
+describe('RunScriptAssistantComponent', () => {
+ let component: RunScriptAssistantComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ const mockConfigService = {
+ apiUrl: 'http://mock-api-url',
+ mercureUrl: 'http://mock-mercure-url'
+ };
+
+ await TestBed.configureTestingModule({
+ declarations: [DeployImageComponent, LoadingComponent],
+ imports: [
+ ReactiveFormsModule,
+ FormsModule,
+ MatDialogModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatCheckboxModule,
+ MatExpansionModule,
+ MatButtonModule,
+ MatTableModule,
+ MatDividerModule,
+ MatRadioModule,
+ MatSelectModule,
+ BrowserAnimationsModule,
+ ToastrModule.forRoot(),
+ TranslateModule.forRoot()
+ ],
+ providers: [
+ FormBuilder,
+ ToastrService,
+ provideHttpClient(),
+ provideHttpClientTesting(),
+ provideRouter([]),
+ {
+ provide: MatDialogRef,
+ useValue: {}
+ },
+ {
+ provide: MAT_DIALOG_DATA,
+ useValue: {}
+ },
+ { provide: ConfigService, useValue: mockConfigService }
+ ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(RunScriptAssistantComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
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
new file mode 100644
index 0000000..6de3680
--- /dev/null
+++ b/ogWebconsole/src/app/components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component.ts
@@ -0,0 +1,135 @@
+import {Component, EventEmitter, Output} from '@angular/core';
+import {SelectionModel} from "@angular/cdk/collections";
+import {HttpClient} from "@angular/common/http";
+import {ToastrService} from "ngx-toastr";
+import {ConfigService} from "@services/config.service";
+import {ActivatedRoute, Router} from "@angular/router";
+
+@Component({
+ selector: 'app-run-script-assistant',
+ templateUrl: './run-script-assistant.component.html',
+ styleUrl: './run-script-assistant.component.css'
+})
+export class RunScriptAssistantComponent {
+ baseUrl: string;
+ @Output() dataChange = new EventEmitter();
+
+ errorMessage = '';
+ clientId: string | null = null;
+ name: string = '';
+ client: any = null;
+ clientData: any = [];
+ loading: boolean = false;
+ scripts: any[] = [];
+ scriptContent: string = "";
+ parameters: string[] = [];
+ selectedScript: any = null;
+ selectedClients: any[] = [];
+ allSelected: boolean = true;
+ selection = new SelectionModel(true, []);
+
+ constructor(
+ private http: HttpClient,
+ private toastService: ToastrService,
+ private configService: ConfigService,
+ private router: Router,
+ private route: ActivatedRoute
+ ) {
+ this.baseUrl = this.configService.apiUrl;
+ this.route.queryParams.subscribe(params => {
+ if (params['clientData']) {
+ this.clientData = JSON.parse(params['clientData']);
+ }
+ });
+ this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
+ this.clientData.forEach((client: { selected: boolean; status: string}) => {
+ if (client.status === 'og-live') {
+ client.selected = true;
+ }
+ });
+ this.selectedClients = this.clientData.filter(
+ (client: { status: string }) => client.status === 'og-live'
+ );
+ this.loadScripts()
+ }
+
+ loadScripts(): void {
+ this.loading = true;
+
+ this.http.get(`${this.baseUrl}/commands?readOnly=false&enabled=true`).subscribe((data: any) => {
+ this.scripts = data['hydra:member'];
+ this.loading = false;
+ }, (error) => {
+ this.toastService.error(error.error['hydra:description']);
+ this.loading = false;
+ });
+ }
+
+ toggleClientSelection(client: any) {
+ client.selected = !client.selected;
+ this.updateSelectedClients();
+ }
+
+ updateSelectedClients() {
+ this.selectedClients = this.clientData.filter(
+ (client: { selected: boolean; status: string }) => client.selected && client.status === "og-live"
+ );
+ }
+
+ toggleSelectAll() {
+ this.allSelected = !this.allSelected;
+ this.clientData.forEach((client: { selected: boolean; status: string }) => {
+ if (client.status === "og-live") {
+ client.selected = this.allSelected;
+ }
+ });
+ }
+
+ getPartitionsTooltip(client: any): string {
+ if (!client.partitions || client.partitions.length === 0) {
+ return 'No hay particiones disponibles';
+ }
+
+ return client.partitions
+ .map((p: { partitionNumber: any; size: any; filesystem: any }) => `#${p.partitionNumber} ${p.filesystem} - ${p.size / 1024 }GB`)
+ .join('\n');
+ }
+
+ onScriptChange() {
+ if (this.selectedScript) {
+ this.scriptContent = this.selectedScript.script;
+
+ const matches = this.scriptContent.match(/@\d+/g) || [];
+ this.parameters = new Array(matches.length).fill("");
+ }
+ }
+
+ updateScript() {
+ let updatedScript = this.selectedScript.script;
+
+ this.parameters.forEach((value, index) => {
+ updatedScript = updatedScript.replace(new RegExp(`@${index + 1}`, "g"), value || `@${index + 1}`);
+ });
+
+ this.scriptContent = updatedScript;
+ }
+
+ save(): void {
+ this.loading = true;
+
+ this.http.post(`${this.baseUrl}/commands/run-script`, {
+ clients: this.selectedClients.map((client: any) => client.uuid),
+ script: this.scriptContent
+ }).subscribe(
+ response => {
+ this.toastService.success('Script ejecutado correctamente');
+ this.dataChange.emit();
+ },
+ error => {
+ this.toastService.error('Error al ejecutar el script');
+ }
+ );
+
+ this.loading = false;
+ }
+}
diff --git a/ogWebconsole/src/assets/images/computer_busy.svg b/ogWebconsole/src/assets/images/computer_busy.svg
new file mode 100644
index 0000000..7e76860
--- /dev/null
+++ b/ogWebconsole/src/assets/images/computer_busy.svg
@@ -0,0 +1,3 @@
+
diff --git a/ogWebconsole/src/assets/images/computer_initializing.svg b/ogWebconsole/src/assets/images/computer_initializing.svg
new file mode 100644
index 0000000..f0e2e65
--- /dev/null
+++ b/ogWebconsole/src/assets/images/computer_initializing.svg
@@ -0,0 +1,3 @@
+
diff --git a/ogWebconsole/src/assets/images/computer_linux-session.svg b/ogWebconsole/src/assets/images/computer_linux-session.svg
new file mode 100644
index 0000000..ce9750c
--- /dev/null
+++ b/ogWebconsole/src/assets/images/computer_linux-session.svg
@@ -0,0 +1,5 @@
+
diff --git a/ogWebconsole/src/assets/images/computer_linux.svg b/ogWebconsole/src/assets/images/computer_linux.svg
new file mode 100644
index 0000000..0374358
--- /dev/null
+++ b/ogWebconsole/src/assets/images/computer_linux.svg
@@ -0,0 +1,3 @@
+
diff --git a/ogWebconsole/src/assets/images/computer_macos.svg b/ogWebconsole/src/assets/images/computer_macos.svg
new file mode 100644
index 0000000..b647af6
--- /dev/null
+++ b/ogWebconsole/src/assets/images/computer_macos.svg
@@ -0,0 +1,3 @@
+
diff --git a/ogWebconsole/src/assets/images/computer_off.svg b/ogWebconsole/src/assets/images/computer_off.svg
new file mode 100644
index 0000000..f23c580
--- /dev/null
+++ b/ogWebconsole/src/assets/images/computer_off.svg
@@ -0,0 +1,2 @@
+
diff --git a/ogWebconsole/src/assets/images/computer_og-live.svg b/ogWebconsole/src/assets/images/computer_og-live.svg
new file mode 100644
index 0000000..4e5b75e
--- /dev/null
+++ b/ogWebconsole/src/assets/images/computer_og-live.svg
@@ -0,0 +1,3 @@
+
diff --git a/ogWebconsole/src/assets/images/computer_turning-off.svg b/ogWebconsole/src/assets/images/computer_turning-off.svg
new file mode 100644
index 0000000..f23c580
--- /dev/null
+++ b/ogWebconsole/src/assets/images/computer_turning-off.svg
@@ -0,0 +1,2 @@
+
diff --git a/ogWebconsole/src/assets/images/computer_windows-session.svg b/ogWebconsole/src/assets/images/computer_windows-session.svg
new file mode 100644
index 0000000..dc9b531
--- /dev/null
+++ b/ogWebconsole/src/assets/images/computer_windows-session.svg
@@ -0,0 +1,5 @@
+
diff --git a/ogWebconsole/src/assets/images/computer_windows.svg b/ogWebconsole/src/assets/images/computer_windows.svg
new file mode 100644
index 0000000..8333a89
--- /dev/null
+++ b/ogWebconsole/src/assets/images/computer_windows.svg
@@ -0,0 +1,3 @@
+
diff --git a/ogWebconsole/src/assets/images/ordernador_og-live.png b/ogWebconsole/src/assets/images/ordernador_og-live.png
new file mode 100644
index 0000000..3bc1ae1
Binary files /dev/null and b/ogWebconsole/src/assets/images/ordernador_og-live.png differ