refs #2756. Fixed bug with stored comands

pull/41/head
Manuel Aranda Rosales 2025-09-05 12:12:11 +02:00
parent 4e22286e04
commit 2d9c66919a
3 changed files with 439 additions and 31 deletions

View File

@ -533,4 +533,274 @@ mat-spinner {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.script-input-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.script-input-header h4 {
margin: 0;
display: flex;
align-items: center;
gap: 8px;
color: #2c3e50;
font-weight: 600;
}
.script-stats {
display: flex;
gap: 16px;
align-items: center;
}
.stat-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #6c757d;
background: white;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #e9ecef;
}
.stat-item.valid {
color: #28a745;
border-color: #28a745;
background: #f8fff9;
}
.stat-item.invalid {
color: #dc3545;
border-color: #dc3545;
background: #fff8f8;
}
.stat-item mat-icon {
font-size: 14px;
width: 14px;
height: 14px;
}
.script-textarea {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.5;
background: #f8f9fa;
}
.detected-params {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
}
.detected-params h4 {
margin: 0 0 12px 0;
display: flex;
align-items: center;
gap: 8px;
color: #856404;
font-weight: 600;
}
.params-grid .mat-mdc-chip-listbox {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.param-chip {
background: #fff !important;
border: 1px solid #ffeaa7 !important;
color: #856404 !important;
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 16px;
}
.action-buttons button {
display: flex;
align-items: center;
gap: 8px;
}
.script-selector-container {
margin-bottom: 20px;
}
.script-option {
display: flex;
flex-direction: column;
gap: 2px;
}
.script-name {
font-weight: 500;
color: #2c3e50;
}
.script-description {
font-size: 12px;
color: #6c757d;
opacity: 0.8;
}
/* Tarjetas de vista previa */
.script-preview-container {
margin-top: 20px;
}
.script-card, .params-card {
margin-bottom: 16px;
border: 1px solid #e3f2fd !important;
border-radius: 12px !important;
}
.script-card .mat-mdc-card-header,
.params-card .mat-mdc-card-header {
background: #f8f9fa;
border-radius: 12px 12px 0 0;
}
.script-card .mat-mdc-card-title,
.params-card .mat-mdc-card-title {
display: flex;
align-items: center;
gap: 8px;
color: #2c3e50;
font-weight: 600;
}
.script-card .mat-mdc-card-subtitle,
.params-card .mat-mdc-card-subtitle {
color: #6c757d;
margin-top: 4px;
}
.script-content-wrapper {
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
max-height: 300px;
overflow-y: auto;
}
.script-preview {
background-color: #ffffff;
border: 1px solid #e9ecef;
padding: 16px;
border-radius: 8px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
min-height: 60px;
max-height: 280px;
overflow-y: auto;
}
.script-params-section {
margin-top: 16px;
}
.params-count {
background: #667eea !important;
color: white !important;
font-size: 11px;
height: 20px;
min-height: 20px;
padding: 0 8px;
}
.params-form {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.param-field {
position: relative;
}
/* Estados vacíos */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
background: #f8f9fa;
border: 2px dashed #e9ecef;
border-radius: 12px;
margin: 20px 0;
}
.empty-state-content {
text-align: center;
color: #6c757d;
}
.empty-state-content mat-icon {
font-size: 48px;
width: 48px;
height: 48px;
color: #dee2e6;
margin-bottom: 16px;
}
.empty-state-content h3 {
margin: 0 0 8px 0;
color: #495057;
font-weight: 500;
}
.empty-state-content p {
margin: 0;
font-size: 14px;
max-width: 300px;
}
/* Mejoras en chips de acción */
::ng-deep .action-chip mat-icon {
font-size: 18px !important;
width: 18px !important;
height: 18px !important;
margin-right: 4px !important;
}
/* Responsive design */
@media (max-width: 768px) {
.script-input-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.script-stats {
flex-wrap: wrap;
gap: 8px;
}
.params-form {
grid-template-columns: 1fr;
}
.action-buttons {
justify-content: center;
}
}

View File

@ -86,50 +86,168 @@
<div class="action-chips-container">
<mat-chip-listbox [(ngModel)]="commandType" required class="action-chip-listbox">
<mat-chip-option value="new" class="action-chip create-chip firmware-chip" (click)="commandType = 'new'">
<mat-icon>add_circle</mat-icon>
<span>Nuevo Script</span>
</mat-chip-option>
<mat-chip-option value="existing" class="action-chip update-chip firmware-chip" (click)="commandType = 'existing'">
<mat-icon>folder</mat-icon>
<span>Script Guardado</span>
</mat-chip-option>
</mat-chip-listbox>
</div>
<!-- Nuevo Script -->
<div *ngIf="commandType === 'new'" class="new-command-container">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Ingrese el script</mat-label>
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
</mat-form-field>
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
</div>
<div *ngIf="commandType === 'existing'">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Seleccione script a ejecutar</mat-label>
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
<div class="script-content">
<h3>Script:</h3>
<div class="script-preview" [innerHTML]="scriptContent"></div>
<div class="script-input-header">
<h4>
<mat-icon>edit_note</mat-icon>
Editor de Script
</h4>
<div class="script-stats" *ngIf="newScript.trim()">
<span class="stat-item">
<mat-icon>format_list_numbered</mat-icon>
{{ getLineCount(newScript) }} líneas
</span>
<span class="stat-item">
<mat-icon>text_fields</mat-icon>
{{ newScript.length }} caracteres
</span>
<span class="stat-item" [ngClass]="{'valid': isScriptValid(newScript), 'invalid': !isScriptValid(newScript)}">
<mat-icon>{{ isScriptValid(newScript) ? 'check_circle' : 'error' }}</mat-icon>
{{ isScriptValid(newScript) ? 'Válido' : 'Requiere contenido' }}
</span>
</div>
</div>
<div class="script-params" *ngIf="parameterNames.length > 0 && selectedScript.parameters">
<h3>Ingrese los parámetros:</h3>
<div *ngFor="let paramName of parameterNames">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ paramName }}</mat-label>
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Ingrese el script</mat-label>
<textarea
matInput
[(ngModel)]="newScript"
(ngModelChange)="onNewScriptChange()"
rows="8"
placeholder="#!/bin/bash&#10;# Escriba su script aquí&#10;echo 'Iniciando script...'&#10;&#10;# Sus comandos aquí"
class="script-textarea"></textarea>
<mat-hint>Escriba comandos de shell/bash. Use &#64;parametro para variables dinámicas.</mat-hint>
</mat-form-field>
<!-- Vista previa de parámetros detectados en nuevo script -->
<div *ngIf="newScriptParameters.length > 0" class="detected-params">
<h4>
<mat-icon>tune</mat-icon>
Parámetros detectados
</h4>
<div class="params-grid">
<mat-chip-listbox>
<mat-chip-option *ngFor="let param of newScriptParameters" class="param-chip" selected>
<mat-icon>label</mat-icon>
&#64;{{ param }}
</mat-chip-option>
</mat-chip-listbox>
</div>
</div>
<div class="action-buttons">
<button
mat-flat-button
color="primary"
[disabled]="!isScriptValid(newScript)"
(click)="saveNewScript()">
<mat-icon>save</mat-icon>
Guardar Script
</button>
</div>
</div>
<!-- Script Existente -->
<div *ngIf="commandType === 'existing'">
<div class="script-selector-container">
<mat-form-field appearance="outline" class="full-width">
<mat-label>Seleccione script a ejecutar</mat-label>
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
<mat-option *ngFor="let script of scripts" [value]="script">
<div class="script-option">
<span class="script-name">{{ script.name }}</span>
<span class="script-description" *ngIf="script.description">{{ script.description }}</span>
</div>
</mat-option>
</mat-select>
<mat-hint *ngIf="scripts.length === 0">No hay scripts guardados disponibles</mat-hint>
<mat-hint *ngIf="scripts.length > 0">{{ scripts.length }} script(s) disponible(s)</mat-hint>
</mat-form-field>
</div>
<!-- Vista previa del script seleccionado -->
<div *ngIf="selectedScript && commandType === 'existing'" class="script-preview-container">
<mat-card class="script-card">
<mat-card-header>
<mat-card-title>
<mat-icon>preview</mat-icon>
Vista previa del script
</mat-card-title>
<mat-card-subtitle>{{ selectedScript.name }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="script-content-wrapper">
<div class="script-preview" [innerHTML]="scriptContent"></div>
</div>
</mat-card-content>
</mat-card>
<!-- Parámetros del script -->
<div *ngIf="parameterNames.length > 0 && selectedScript.parameters" class="script-params-section">
<mat-card class="params-card">
<mat-card-header>
<mat-card-title>
<mat-icon>settings</mat-icon>
Parámetros del script
<mat-chip class="params-count">{{ parameterNames.length }}</mat-chip>
</mat-card-title>
<mat-card-subtitle>Configure los valores para los parámetros requeridos</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="params-form">
<div *ngFor="let paramName of parameterNames; trackBy: trackByParam" class="param-field">
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ paramName }}</mat-label>
<input
matInput
[ngModel]="parameters[paramName]"
(ngModelChange)="onParamChange(paramName, $event)"
[placeholder]="'Valor para ' + paramName"
[required]="true">
<mat-icon matPrefix>label</mat-icon>
<mat-hint>Parámetro: &#64;{{ paramName }}</mat-hint>
<mat-error *ngIf="!parameters[paramName]">Este parámetro es requerido</mat-error>
</mat-form-field>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
</div>
<!-- Estado vacío para script existente -->
<div *ngIf="commandType === 'existing' && !selectedScript" class="empty-state">
<div class="empty-state-content">
<mat-icon>folder_open</mat-icon>
<h3>Selecciona un script</h3>
<p>Elige un script guardado de la lista para ver su contenido y configurar parámetros.</p>
</div>
</div>
<!-- Estado vacío para nuevo script -->
<div *ngIf="commandType === 'new' && !newScript.trim()" class="empty-state">
<div class="empty-state-content">
<mat-icon>edit_note</mat-icon>
<h3>Escribe tu script</h3>
<p>Crea un nuevo script escribiendo comandos de shell/bash en el editor de texto.</p>
</div>
</div>
</div>
</div>

