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 @@ + + +
+
+

+ {{ 'runScript' | translate }} +

+
+
+ +
+
+ + +
+ + + Clientes + Listado de clientes donde se ejectutará el script + + +
+
+ +
+
+
+ + Client Icon + +
+ {{ client.name }} + {{ client.ip }} + {{ client.mac }} +
+
+
+
+
+
+ + + +
+
+ + Seleccione script a ejecutar + + {{ script.name }} + + +
+
+
+

Script:

+
+
+ +
+

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