View File

@ -35,6 +35,7 @@ export class RunScriptAssistantComponent implements OnInit{
selection = new SelectionModel(true, []);
parameterNames: string[] = Object.keys(this.parameters);
runScriptContext: any = null;
newScriptParameters: string[] = [];
constructor(
private http: HttpClient,
@ -168,6 +169,24 @@ export class RunScriptAssistantComponent implements OnInit{
this.scriptContent = updatedScript;
}
getLineCount(text: string): number {
if (!text) return 0;
return text.split('\n').length;
}
isScriptValid(script: string): boolean {
return Boolean(script && script.trim().length > 0);
}
onNewScriptChange(): void {
const matches = this.newScript.match(/@(\w+)/g) || [];
this.newScriptParameters = Array.from(new Set(matches.map(m => m.slice(1))));
}
trackByParam(index: number, paramName: string): string {
return paramName;
}
save(): void {
const dialogRef = this.dialog.open(QueueConfirmationModalComponent, {
width: '400px',
@ -228,11 +247,12 @@ export class RunScriptAssistantComponent implements OnInit{
});
dialogRef.afterClosed().subscribe(result => {
console.log(result);
if (result) {
this.http.post(`${this.baseUrl}/command-task-scripts`, {
commandTask: result.taskId['@id'],
commandTask: result['@id'],
content: this.commandType === 'existing' ? this.scriptContent : this.newScript,
order: result.executionOrder,
order: result['executionOrder'] || 1,
type: 'run-script',
}).subscribe({
next: () => {