feature-run-as-admin #57
|
|
@ -7,3 +7,5 @@ debian/*.substvars
|
|||
debian/*.log
|
||||
debian/.debhelper/
|
||||
debian/files
|
||||
.venv/
|
||||
.vscode/
|
||||
|
|
|
|||
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -1,4 +1,27 @@
|
|||
# Changelog
|
||||
## [1.2.5] - 2026-03-27
|
||||
### Fixed
|
||||
- Corregido error 401 Unauthorized en la edición de menús con nombres que contienen la palabra "login". Se ha refinado el interceptor para aplicar el prefijo "Bearer" de forma selectiva mediante validación exacta del path `/auth/login`.
|
||||
|
||||
## [1.2.4] - 2026-03-20
|
||||
### Added
|
||||
- Se ha mejorado el editor de menus para poder editar directamente la plantilla twig en lugar del html renderizado.
|
||||
|
||||
## [1.2.3] - 2026-03-17
|
||||
### Added
|
||||
- Se ha creado una nueva opción para gestionar assets en la pantalla de menus.
|
||||
|
||||
## [1.2.2] - 2026-03-17
|
||||
### Added
|
||||
- Se ha integrado un editor interactivo en texto plano para los menús HTML directamente en la pantalla de edición, permitiendo previsualizar y modificar el código en vivo sin necesidad de resubir archivos. Utiliza la sintaxis moderna `@if/@else` de Angular 17+ para su lógica de renderizado.
|
||||
|
||||
## [1.2.1] - 2026-03-11
|
||||
### Added
|
||||
- Se ha añadido la funcionalidad para poder adjuntar un fichero HTML local en la creación/edición de menús, codificándolo y enviándolo en Base64 de forma transparente.
|
||||
|
||||
### Improved
|
||||
- Se ha mejorado la UX integrando el campo de subida de ficheros en el formulario de menús (`create-menu`) utilizando la estética nativa de Angular Material.
|
||||
|
||||
## [1.2.0] - 2025-10-21
|
||||
### Added
|
||||
- Se ha añadido una nueva funcionalidad la cual permite poder borrar varios equipos a la vez.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ db_get opengnsys/oggui_ogcoreUrl
|
|||
OGCORE_URL="$RET"
|
||||
db_get opengnsys/oggui_ogmercureUrl
|
||||
OGMERCURE_URL="$RET"
|
||||
db_get opengnsys/oggui_oglogUrl
|
||||
OGLOG_URL="$RET"
|
||||
|
||||
# Asegurarse de que el usuario exista
|
||||
USER="opengnsys"
|
||||
|
|
@ -35,7 +37,7 @@ restore_config_if_modified() {
|
|||
|
||||
# Detectar si es una instalación nueva o una actualización
|
||||
if [ "$1" = "configure" ] && [ -z "$2" ]; then
|
||||
jq --arg apiUrl "$OGCORE_URL" --arg mercureUrl "$OGMERCURE_URL" '.apiUrl = $apiUrl | .mercureUrl = $mercureUrl' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
|
||||
jq --arg apiUrl "$OGCORE_URL" --arg mercureUrl "$OGMERCURE_URL" --arg logUrl "$OGLOG_URL" '.apiUrl = $apiUrl | .mercureUrl = $mercureUrl | .logUrl = $logUrl' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
|
||||
ln -s /opt/opengnsys/oggui/etc/nginx/oggui.conf /etc/nginx/sites-enabled/oggui.conf
|
||||
ln -s $CONFIG_FILE /opt/opengnsys/oggui/etc/config.json
|
||||
mkdir -p /etc/nginx/certs/
|
||||
|
|
|
|||
|
|
@ -7,3 +7,8 @@ Template: opengnsys/oggui_ogmercureUrl
|
|||
Type: string
|
||||
Default: https://127.0.0.1:3000/.well-known/mercure
|
||||
Description: Introduzca el endpoint de mercure
|
||||
|
||||
Template: opengnsys/oggui_oglogUrl
|
||||
Type: string
|
||||
Default: https://127.0.0.1:3030/
|
||||
Description: Oglog Grafana URL
|
||||
|
|
@ -1,4 +1,20 @@
|
|||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, LOCALE_ID, APP_INITIALIZER } from '@angular/core';
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, LOCALE_ID, APP_INITIALIZER, Injectable } from '@angular/core';
|
||||
import { MatPaginatorIntl } from '@angular/material/paginator';
|
||||
|
||||
@Injectable()
|
||||
class SpanishPaginatorIntl extends MatPaginatorIntl {
|
||||
override itemsPerPageLabel = 'Elementos por página:';
|
||||
override nextPageLabel = 'Siguiente';
|
||||
override previousPageLabel = 'Anterior';
|
||||
override firstPageLabel = 'Primera página';
|
||||
override lastPageLabel = 'Última página';
|
||||
override getRangeLabel = (page: number, pageSize: number, length: number) => {
|
||||
if (length === 0 || pageSize === 0) return `0 de ${length}`;
|
||||
const startIndex = page * pageSize;
|
||||
const endIndex = Math.min(startIndex + pageSize, length);
|
||||
return `${startIndex + 1} – ${endIndex} de ${length}`;
|
||||
};
|
||||
}
|
||||
import { ConfigService } from './services/config.service';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
|
|
@ -106,6 +122,7 @@ import { RepositoriesComponent } from './components/repositories/repositories.co
|
|||
import { ManageRepositoryComponent } from './components/repositories/manage-repository/manage-repository.component';
|
||||
import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component';
|
||||
import { DeployImageComponent } from './components/groups/components/client-main-view/deploy-image/deploy-image.component';
|
||||
import { DeployMethodConfigDialogComponent } from './components/groups/components/client-main-view/deploy-image/deploy-method-config-dialog/deploy-method-config-dialog.component';
|
||||
import { MainRepositoryViewComponent } from './components/repositories/main-repository-view/main-repository-view.component';
|
||||
import { ExecuteCommandOuComponent } from './components/groups/shared/execute-command-ou/execute-command-ou.component';
|
||||
import { JoyrideModule } from 'ngx-joyride';
|
||||
|
|
@ -114,6 +131,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
|||
import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MenusComponent } from './components/menus/menus.component';
|
||||
import { AssetManagerModalComponent } from './components/menus/asset-manager-modal/asset-manager-modal.component';
|
||||
import { CreateMenuComponent } from './components/menus/create-menu/create-menu.component';
|
||||
import { CreateMultipleClientComponent } from './components/groups/shared/clients/create-multiple-client/create-multiple-client.component';
|
||||
import { ExportImageComponent } from './components/images/export-image/export-image.component';
|
||||
|
|
@ -171,7 +189,6 @@ import { HardwareProfileComponent } from './components/hardware-profile/hardware
|
|||
import { HardwareCollectionModalComponent } from './components/hardware-profile/hardware-collection-modal/hardware-collection-modal.component';
|
||||
import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component';
|
||||
import { ShowDetailsComponent } from './components/hardware-type/show-details/show-details.component';
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
}
|
||||
|
|
@ -249,6 +266,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
ExecuteCommandComponent,
|
||||
ExecuteCommandOuComponent,
|
||||
DeployImageComponent,
|
||||
DeployMethodConfigDialogComponent,
|
||||
MainRepositoryViewComponent,
|
||||
EnvVarsComponent,
|
||||
MenusComponent,
|
||||
|
|
@ -298,7 +316,8 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
HardwareProfileComponent,
|
||||
HardwareCollectionModalComponent,
|
||||
HardwareTypeComponent,
|
||||
ShowDetailsComponent
|
||||
ShowDetailsComponent,
|
||||
AssetManagerModalComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
|
@ -359,6 +378,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
multi: true
|
||||
},
|
||||
{ provide: LOCALE_ID, useValue: 'es-ES' },
|
||||
{ provide: MatPaginatorIntl, useClass: SpanishPaginatorIntl },
|
||||
provideAnimationsAsync(),
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
ConfigService,
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ table {
|
|||
}
|
||||
|
||||
/* Estilos para los botones de gestión */
|
||||
.schedule-btn, .script-btn {
|
||||
.schedule-btn, .script-btn, .traces-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -114,7 +114,17 @@ table {
|
|||
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.schedule-btn mat-icon, .script-btn mat-icon {
|
||||
.traces-btn {
|
||||
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.traces-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3);
|
||||
}
|
||||
|
||||
.schedule-btn mat-icon, .script-btn mat-icon, .traces-btn mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
|
@ -123,15 +133,58 @@ table {
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.schedule-btn span, .script-btn span {
|
||||
.schedule-btn span, .script-btn span, .traces-btn span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Indicadores de estado de trazas */
|
||||
.trace-stats {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trace-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
padding: 0 6px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.trace-success {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
.trace-in-progress {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
.trace-pending {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
.trace-failed {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.trace-no-data {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Responsive para los botones */
|
||||
@media (max-width: 768px) {
|
||||
.schedule-btn, .script-btn {
|
||||
.schedule-btn, .script-btn, .traces-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin: 4px 0;
|
||||
|
|
|
|||
|
|
@ -46,11 +46,11 @@
|
|||
<ng-container *ngIf="column.columnDef === 'management'">
|
||||
<button class="action-button schedule-btn" (click)="openShowScheduleDialog(task)">
|
||||
<mat-icon>schedule</mat-icon>
|
||||
<span>Programaciones</span>
|
||||
<span>{{ 'schedules' | translate }}</span>
|
||||
</button>
|
||||
<button class="action-button script-btn" (click)="openShowScriptDialog(task)">
|
||||
<mat-icon>code</mat-icon>
|
||||
<span>Acciones</span>
|
||||
<span>{{ 'taskActions' | translate }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export class CommandsTaskComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
updateDisplayedColumns(): void {
|
||||
const hasNonClientTasks = this.tasks.some(task => task.scope !== 'clients');
|
||||
|
||||
|
|
@ -127,7 +128,7 @@ export class CommandsTaskComponent implements OnInit {
|
|||
deleteTask(task: any): void {
|
||||
this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: task.createdBy },
|
||||
data: { name: task.name },
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.http.delete(`${this.apiUrl}/${task.uuid}`).subscribe({
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
<h2 mat-dialog-title class="dialog-title">
|
||||
{{ data.schedule ? 'Editar' : 'Crear' }} Programación de Tarea
|
||||
{{ (data.schedule ? 'editSchedule' : 'createSchedule') | translate }} {{ 'taskSchedule' | translate }}
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="task-form">
|
||||
|
||||
<mat-form-field appearance="fill" class="w-full">
|
||||
<mat-label>Repetición</mat-label>
|
||||
<mat-label>{{ 'repetition' | translate }}</mat-label>
|
||||
<mat-select formControlName="recurrenceType">
|
||||
<mat-option *ngFor="let opt of recurrenceTypes" [value]="opt.value">{{ opt.label }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="w-full" *ngIf="form.get('recurrenceType')?.value === 'none'">
|
||||
<mat-label>Fecha de ejecución</mat-label>
|
||||
<mat-label>{{ 'executionDate' | translate }}</mat-label>
|
||||
<input matInput [matDatepicker]="oneTimePicker" formControlName="executionDate">
|
||||
<mat-datepicker-toggle matSuffix [for]="oneTimePicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #oneTimePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="w-full">
|
||||
<mat-label>Hora de ejecución</mat-label>
|
||||
<mat-label>{{ 'executionTime' | translate }}</mat-label>
|
||||
<input matInput formControlName="executionTime" placeholder="08:00" type="time">
|
||||
<mat-hint>Hora en formato 24h (ej: 14:30)</mat-hint>
|
||||
<mat-hint>{{ 'timeFormatHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
|
||||
<label class="section-label">Días de la semana:</label>
|
||||
<label class="section-label">{{ 'weekDaysLabel' | translate }}:</label>
|
||||
<div class="weekday-toggle-group">
|
||||
<button
|
||||
*ngFor="let day of weekDays"
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
</div>
|
||||
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
|
||||
<label class="section-label">Meses:</label>
|
||||
<label class="section-label">{{ 'monthsLabel' | translate }}:</label>
|
||||
<div class="month-toggle-row" *ngFor="let row of monthRows">
|
||||
<button
|
||||
*ngFor="let month of row"
|
||||
|
|
@ -55,14 +55,14 @@
|
|||
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails">
|
||||
<mat-form-field appearance="fill" class="w-half">
|
||||
<mat-label>Fecha de inicio</mat-label>
|
||||
<mat-label>{{ 'startDate' | translate }}</mat-label>
|
||||
<input matInput [matDatepicker]="fromPicker" formControlName="initDate">
|
||||
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #fromPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="w-half">
|
||||
<mat-label>Fecha de fin</mat-label>
|
||||
<mat-label>{{ 'endDate' | translate }}</mat-label>
|
||||
<input matInput [matDatepicker]="toPicker" formControlName="endDate">
|
||||
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #toPicker></mat-datepicker>
|
||||
|
|
@ -71,15 +71,14 @@
|
|||
|
||||
<mat-checkbox formControlName="enabled" class="enabled-checkbox">
|
||||
<mat-icon class="checkbox-icon">power_settings_new</mat-icon>
|
||||
Activar programación
|
||||
{{ 'enableSchedule' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<!-- Resumen mejorado de la programación -->
|
||||
<mat-card *ngIf="summaryText" class="summary-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="summary-title">
|
||||
<mat-icon color="primary">info</mat-icon>
|
||||
Resumen de la Programación
|
||||
{{ 'scheduleSummaryTitle' | translate }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
|
|
@ -90,11 +89,11 @@
|
|||
</div>
|
||||
<div class="summary-item" *ngIf="nextExecutionDate">
|
||||
<mat-icon class="summary-icon">next_plan</mat-icon>
|
||||
<span class="summary-text">Próxima ejecución: {{ nextExecutionDate | date:'full' }}</span>
|
||||
<span class="summary-text">{{ 'nextExecution' | translate }}: {{ nextExecutionDate | date:'full' }}</span>
|
||||
</div>
|
||||
<div class="summary-item" *ngIf="executionCount">
|
||||
<mat-icon class="summary-icon">repeat</mat-icon>
|
||||
<span class="summary-text">Se ejecutará {{ executionCount }} veces</span>
|
||||
<span class="summary-text">{{ 'willExecuteTimes' | translate:{ count: executionCount } }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
|
|
|
|||
|
|
@ -168,6 +168,34 @@ export class CreateTaskScheduleComponent implements OnInit{
|
|||
onSubmit() {
|
||||
const formData = this.form.value;
|
||||
|
||||
if (formData.recurrenceType === 'custom') {
|
||||
const selectedDayNames = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
|
||||
const selectedMonthNames = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
|
||||
|
||||
if (selectedDayNames.length === 0) {
|
||||
this.toastr.error('Debe seleccionar al menos un día de la semana');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedMonthNames.length === 0) {
|
||||
this.toastr.error('Debe seleccionar al menos un mes');
|
||||
return;
|
||||
}
|
||||
|
||||
const initDate = formData.recurrenceDetails?.initDate;
|
||||
const endDate = formData.recurrenceDetails?.endDate;
|
||||
|
||||
if (!initDate || !endDate) {
|
||||
this.toastr.error('Debe especificar fecha de inicio y fecha de fin');
|
||||
return;
|
||||
}
|
||||
|
||||
if (new Date(endDate) <= new Date(initDate)) {
|
||||
this.toastr.error('La fecha de fin debe ser posterior a la fecha de inicio');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
commandTask: this.data.task['@id'],
|
||||
executionDate: formData.recurrenceType === 'none' ? this.convertDateToLocalISO(formData.executionDate) : null,
|
||||
|
|
|
|||
|
|
@ -1,31 +1,312 @@
|
|||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
.dialog-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
table {
|
||||
.back-icon {
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.back-icon:hover {
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.title-with-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ===== Category cards ===== */
|
||||
.categories-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 18px 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-left: 5px solid;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.category-card:hover {
|
||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-icon mat-icon {
|
||||
color: #fff;
|
||||
font-size: 26px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.category-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.category-info h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.category-info p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.category-arrow {
|
||||
color: #bbb;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.category-card:hover .category-arrow {
|
||||
transform: translateX(4px);
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
color: #90a4ae;
|
||||
}
|
||||
|
||||
/* ===== Info panel (for assistant-required categories) ===== */
|
||||
.info-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-panel-icon .big-info-icon {
|
||||
font-size: 56px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.info-panel-content {
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.info-panel-content h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.info-panel-content > p {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.info-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
text-align: left;
|
||||
margin-bottom: 28px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.info-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-size: 14px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.step-number-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-supported-ops {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info-supported-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
margin: 0 0 10px 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.info-op-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
margin: 4px 6px 4px 0;
|
||||
background: #f0f4f8;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.info-op-chip mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* ===== Action selector within category ===== */
|
||||
.actions-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.actions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-left: 4px solid;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.action-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.action-card mat-icon:first-child {
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.action-card span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ===== Form sections ===== */
|
||||
.action-form {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.order-field {
|
||||
width: 200px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.task-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
.radio-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
padding: 12px 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.deploy-container {
|
||||
/* ===== System action info ===== */
|
||||
.system-action-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
padding: 32px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.system-action-info .big-icon {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.system-action-info p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
/* ===== Script section ===== */
|
||||
.script-type-selector {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.new-command-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
background-color: #eaeff6;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.new-command-container textarea {
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.script-container {
|
||||
|
|
@ -33,7 +314,7 @@ table {
|
|||
padding: 20px;
|
||||
background-color: #eaeff6;
|
||||
border-radius: 12px;
|
||||
margin-top: 20px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.script-content {
|
||||
|
|
@ -46,35 +327,6 @@ table {
|
|||
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;
|
||||
|
|
@ -85,145 +337,7 @@ table {
|
|||
min-height: 50px;
|
||||
}
|
||||
|
||||
.custom-width {
|
||||
width: 50%;
|
||||
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 {
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* ===== Action buttons ===== */
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
@ -231,178 +345,14 @@ table {
|
|||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.new-command-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
background-color: #eaeff6;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.new-command-container mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.new-command-container textarea {
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.new-command-container .action-button {
|
||||
align-self: flex-end;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.script-selector-card {
|
||||
margin: 20px 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* Estilos para las pestañas principales */
|
||||
.action-tabs {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.action-tabs ::ng-deep .mat-tab-header {
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.action-tabs ::ng-deep .mat-tab-label {
|
||||
min-width: 160px;
|
||||
padding: 12px 24px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-tabs ::ng-deep .mat-tab-label:hover {
|
||||
color: #1976d2;
|
||||
background-color: rgba(25, 118, 210, 0.04);
|
||||
}
|
||||
|
||||
.action-tabs ::ng-deep .mat-tab-label.mat-tab-label-active {
|
||||
color: #1976d2;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-tabs ::ng-deep .mat-ink-bar {
|
||||
background-color: #1976d2;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
/* Contenido de las pestañas */
|
||||
.tab-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.action-description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #1976d2;
|
||||
}
|
||||
|
||||
.action-description mat-icon {
|
||||
color: #1976d2;
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.action-description span {
|
||||
color: #495057;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Selector de tipo de script */
|
||||
.script-type-selector {
|
||||
margin-bottom: 24px;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.radio-button:hover {
|
||||
background-color: rgba(25, 118, 210, 0.04);
|
||||
}
|
||||
|
||||
.radio-button mat-icon {
|
||||
color: #1976d2;
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Contenedor de acciones básicas */
|
||||
.basic-action-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Contenedor de scripts */
|
||||
.script-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
/* ===== Responsive ===== */
|
||||
@media (max-width: 768px) {
|
||||
.action-tabs ::ng-deep .mat-tab-label {
|
||||
min-width: 120px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
.radio-row {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-description {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
|
||||
.info-steps {
|
||||
padding: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,118 +1,169 @@
|
|||
<h2 mat-dialog-title class="dialog-title">Añadir acción a: {{ data.task?.name }}</h2>
|
||||
<h2 mat-dialog-title class="dialog-title">
|
||||
<mat-icon *ngIf="selectedCategory" class="back-icon" (click)="goBackToCategories()">arrow_back</mat-icon>
|
||||
<span *ngIf="!selectedCategory">{{ 'addActionTo' | translate }}: {{ data.commandTask?.name }}</span>
|
||||
<span *ngIf="selectedCategory && !selectedAction && isInfoOnlyCategory()" class="title-with-action">
|
||||
<mat-icon [style.color]="getSelectedCategoryColor()">{{ categories | keyvalue : undefined }}</mat-icon>
|
||||
<ng-container *ngFor="let cat of categories">
|
||||
<ng-container *ngIf="cat.id === selectedCategory">
|
||||
<mat-icon [style.color]="cat.color">{{ cat.icon }}</mat-icon>
|
||||
{{ cat.labelKey | translate }}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</span>
|
||||
<span *ngIf="selectedAction" class="title-with-action">
|
||||
<mat-icon [style.color]="getSelectedCategoryColor()">{{ getSelectedActionIcon() }}</mat-icon>
|
||||
{{ getSelectedActionLabel() }}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<div class="task-form">
|
||||
|
||||
<!-- Pestañas principales -->
|
||||
<mat-tab-group [(selectedIndex)]="selectedTabIndex" (selectedIndexChange)="onTabChange($event)" class="action-tabs">
|
||||
|
||||
<!-- Pestaña de Acciones Básicas -->
|
||||
<mat-tab label="Acciones Básicas" class="basic-tab">
|
||||
<div class="tab-content">
|
||||
<div class="action-description">
|
||||
<mat-icon>power_settings_new</mat-icon>
|
||||
<span>Selecciona una acción básica del sistema</span>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Tipo de Acción</mat-label>
|
||||
<mat-select [(ngModel)]="selectedBasicAction" (selectionChange)="onBasicActionChange()">
|
||||
<mat-option value="poweron">
|
||||
<mat-icon>power</mat-icon> Encender Equipo
|
||||
</mat-option>
|
||||
<mat-option value="shutdown">
|
||||
<mat-icon>power_off</mat-icon> Apagar Equipo
|
||||
</mat-option>
|
||||
<mat-option value="reboot">
|
||||
<mat-icon>restart_alt</mat-icon> Reiniciar Equipo
|
||||
</mat-option>
|
||||
<mat-option value="login">
|
||||
<mat-icon>login</mat-icon> Iniciar Sesión
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Orden de ejecución</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<!-- Pestaña de Scripts -->
|
||||
<mat-tab label="Ejecutar Script" class="script-tab">
|
||||
<div class="tab-content">
|
||||
<div class="action-description">
|
||||
<mat-icon>code</mat-icon>
|
||||
<span>Ejecuta un script personalizado o crea uno nuevo</span>
|
||||
</div>
|
||||
|
||||
<!-- Opciones de script -->
|
||||
<div class="script-options">
|
||||
<div class="script-type-selector">
|
||||
<mat-radio-group [(ngModel)]="commandType" class="radio-group">
|
||||
<mat-radio-button value="new" class="radio-button">
|
||||
<mat-icon>edit</mat-icon> Nuevo Script
|
||||
</mat-radio-button>
|
||||
<mat-radio-button value="existing" class="radio-button">
|
||||
<mat-icon>storage</mat-icon> Script Guardado
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- Nuevo Script -->
|
||||
<div *ngIf="commandType === 'new'" class="new-command-container">
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Orden de ejecución</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
|
||||
</mat-form-field>
|
||||
<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>
|
||||
|
||||
<!-- Script Existente -->
|
||||
<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>
|
||||
|
||||
<!-- Detalles del Script -->
|
||||
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Orden de ejecución</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
|
||||
</mat-form-field>
|
||||
|
||||
<div class="script-content">
|
||||
<h3>Script:</h3>
|
||||
<div class="script-preview" [innerHTML]="scriptContent"></div>
|
||||
</div>
|
||||
|
||||
<div class="script-params" *ngIf="parameterNames.length > 0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
<!-- PASO 1: Selector de categorías -->
|
||||
<div *ngIf="!selectedCategory" class="categories-grid">
|
||||
<div *ngFor="let category of categories"
|
||||
class="category-card"
|
||||
[style.border-left-color]="category.color"
|
||||
(click)="selectCategory(category.id)">
|
||||
<div class="category-icon" [style.background-color]="category.color">
|
||||
<mat-icon>{{ category.icon }}</mat-icon>
|
||||
</div>
|
||||
<div class="category-info">
|
||||
<h3>{{ category.labelKey | translate }}</h3>
|
||||
<p>{{ category.descriptionKey | translate }}</p>
|
||||
</div>
|
||||
<mat-icon class="category-arrow" *ngIf="!category.infoOnly">chevron_right</mat-icon>
|
||||
<mat-icon class="category-arrow info-icon" *ngIf="category.infoOnly">info_outline</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PANEL INFORMATIVO: categorías que requieren contexto de equipo -->
|
||||
<div *ngIf="selectedCategory && isInfoOnlyCategory()" class="info-panel">
|
||||
<div class="info-panel-icon">
|
||||
<ng-container *ngFor="let cat of categories">
|
||||
<mat-icon *ngIf="cat.id === selectedCategory" [style.color]="cat.color" class="big-info-icon">{{ cat.icon }}</mat-icon>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="info-panel-content">
|
||||
<h3>{{ 'assistantRequiredTitle' | translate }}</h3>
|
||||
<p>{{ 'assistantRequiredDesc' | translate }}</p>
|
||||
|
||||
<div class="info-steps">
|
||||
<div class="info-step">
|
||||
<div class="step-number-badge">1</div>
|
||||
<span>{{ 'assistantStep1' | translate }}</span>
|
||||
</div>
|
||||
<div class="info-step">
|
||||
<div class="step-number-badge">2</div>
|
||||
<span>{{ 'assistantStep2' | translate }}</span>
|
||||
</div>
|
||||
<div class="info-step">
|
||||
<div class="step-number-badge">3</div>
|
||||
<span>{{ 'assistantStep3' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-supported-ops">
|
||||
<p class="info-supported-label">{{ 'supportedOpsLabel' | translate }}:</p>
|
||||
<ng-container *ngFor="let cat of categories">
|
||||
<ng-container *ngIf="cat.id === selectedCategory">
|
||||
<div *ngFor="let action of cat.actions" class="info-op-chip">
|
||||
<mat-icon [style.color]="cat.color">{{ action.icon }}</mat-icon>
|
||||
<span>{{ action.labelKey | translate }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PASO 2: Selector de acción dentro de categoría (solo si tiene >1 acción y no es infoOnly) -->
|
||||
<div *ngIf="selectedCategory && !selectedAction && !isInfoOnlyCategory()" class="actions-grid">
|
||||
<div class="actions-list">
|
||||
<ng-container *ngFor="let category of categories">
|
||||
<ng-container *ngIf="category.id === selectedCategory">
|
||||
<div *ngFor="let action of category.actions"
|
||||
class="action-card"
|
||||
[style.border-left-color]="category.color"
|
||||
(click)="selectAction(action.type)">
|
||||
<mat-icon [style.color]="category.color">{{ action.icon }}</mat-icon>
|
||||
<span>{{ action.labelKey | translate }}</span>
|
||||
<mat-icon class="category-arrow">chevron_right</mat-icon>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PASO 3: Formulario según acción seleccionada -->
|
||||
<div *ngIf="selectedAction" class="action-form">
|
||||
|
||||
<!-- Orden de ejecución (común a todas) -->
|
||||
<mat-form-field appearance="fill" class="order-field">
|
||||
<mat-label>{{ 'executionOrder' | translate }}</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="executionOrder" min="0">
|
||||
</mat-form-field>
|
||||
|
||||
<!-- ========== SYSTEM CONTROL (power-on, shutdown, reboot, login) ========== -->
|
||||
<div *ngIf="selectedAction === 'power-on' || selectedAction === 'shutdown' || selectedAction === 'reboot' || selectedAction === 'login'"
|
||||
class="form-section system-info">
|
||||
<div class="system-action-info">
|
||||
<mat-icon [style.color]="getSelectedCategoryColor()" class="big-icon">{{ getSelectedActionIcon() }}</mat-icon>
|
||||
<p>{{ 'systemActionHint' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== RUN SCRIPT ========== -->
|
||||
<div *ngIf="selectedAction === 'run-script'" class="form-section">
|
||||
<div class="script-type-selector">
|
||||
<mat-radio-group [(ngModel)]="commandType" class="radio-row">
|
||||
<mat-radio-button value="new">
|
||||
<mat-icon>edit</mat-icon> {{ 'newScript' | translate }}
|
||||
</mat-radio-button>
|
||||
<mat-radio-button value="existing">
|
||||
<mat-icon>storage</mat-icon> {{ 'savedScript' | translate }}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<div *ngIf="commandType === 'new'" class="new-command-container">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'enterScript' | translate }}</mat-label>
|
||||
<textarea matInput [(ngModel)]="newScript" rows="6" [placeholder]="'writeScriptHere' | translate"></textarea>
|
||||
</mat-form-field>
|
||||
<button mat-flat-button color="primary" (click)="saveNewScript()">{{ 'saveScript' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="commandType === 'existing'">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectScriptToRun' | translate }}</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>{{ 'scriptLabel' | translate }}:</h3>
|
||||
<div class="script-preview" [innerHTML]="scriptContent"></div>
|
||||
</div>
|
||||
<div class="script-params" *ngIf="parameterNames.length > 0">
|
||||
<h3>{{ 'enterParameters' | translate }}:</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]="('valueFor' | translate) + ' ' + paramName">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" >{{ 'buttonSave' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" *ngIf="selectedAction">{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {ConfigService} from "@services/config.service";
|
|||
import {ActivatedRoute} from "@angular/router";
|
||||
import {MatButtonToggleModule} from "@angular/material/button-toggle";
|
||||
import {MatRadioModule} from "@angular/material/radio";
|
||||
import {MatCheckboxModule} from "@angular/material/checkbox";
|
||||
|
||||
describe('CreateTaskScriptComponent', () => {
|
||||
let component: CreateTaskScriptComponent;
|
||||
|
|
@ -68,6 +69,7 @@ describe('CreateTaskScriptComponent', () => {
|
|||
MatMenuModule,
|
||||
MatButtonToggleModule,
|
||||
MatRadioModule,
|
||||
MatCheckboxModule,
|
||||
MatTreeModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,23 @@ import {
|
|||
SaveScriptComponent
|
||||
} from "../../../groups/components/client-main-view/run-script-assistant/save-script/save-script.component";
|
||||
import {FormBuilder, FormGroup} from "@angular/forms";
|
||||
import {TranslateService} from "@ngx-translate/core";
|
||||
|
||||
interface ActionCategory {
|
||||
id: string;
|
||||
icon: string;
|
||||
labelKey: string;
|
||||
descriptionKey: string;
|
||||
color: string;
|
||||
infoOnly: boolean;
|
||||
actions: ActionOption[];
|
||||
}
|
||||
|
||||
interface ActionOption {
|
||||
type: string;
|
||||
icon: string;
|
||||
labelKey: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-task-script',
|
||||
|
|
@ -19,20 +36,72 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
form: FormGroup;
|
||||
baseUrl: string;
|
||||
@Output() dataChange = new EventEmitter<any>();
|
||||
errorMessage = '';
|
||||
loading: boolean = false;
|
||||
scripts: any[] = [];
|
||||
scriptContent: string = "";
|
||||
parameters: any = {};
|
||||
selectedTabIndex: number = 0;
|
||||
selectedBasicAction: string = '';
|
||||
commandType: string = 'existing';
|
||||
selectedScript: any = null;
|
||||
newScript: string = '';
|
||||
executionOrder: Number = 0;
|
||||
executionOrder: number = 0;
|
||||
selection = new SelectionModel(true, []);
|
||||
parameterNames: string[] = Object.keys(this.parameters);
|
||||
commandTask: any;
|
||||
|
||||
selectedCategory: string = '';
|
||||
selectedAction: string = '';
|
||||
|
||||
categories: ActionCategory[] = [
|
||||
{
|
||||
id: 'image-ops',
|
||||
icon: 'perm_media',
|
||||
labelKey: 'catImageOps',
|
||||
descriptionKey: 'catImageOpsDesc',
|
||||
color: '#1565c0',
|
||||
infoOnly: true,
|
||||
actions: [
|
||||
{ type: 'deploy-image', icon: 'cloud_download', labelKey: 'deployImage' },
|
||||
{ type: 'create-image', icon: 'add_photo_alternate', labelKey: 'createImage' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'partition',
|
||||
icon: 'storage',
|
||||
labelKey: 'catPartition',
|
||||
descriptionKey: 'catPartitionDesc',
|
||||
color: '#2e7d32',
|
||||
infoOnly: true,
|
||||
actions: [
|
||||
{ type: 'partition-assistant', icon: 'view_column', labelKey: 'partitionAndFormat' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'system',
|
||||
icon: 'settings_power',
|
||||
labelKey: 'catSystemControl',
|
||||
descriptionKey: 'catSystemControlDesc',
|
||||
color: '#e65100',
|
||||
infoOnly: false,
|
||||
actions: [
|
||||
{ type: 'power-on', icon: 'power', labelKey: 'powerOnDevice' },
|
||||
{ type: 'shutdown', icon: 'power_off', labelKey: 'shutdownDevice' },
|
||||
{ type: 'reboot', icon: 'restart_alt', labelKey: 'rebootDevice' },
|
||||
{ type: 'login', icon: 'login', labelKey: 'loginSession' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'script',
|
||||
icon: 'code',
|
||||
labelKey: 'catCustomScript',
|
||||
descriptionKey: 'catCustomScriptDesc',
|
||||
color: '#6a1b9a',
|
||||
infoOnly: false,
|
||||
actions: [
|
||||
{ type: 'run-script', icon: 'terminal', labelKey: 'runScript' },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private http: HttpClient,
|
||||
|
|
@ -42,25 +111,56 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
private router: Router,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
private translate: TranslateService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.commandTask = this.data.commandTask;
|
||||
|
||||
this.loadScripts()
|
||||
|
||||
this.loadScripts();
|
||||
this.form = this.fb.group({
|
||||
content: [''],
|
||||
order: [''],
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
ngOnInit(): void {}
|
||||
|
||||
selectCategory(categoryId: string): void {
|
||||
const cat = this.categories.find(c => c.id === categoryId);
|
||||
if (cat?.infoOnly) {
|
||||
this.selectedCategory = categoryId;
|
||||
this.selectedAction = '';
|
||||
return;
|
||||
}
|
||||
this.selectedCategory = categoryId;
|
||||
if (cat && cat.actions.length === 1) {
|
||||
this.selectAction(cat.actions[0].type);
|
||||
} else {
|
||||
this.selectedAction = '';
|
||||
}
|
||||
}
|
||||
|
||||
selectAction(actionType: string): void {
|
||||
this.selectedAction = actionType;
|
||||
}
|
||||
|
||||
goBackToCategories(): void {
|
||||
this.selectedCategory = '';
|
||||
this.selectedAction = '';
|
||||
}
|
||||
|
||||
goBackToActions(): void {
|
||||
this.selectedAction = '';
|
||||
}
|
||||
|
||||
isInfoOnlyCategory(): boolean {
|
||||
const cat = this.categories.find(c => c.id === this.selectedCategory);
|
||||
return cat?.infoOnly || false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -70,75 +170,16 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
onTabChange(index: number): void {
|
||||
this.selectedTabIndex = index;
|
||||
if (index === 0) {
|
||||
this.selectedBasicAction = '';
|
||||
} else {
|
||||
this.selectedScript = null;
|
||||
this.newScript = '';
|
||||
}
|
||||
}
|
||||
|
||||
onBasicActionChange(): void {
|
||||
this.parameters = {};
|
||||
this.parameterNames = Object.keys(this.parameters);
|
||||
}
|
||||
|
||||
getBasicActionType(): string {
|
||||
const actionMap: { [key: string]: string } = {
|
||||
'poweron': 'POWER-ON',
|
||||
'shutdown': 'SHUTDOWN',
|
||||
'reboot': 'REBOOT',
|
||||
'login': 'LOGIN',
|
||||
};
|
||||
|
||||
return actionMap[this.selectedBasicAction] || '';
|
||||
}
|
||||
|
||||
validateBasicAction(): boolean {
|
||||
if (!this.selectedBasicAction) {
|
||||
this.toastService.error('Debe seleccionar un tipo de acción');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.executionOrder === null || this.executionOrder === undefined) {
|
||||
this.toastService.error('Debe especificar el orden de ejecución');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
saveNewScript() {
|
||||
if (!this.newScript.trim()) {
|
||||
this.toastService.error('Debe ingresar un script antes de guardar.');
|
||||
return;
|
||||
}
|
||||
const dialogRef = this.dialog.open(SaveScriptComponent, {
|
||||
width: '400px',
|
||||
data: this.newScript
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success('Script guardado correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onScriptChange() {
|
||||
onScriptChange(): void {
|
||||
if (this.selectedScript) {
|
||||
this.scriptContent = this.selectedScript.script;
|
||||
|
||||
const matches = this.scriptContent.match(/@(\w+)/g) || [];
|
||||
const uniqueParams = Array.from(new Set(matches.map(m => m.slice(1))));
|
||||
const uniqueParams = Array.from(new Set(matches.map((m: string) => m.slice(1))));
|
||||
|
||||
this.parameters = {};
|
||||
uniqueParams.forEach(param => this.parameters[param] = '');
|
||||
|
||||
this.parameterNames = uniqueParams;
|
||||
|
||||
this.updateScript();
|
||||
}
|
||||
}
|
||||
|
|
@ -150,16 +191,82 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
|
||||
updateScript(): void {
|
||||
let updatedScript = this.selectedScript.script;
|
||||
|
||||
for (const [key, value] of Object.entries(this.parameters)) {
|
||||
const regex = new RegExp(`@${key}\\b`, 'g');
|
||||
updatedScript = updatedScript.replace(regex, value || `@${key}`);
|
||||
}
|
||||
|
||||
this.scriptContent = updatedScript;
|
||||
}
|
||||
|
||||
validateScript(): boolean {
|
||||
saveNewScript(): void {
|
||||
if (!this.newScript.trim()) {
|
||||
this.toastService.error('Debe ingresar un script antes de guardar.');
|
||||
return;
|
||||
}
|
||||
const dialogRef = this.dialog.open(SaveScriptComponent, {
|
||||
width: '400px',
|
||||
data: this.newScript
|
||||
});
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success('Script guardado correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (!this.selectedAction) {
|
||||
this.toastService.error(this.translate.instant('selectActionTypeError'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.executionOrder === null || this.executionOrder === undefined) {
|
||||
this.toastService.error(this.translate.instant('executionOrderRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
let type = '';
|
||||
let content = '';
|
||||
|
||||
switch (this.selectedAction) {
|
||||
case 'run-script':
|
||||
if (!this.validateScript()) return;
|
||||
type = 'run-script';
|
||||
content = this.commandType === 'existing' ? this.scriptContent : this.newScript;
|
||||
break;
|
||||
|
||||
case 'power-on':
|
||||
case 'shutdown':
|
||||
case 'reboot':
|
||||
case 'login':
|
||||
type = this.selectedAction;
|
||||
content = this.selectedAction;
|
||||
break;
|
||||
|
||||
default:
|
||||
this.toastService.error('Tipo de acción no reconocido');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
commandTask: this.commandTask['@id'],
|
||||
content: content,
|
||||
order: this.executionOrder,
|
||||
type: type,
|
||||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/command-task-scripts`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(this.translate.instant('actionAddedSuccess'));
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private validateScript(): boolean {
|
||||
if (this.commandType === 'new') {
|
||||
if (!this.newScript.trim()) {
|
||||
this.toastService.error('Debe ingresar un script');
|
||||
|
|
@ -171,60 +278,31 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.executionOrder === null || this.executionOrder === undefined) {
|
||||
this.toastService.error('Debe especificar el orden de ejecución');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
let isValid = false;
|
||||
let content = '';
|
||||
let type = '';
|
||||
|
||||
if (this.selectedTabIndex === 0) {
|
||||
isValid = this.validateBasicAction();
|
||||
if (isValid) {
|
||||
content = this.getBasicActionType();
|
||||
type = 'basic-action';
|
||||
}
|
||||
} else { // Pestaña de scripts
|
||||
isValid = this.validateScript();
|
||||
if (isValid) {
|
||||
content = this.commandType === 'existing' ? this.scriptContent : this.newScript;
|
||||
type = 'run-script';
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.data) {
|
||||
this.toastService.error('Error: No se recibieron datos del diálogo');
|
||||
return;
|
||||
}
|
||||
|
||||
this.http.post(`${this.baseUrl}/command-task-scripts`, {
|
||||
commandTask: this.commandTask['@id'],
|
||||
content: content,
|
||||
order: this.executionOrder,
|
||||
type: this.selectedBasicAction,
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Tarea creada con éxito');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
getSelectedCategoryColor(): string {
|
||||
const cat = this.categories.find(c => c.id === this.selectedCategory);
|
||||
return cat?.color || '#1976d2';
|
||||
}
|
||||
|
||||
getSelectedActionLabel(): string {
|
||||
for (const cat of this.categories) {
|
||||
const action = cat.actions.find(a => a.type === this.selectedAction);
|
||||
if (action) return this.translate.instant(action.labelKey);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
getSelectedActionIcon(): string {
|
||||
for (const cat of this.categories) {
|
||||
const action = cat.actions.find(a => a.type === this.selectedAction);
|
||||
if (action) return action.icon;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,32 +9,32 @@
|
|||
<mat-button-toggle-group *ngIf="data?.source === 'assistant'" [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode" appearance="standard">
|
||||
<mat-button-toggle value="create">
|
||||
<mat-icon>add_task</mat-icon>
|
||||
Crear tarea
|
||||
{{ 'createTaskLabel' | translate }}
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle value="add">
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
Introducir en tarea existente
|
||||
{{ 'addToExistingTask' | translate }}
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
||||
<div *ngIf="taskMode === 'add'" class="select-task">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione una tarea</mat-label>
|
||||
<mat-label>{{ 'selectTask' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="selectedExistingTask" name="existingTask">
|
||||
<mat-option *ngFor="let task of existingTasks" [value]="task">{{ task.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Orden de ejecución</mat-label>
|
||||
<mat-label>{{ 'executionOrder' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[(ngModel)]="executionOrder"
|
||||
name="executionOrder"
|
||||
min="1"
|
||||
placeholder="Introduce el orden"
|
||||
[placeholder]="'enterOrder' | translate"
|
||||
>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
|
@ -42,30 +42,30 @@
|
|||
<form *ngIf="taskMode === 'create' && taskForm && !loading" [formGroup]="taskForm" class="task-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" placeholder="{{ 'nameLabel' | translate }}">
|
||||
<input matInput formControlName="name" [placeholder]="'nameLabel' | translate">
|
||||
<mat-error *ngIf="taskForm.get('name')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'notesLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="notes" placeholder="{{ 'notesPlaceholder' | translate }}"></textarea>
|
||||
<textarea matInput formControlName="notes" [placeholder]="'notesPlaceholder' | translate"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox *ngIf="!editing" formControlName="scheduleAfterCreate" class="schedule-checkbox">
|
||||
¿Quieres programar la tarea al finalizar su creación?
|
||||
{{ 'scheduleAfterCreate' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<mat-divider style="margin: 15px 0;"></mat-divider>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Ámbito</mat-label>
|
||||
<mat-label>{{ 'scopeLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)"
|
||||
[disabled]="data?.clients && data.clients.length >= 1">
|
||||
<mat-option value="organizational-unit">Unidad Organizativa</mat-option>
|
||||
<mat-option value="classrooms-group">Grupo de aulas</mat-option>
|
||||
<mat-option value="classroom">Aulas</mat-option>
|
||||
<mat-option value="clients-group">Grupos de clientes</mat-option>
|
||||
<mat-option value="clients">Clientes</mat-option>
|
||||
<mat-option value="organizational-unit">{{ 'scopeOrganizationalUnit' | translate }}</mat-option>
|
||||
<mat-option value="classrooms-group">{{ 'scopeClassroomsGroup' | translate }}</mat-option>
|
||||
<mat-option value="classroom">{{ 'scopeClassroom' | translate }}</mat-option>
|
||||
<mat-option value="clients-group">{{ 'scopeClientsGroup' | translate }}</mat-option>
|
||||
<mat-option value="clients">{{ 'scopeClients' | translate }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
|
|
@ -80,11 +80,11 @@
|
|||
</mat-form-field>
|
||||
|
||||
<div *ngIf="taskForm.get('scope')?.value === 'clients'" class="clients-selection">
|
||||
<h4>Clientes seleccionados</h4>
|
||||
<h4>{{ 'selectedClients' | translate }}</h4>
|
||||
|
||||
<div *ngIf="data?.selectedClients && data.selectedClients.length > 0" class="pre-selected-info">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>Los clientes han sido pre-seleccionados desde el componente de despliegue de imágenes.</span>
|
||||
<span>{{ 'preSelectedInfo' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<div class="clients-list">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { ConfigService } from '@services/config.service';
|
|||
import {of} from "rxjs";
|
||||
import {startWith, switchMap} from "rxjs/operators";
|
||||
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
|
||||
import {ShowTaskScriptComponent} from "../show-task-script/show-task-script.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-task',
|
||||
|
|
@ -291,17 +292,12 @@ export class CreateTaskComponent implements OnInit {
|
|||
next: response => {
|
||||
this.toastr.success('Tarea creada con éxito');
|
||||
this.dialogRef.close(response);
|
||||
if (formData.scheduleAfterCreate) {
|
||||
const dialogRef = this.dialog.open(CreateTaskScheduleComponent, {
|
||||
width: '800px',
|
||||
data: { task: response }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastr.success('Tarea programada correctamente');
|
||||
}
|
||||
});
|
||||
if (this.data?.source === 'assistant') {
|
||||
if (formData.scheduleAfterCreate) {
|
||||
this.openScheduleDialog(response);
|
||||
}
|
||||
} else {
|
||||
this.openPostCreationFlow(response, formData.scheduleAfterCreate);
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
|
|
@ -311,6 +307,40 @@ export class CreateTaskComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
private openScheduleDialog(task: any): void {
|
||||
this.dialog.open(CreateTaskScheduleComponent, {
|
||||
width: '800px',
|
||||
data: { task: task }
|
||||
}).afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastr.success('Tarea programada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private openPostCreationFlow(task: any, scheduleAfterCreate: boolean): void {
|
||||
const actionsDialog = this.dialog.open(ShowTaskScriptComponent, {
|
||||
width: '85vw',
|
||||
height: '85vh',
|
||||
maxWidth: '85vw',
|
||||
maxHeight: '85vh',
|
||||
data: { commandTask: task }
|
||||
});
|
||||
|
||||
actionsDialog.afterClosed().subscribe(() => {
|
||||
if (scheduleAfterCreate) {
|
||||
this.dialog.open(CreateTaskScheduleComponent, {
|
||||
width: '800px',
|
||||
data: { task: task }
|
||||
}).afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastr.success('Tarea programada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,48 +8,40 @@
|
|||
|
||||
<mat-card-content>
|
||||
<p><strong>{{ 'taskId' | translate }}:</strong> {{ task.uuid }}</p>
|
||||
<p><strong>{{ 'status' | translate }}:</strong> {{ task.status }}</p>
|
||||
<p><strong>{{ 'creationDate' | translate }}:</strong> {{ task.createdAt | date: 'short' }}</p>
|
||||
<p><strong>{{ 'notes' | translate }}:</strong> {{ task.notes }}</p>
|
||||
|
||||
<h3>{{ 'includedCommandGroups' | translate }}</h3>
|
||||
<table mat-table [dataSource]="task.commandGroups" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'commandGroupColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let group"> {{ group.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="uuid">
|
||||
<th mat-header-cell *matHeaderCellDef> UUID </th>
|
||||
<td mat-cell *matCellDef="let group"> {{ group.uuid }} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['name', 'uuid']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['name', 'uuid'];"></tr>
|
||||
</table>
|
||||
|
||||
<h3>{{ 'commandsToExecute' | translate }}</h3>
|
||||
<div *ngFor="let group of task.commandGroups">
|
||||
<p><strong>{{ 'group' | translate }}: </strong>{{ group.name }}</p>
|
||||
<table mat-table [dataSource]="group.commands" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="commandName">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'commandColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||
</ng-container>
|
||||
<table mat-table [dataSource]="task.commandTaskScripts || []" class="mat-elevation-z8" *ngIf="task.commandTaskScripts?.length > 0">
|
||||
<ng-container matColumnDef="order">
|
||||
<th mat-header-cell *matHeaderCellDef>Orden</th>
|
||||
<td mat-cell *matCellDef="let script">{{ script.order }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="commandUuid">
|
||||
<th mat-header-cell *matHeaderCellDef> UUID </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.uuid }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>Tipo</th>
|
||||
<td mat-cell *matCellDef="let script">{{ script.type }}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['commandName', 'commandUuid']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['commandName', 'commandUuid'];"></tr>
|
||||
</table>
|
||||
</div>
|
||||
<ng-container matColumnDef="content">
|
||||
<th mat-header-cell *matHeaderCellDef>Contenido</th>
|
||||
<td mat-cell *matCellDef="let script">
|
||||
<span *ngIf="script.content">{{ script.content }}</span>
|
||||
<span *ngIf="!script.content && script.parameters">
|
||||
(parámetros configurados)
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['order', 'type', 'content']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['order', 'type', 'content'];"></tr>
|
||||
</table>
|
||||
|
||||
<p *ngIf="!task.commandTaskScripts?.length">No hay acciones configuradas.</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<div class="task-actions">
|
||||
<button class="ordinary-button" (click)="closeDialog()">Cancel</button>
|
||||
<button class="ordinary-button" (click)="closeDialog()">{{ 'buttonCancel' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ table {
|
|||
|
||||
.recurrence-type-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.recurrence-chip {
|
||||
|
|
@ -158,14 +157,12 @@ table {
|
|||
|
||||
/* Estilos para el campo executionTime */
|
||||
.time-display {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
background-color: #667eea;
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.time-icon {
|
||||
|
|
@ -182,14 +179,12 @@ table {
|
|||
|
||||
/* Estilos para el campo nextExecution */
|
||||
.next-execution {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
background-color: #4facfe;
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.execution-icon {
|
||||
|
|
@ -201,7 +196,6 @@ table {
|
|||
|
||||
/* Estilos para el campo daysOfWeek */
|
||||
.days-display {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.days-header {
|
||||
|
|
@ -229,7 +223,6 @@ table {
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.day-chip {
|
||||
|
|
@ -245,7 +238,6 @@ table {
|
|||
|
||||
/* Estilos para el campo months */
|
||||
.months-display {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.months-header {
|
||||
|
|
@ -273,7 +265,6 @@ table {
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.month-chip {
|
||||
|
|
@ -290,18 +281,15 @@ table {
|
|||
/* Estilos para el campo enabled */
|
||||
.status-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
min-width: 100px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
|
|
@ -323,8 +311,7 @@ table {
|
|||
|
||||
/* Estilos para datos no definidos */
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.no-data-text {
|
||||
|
|
@ -375,7 +362,7 @@ table {
|
|||
.paginator-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Estilos para los botones de acción en la tabla */
|
||||
|
|
|
|||
|
|
@ -1,44 +1,30 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<h2 mat-dialog-title>Gestionar programaciones de tareas en {{ data.commandTask?.name }}</h2>
|
||||
<h2 mat-dialog-title>{{ 'manageTaskSchedules' | translate }} {{ data.commandTask?.name }}</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="header-actions">
|
||||
<button (click)="createNewSchedule()" class="action-button">
|
||||
<mat-icon>add</mat-icon>
|
||||
Nueva Programación
|
||||
{{ 'newSchedule' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
|
||||
text="Busca programaciones por nombre para localizar una programación específica rápidamente.">
|
||||
<mat-label i18n="@@searchLabel">Buscar programación</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder"
|
||||
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'searchScheduleLabel' | translate }}</mat-label>
|
||||
<input matInput [placeholder]="'searchPlaceholder' | translate" [(ngModel)]="filters['name']"
|
||||
(keyup.enter)="loadData()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<button *ngIf="filters['name']" mat-icon-button matSuffix aria-label="Clear search"
|
||||
(click)="filters['name'] = ''; loadData()">
|
||||
<button *ngIf="filters['name']" mat-icon-button matSuffix (click)="filters['name'] = ''; loadData()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchTypeStep" text="Busca programaciones por tipo de recurrencia.">
|
||||
<mat-label i18n="@@searchLabel">Buscar por tipo</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['recurrence']" i18n-placeholder="@@searchPlaceholder"
|
||||
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<button *ngIf="filters['recurrence']" mat-icon-button matSuffix aria-label="Clear type search"
|
||||
(click)="filters['recurrence'] = ''; loadData()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="Visualiza y administra las programaciones de tareas según los filtros aplicados.">
|
||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let schedule">
|
||||
|
|
@ -54,7 +40,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'executionTime'">
|
||||
<div class="time-display">
|
||||
<mat-icon class="time-icon">schedule</mat-icon>
|
||||
|
|
@ -65,45 +51,45 @@
|
|||
<ng-container *ngIf="column.columnDef === 'nextExecution'">
|
||||
<div class="next-execution">
|
||||
<mat-icon class="execution-icon">schedule</mat-icon>
|
||||
<span>{{ column.cell(schedule) || 'No calculada' }}</span>
|
||||
<span>{{ column.cell(schedule) || ('notCalculated' | translate) }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'daysOfWeek'">
|
||||
<div class="days-display" *ngIf="column.cell(schedule)">
|
||||
<div class="days-chips">
|
||||
<span *ngFor="let day of column.cell(schedule).days"
|
||||
<span *ngFor="let day of column.cell(schedule).days"
|
||||
class="day-chip">{{ day }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="no-data" *ngIf="!column.cell(schedule)">
|
||||
<span class="no-data-text">No definido</span>
|
||||
<span class="no-data-text">{{ 'notDefined' | translate }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'months'">
|
||||
<div class="months-display" *ngIf="column.cell(schedule)">
|
||||
<div class="months-chips">
|
||||
<span *ngFor="let month of column.cell(schedule).months"
|
||||
<span *ngFor="let month of column.cell(schedule).months"
|
||||
class="month-chip">{{ month }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="no-data" *ngIf="!column.cell(schedule)">
|
||||
<span class="no-data-text">No definido</span>
|
||||
<span class="no-data-text">{{ 'notDefined' | translate }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef !== 'recurrenceType' && column.columnDef !== 'executionTime' &&
|
||||
column.columnDef !== 'daysOfWeek' && column.columnDef !== 'months' &&
|
||||
|
||||
<ng-container *ngIf="column.columnDef !== 'recurrenceType' && column.columnDef !== 'executionTime' &&
|
||||
column.columnDef !== 'daysOfWeek' && column.columnDef !== 'months' &&
|
||||
column.columnDef !== 'nextExecution' && column.columnDef !== 'enabled'">
|
||||
{{ column.cell(schedule) }}
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'enabled'">
|
||||
<div class="status-container">
|
||||
<div class="status-indicator" [ngClass]="schedule.enabled ? 'status-active' : 'status-inactive'">
|
||||
<mat-icon class="status-icon">{{ schedule.enabled ? 'check_circle' : 'cancel' }}</mat-icon>
|
||||
<span class="status-text">{{ schedule.enabled ? 'Activo' : 'Inactivo' }}</span>
|
||||
<span class="status-text">{{ (schedule.enabled ? 'active' : 'inactive') | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
@ -111,13 +97,13 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let schedule" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="editSchedule(schedule)" matTooltip="Editar programación">
|
||||
<mat-icon i18n="@@editElementTooltip">edit</mat-icon>
|
||||
<button mat-icon-button color="primary" (click)="editSchedule(schedule)" [matTooltip]="'editScheduleTooltip' | translate">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteSchedule(schedule)" matTooltip="Eliminar programación">
|
||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||
<button mat-icon-button color="warn" (click)="deleteSchedule(schedule)" [matTooltip]="'deleteScheduleTooltip' | translate">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
|
@ -126,8 +112,7 @@
|
|||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep"
|
||||
text="Navega entre las páginas de programaciones usando el paginador.">
|
||||
<div class="paginator-container">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
|
|
@ -135,5 +120,5 @@
|
|||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">Cerrar</button>
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'close' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
];
|
||||
|
||||
displayedColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'daysOfWeek', 'months', 'enabled', 'actions'];
|
||||
allColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'daysOfWeek', 'months', 'enabled', 'actions'];
|
||||
compactColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'enabled', 'actions'];
|
||||
|
||||
constructor(
|
||||
private toastService: ToastrService,
|
||||
|
|
@ -60,6 +62,7 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
(data) => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.updateDisplayedColumns();
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
|
|
@ -295,6 +298,11 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
return mapping[monthName?.toLowerCase?.()] ?? -1;
|
||||
}
|
||||
|
||||
updateDisplayedColumns(): void {
|
||||
const hasRecurrent = this.dataSource.data.some((s: any) => s.recurrenceType !== 'none');
|
||||
this.displayedColumns = hasRecurrent ? this.allColumns : this.compactColumns;
|
||||
}
|
||||
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
|
|
|
|||
|
|
@ -283,26 +283,129 @@ mat-spinner {
|
|||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* Estilos para el indicador de orden */
|
||||
.order-indicator {
|
||||
/* Badge de paso */
|
||||
.step-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
box-shadow: 0 2px 4px rgba(63, 81, 181, 0.3);
|
||||
}
|
||||
|
||||
/* Display de tipo de acción */
|
||||
.type-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: #f8f9fa;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Contenido del script en celda */
|
||||
.script-content-cell {
|
||||
background-color: #f5f5f5;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
white-space: pre-wrap;
|
||||
font-size: 12px;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.no-content-hint {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Hint secuencial */
|
||||
.sequential-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 24px;
|
||||
color: #1565c0;
|
||||
background: #e3f2fd;
|
||||
border-radius: 4px;
|
||||
margin: 0 24px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.sequential-hint mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Timeline resumen */
|
||||
.timeline-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 16px 24px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.timeline-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
background: white;
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.order-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #6c757d;
|
||||
.timeline-number {
|
||||
background: #3f51b5;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.order-number {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
font-size: 14px;
|
||||
.timeline-label {
|
||||
color: #444;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.timeline-arrow {
|
||||
color: #9e9e9e;
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +1,81 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<h2 mat-dialog-title>Gestionar scripts de tareas en {{ data.commandTask?.name }}</h2>
|
||||
<h2 mat-dialog-title>{{ 'taskActionsTitle' | translate }}: {{ data.commandTask?.name }}</h2>
|
||||
|
||||
<div *ngIf="dataSource.data.length > 1" class="sequential-hint">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>{{ 'taskActionsSequentialHint' | translate }}</span>
|
||||
</div>
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="header-actions">
|
||||
<button (click)="createNewScript()" class="action-button">
|
||||
<mat-icon>add</mat-icon>
|
||||
Nueva Acción
|
||||
{{ 'newAction' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
|
||||
text="Busca subredes por nombre para localizar una subred específica rápidamente.">
|
||||
<mat-label i18n="@@searchLabel">Buscar contenido de script</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder"
|
||||
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<button *ngIf="filters['name']" mat-icon-button matSuffix aria-label="Clear tree search"
|
||||
(click)="filters['name'] = ''; loadData()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
<div *ngIf="dataSource.data.length > 0 && !loading" class="timeline-summary">
|
||||
<ng-container *ngFor="let item of dataSource.data; let last = last">
|
||||
<div class="timeline-step">
|
||||
<span class="timeline-number">{{ item.order }}</span>
|
||||
<span class="timeline-label">{{ getTypeDisplay(item.type).label }}</span>
|
||||
</div>
|
||||
<mat-icon *ngIf="!last" class="timeline-arrow">arrow_forward</mat-icon>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="Visualiza y administra las subredes listadas según los filtros aplicados.">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let schedule">
|
||||
<ng-container *ngIf="column.columnDef === 'content'; else checkOtherColumn">
|
||||
<div style="background-color: #f5f5f5; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-family: monospace; white-space: pre-wrap; font-size: 13px;">
|
||||
{{ column.cell(schedule) }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
|
||||
<ng-template #checkOtherColumn>
|
||||
<ng-container *ngIf="column.columnDef === 'parameters'; else normalCell">
|
||||
<button class="parameters-button" (click)="openParametersModal(schedule.parameters)">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span>Ver parámetros</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<ng-container matColumnDef="order">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'executionOrder' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div class="step-badge">
|
||||
<span class="step-number">{{ item.order }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #normalCell>
|
||||
{{ column.cell(schedule) }}
|
||||
</ng-template>
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'actionType' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div class="type-display">
|
||||
<mat-icon class="type-icon">{{ getTypeDisplay(item.type).icon }}</mat-icon>
|
||||
<span>{{ getTypeDisplay(item.type).label }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="content">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'scriptLabel' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div *ngIf="item.content" class="script-content-cell">
|
||||
<code>{{ item.content }}</code>
|
||||
</div>
|
||||
<span *ngIf="!item.content" class="no-content-hint">
|
||||
({{ 'viewParameters' | translate }})
|
||||
</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="parameters">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'viewParameters' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<button class="parameters-button" (click)="openParametersModal(item.parameters)"
|
||||
*ngIf="item.parameters && (item.parameters | json) !== '{}'">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span>{{ 'viewParameters' | translate }}</span>
|
||||
</button>
|
||||
<span *ngIf="!item.parameters || (item.parameters | json) === '{}'" class="no-content-hint">—</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<td mat-cell *matCellDef="let schedule" style="text-align: center;">
|
||||
<button mat-icon-button color="warn" (click)="deleteTaskScript(schedule)">
|
||||
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let item" style="text-align: center;">
|
||||
<button mat-icon-button color="warn" (click)="deleteTaskScript(item)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
|
@ -65,8 +84,7 @@
|
|||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep"
|
||||
text="Navega entre las páginas de subredes usando el paginador.">
|
||||
<div class="paginator-container">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
|
|
@ -74,5 +92,5 @@
|
|||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">Cerrar</button>
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'close' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
|||
|
|
@ -25,14 +25,31 @@ export class ShowTaskScriptComponent implements OnInit{
|
|||
filters: { [key: string]: string } = {};
|
||||
|
||||
columns = [
|
||||
{ columnDef: 'id', header: 'ID', cell: (client: any) => client.id },
|
||||
{ columnDef: 'order', header: 'Orden', cell: (client: any) => client.order },
|
||||
{ columnDef: 'content', header: 'Script', cell: (client: any) => client.content },
|
||||
{ columnDef: 'type', header: 'Tipo', cell: (client: any) => client.type },
|
||||
{ columnDef: 'parameters', header: 'Parámetros', cell: (client: any) => client.parameters },
|
||||
{ columnDef: 'id', header: 'ID', cell: (item: any) => item.id },
|
||||
{ columnDef: 'order', header: 'Orden', cell: (item: any) => item.order },
|
||||
{ columnDef: 'type', header: 'Tipo', cell: (item: any) => item.type },
|
||||
{ columnDef: 'parameters', header: 'Parámetros', cell: (item: any) => item.parameters },
|
||||
{ columnDef: 'content', header: 'Contenido', cell: (item: any) => item.content },
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['id', 'order', 'type', 'parameters', 'content', 'actions'];
|
||||
displayedColumns: string[] = ['order', 'type', 'content', 'parameters', 'actions'];
|
||||
|
||||
typeLabels: { [key: string]: { label: string, icon: string } } = {
|
||||
'partition-and-format': { label: 'Particionar y formatear', icon: 'storage' },
|
||||
'partition-assistant': { label: 'Asistente de particionado', icon: 'storage' },
|
||||
'deploy-image': { label: 'Desplegar imagen', icon: 'cloud_download' },
|
||||
'create-image': { label: 'Crear imagen', icon: 'cloud_upload' },
|
||||
'run-script': { label: 'Ejecutar script', icon: 'code' },
|
||||
'reboot': { label: 'Reiniciar', icon: 'restart_alt' },
|
||||
'shutdown': { label: 'Apagar', icon: 'power_off' },
|
||||
'poweron': { label: 'Encender', icon: 'power' },
|
||||
'power-on': { label: 'Encender', icon: 'power' },
|
||||
'login': { label: 'Iniciar sesión', icon: 'login' },
|
||||
};
|
||||
|
||||
getTypeDisplay(type: string): { label: string, icon: string } {
|
||||
return this.typeLabels[type] || { label: type, icon: 'play_arrow' };
|
||||
}
|
||||
|
||||
constructor(
|
||||
private toastService: ToastrService,
|
||||
|
|
@ -53,9 +70,11 @@ export class ShowTaskScriptComponent implements OnInit{
|
|||
|
||||
loadData() {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.baseUrl}/command-task-scripts?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&commandTask.id=${this.data.commandTask?.id}`, { params: this.filters }).subscribe(
|
||||
this.http.get<any>(`${this.baseUrl}/command-task-scripts?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&commandTask.id=${this.data.commandTask?.id}&order[order]=ASC`, { params: this.filters }).subscribe(
|
||||
(data) => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
const items = data['hydra:member'];
|
||||
items.sort((a: any, b: any) => (a.order ?? 0) - (b.order ?? 0));
|
||||
this.dataSource.data = items;
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,6 +10,26 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
/* Layout compacto: tipo + configuración en una fila */
|
||||
.compact-config-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.compact-section {
|
||||
padding: 20px !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.type-section {
|
||||
flex: 0 0 280px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Secciones del formulario */
|
||||
.form-section {
|
||||
background: white !important;
|
||||
|
|
@ -249,6 +269,14 @@ mat-form-field {
|
|||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.compact-config-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.type-section {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
|
@ -879,8 +907,8 @@ mat-form-field {
|
|||
|
||||
.git-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 16px;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.git-info-item {
|
||||
|
|
|
|||
|
|
@ -33,14 +33,13 @@
|
|||
</div>
|
||||
|
||||
<div class="select-container">
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>settings</mat-icon>
|
||||
Configuración de tipo de imagen
|
||||
</div>
|
||||
|
||||
<div class="selector">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<div class="compact-config-row">
|
||||
<div class="form-section compact-section type-section">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>settings</mat-icon>
|
||||
Tipo de imagen
|
||||
</div>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Tipo de imagen</mat-label>
|
||||
<mat-select [(ngModel)]="imageType" class="full-width" (selectionChange)="onImageTypeSelected($event.value)">
|
||||
<mat-option [value]="'monolithic'">Monolítica</mat-option>
|
||||
|
|
@ -48,89 +47,128 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section" *ngIf="imageType === 'git'">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>code</mat-icon>
|
||||
Configuración Git
|
||||
<div class="form-section compact-section config-section" *ngIf="imageType === 'monolithic'">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>image</mat-icon>
|
||||
Configuración
|
||||
</div>
|
||||
|
||||
<div class="action-chips-container">
|
||||
<mat-chip-listbox [(ngModel)]="monolithicAction" required class="action-chip-listbox">
|
||||
<mat-chip-option value="create" class="action-chip create-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'create'})">
|
||||
<span>Crear imagen</span>
|
||||
</mat-chip-option>
|
||||
<mat-chip-option value="update" class="action-chip update-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'update'})">
|
||||
<span>Actualizar imagen</span>
|
||||
</mat-chip-option>
|
||||
</mat-chip-listbox>
|
||||
</div>
|
||||
|
||||
<div *ngIf="monolithicAction === 'create'">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Nombre canónico</mat-label>
|
||||
<input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
|
||||
<mat-hint>Nombre para la nueva imagen</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="monolithicAction === 'update'">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione imagen</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required>
|
||||
<mat-option [value]="null">Seleccionar imagen para actualizar</mat-option>
|
||||
<mat-option *ngFor="let image of images" [value]="image">{{ image?.name }}</mat-option>
|
||||
</mat-select>
|
||||
<button *ngIf="selectedImage" mat-icon-button matSuffix aria-label="Clear client search"
|
||||
(click)="selectedImage = null; resetCanonicalName()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint>Selecciona la imagen existente que quieres actualizar.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="git-info-container" *ngIf="selectedPartition && imageType === 'git' && (hasValidGitData || loadingGitData)">
|
||||
<div class="git-info-card">
|
||||
<div class="git-info-header">
|
||||
<mat-icon class="git-info-icon">info</mat-icon>
|
||||
<span class="git-info-title">La imagen en caché a actualizar es:</span>
|
||||
<mat-spinner *ngIf="loadingGitData" diameter="20" class="git-loading-spinner"></mat-spinner>
|
||||
</div>
|
||||
<div class="" *ngIf="!loadingGitData && hasValidGitData">
|
||||
<div class="git-info-grid">
|
||||
<div class="git-info-item">
|
||||
<mat-form-field appearance="outline" class="git-form-field">
|
||||
<mat-label>
|
||||
<mat-icon class="git-item-icon">folder</mat-icon>
|
||||
Repositorio
|
||||
</mat-label>
|
||||
<input matInput [value]="gitData.repo" readonly>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="git-info-item">
|
||||
<mat-form-field appearance="outline" class="git-form-field">
|
||||
<mat-label>
|
||||
<mat-icon class="git-item-icon">account_tree</mat-icon>
|
||||
Rama origen
|
||||
</mat-label>
|
||||
<input matInput [value]="gitData.branch" readonly>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="git-info-item">
|
||||
<mat-form-field appearance="outline" class="git-form-field">
|
||||
<mat-label>
|
||||
<mat-icon class="git-item-icon">call_split</mat-icon>
|
||||
Rama destino
|
||||
</mat-label>
|
||||
<input matInput
|
||||
[(ngModel)]="destinationBranch"
|
||||
[readonly]="!isDestinationBranchEditable"
|
||||
placeholder="Nombre de la rama destino"
|
||||
[matTooltip]="isDestinationBranchEditable ? 'Guardar cambios' : 'Editar nombre de la rama destino'">
|
||||
<button mat-icon-button
|
||||
matSuffix
|
||||
[matTooltip]="isDestinationBranchEditable ? 'Guardar' : 'Editar rama destino'"
|
||||
(click)="toggleDestinationBranchEdit()">
|
||||
<mat-icon>{{ isDestinationBranchEditable ? 'save' : 'edit' }}</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-section compact-section config-section" *ngIf="imageType === 'git'">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>code</mat-icon>
|
||||
Configuración Git
|
||||
</div>
|
||||
|
||||
<div class="git-info-container" *ngIf="selectedPartition && imageType === 'git' && (hasValidGitData || loadingGitData)">
|
||||
<div class="git-info-card">
|
||||
<div class="git-info-header">
|
||||
<mat-icon class="git-info-icon">info</mat-icon>
|
||||
<span class="git-info-title">La imagen en caché a actualizar es:</span>
|
||||
<mat-spinner *ngIf="loadingGitData" diameter="20" class="git-loading-spinner"></mat-spinner>
|
||||
</div>
|
||||
|
||||
<div class="repository-warning" *ngIf="repositoryNotFound">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<div class="warning-content">
|
||||
<div class="warning-title">Repositorio no encontrado</div>
|
||||
<div class="warning-message">
|
||||
El repositorio "{{ gitData.repo }}" no existe en el listado de repositorios disponibles.
|
||||
Es posible que necesites crearlo primero.
|
||||
<div *ngIf="!loadingGitData && hasValidGitData">
|
||||
<div class="git-info-grid">
|
||||
<div class="git-info-item">
|
||||
<mat-form-field appearance="outline" class="git-form-field">
|
||||
<mat-label>
|
||||
<mat-icon class="git-item-icon">folder</mat-icon>
|
||||
Repositorio
|
||||
</mat-label>
|
||||
<input matInput [value]="gitData.repo" readonly>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="git-info-item">
|
||||
<mat-form-field appearance="outline" class="git-form-field">
|
||||
<mat-label>
|
||||
<mat-icon class="git-item-icon">account_tree</mat-icon>
|
||||
Rama origen
|
||||
</mat-label>
|
||||
<input matInput [value]="gitData.branch" readonly>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="git-info-item">
|
||||
<mat-form-field appearance="outline" class="git-form-field">
|
||||
<mat-label>
|
||||
<mat-icon class="git-item-icon">call_split</mat-icon>
|
||||
Rama destino
|
||||
</mat-label>
|
||||
<input matInput
|
||||
[(ngModel)]="destinationBranch"
|
||||
[readonly]="!isDestinationBranchEditable"
|
||||
placeholder="Nombre de la rama destino"
|
||||
[matTooltip]="isDestinationBranchEditable ? 'Guardar cambios' : 'Editar nombre de la rama destino'">
|
||||
<button mat-icon-button
|
||||
matSuffix
|
||||
[matTooltip]="isDestinationBranchEditable ? 'Guardar' : 'Editar rama destino'"
|
||||
(click)="toggleDestinationBranchEdit()">
|
||||
<mat-icon>{{ isDestinationBranchEditable ? 'save' : 'edit' }}</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repository-warning" *ngIf="repositoryNotFound">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<div class="warning-content">
|
||||
<div class="warning-title">Repositorio no encontrado</div>
|
||||
<div class="warning-message">
|
||||
El repositorio "{{ gitData.repo }}" no existe en el listado de repositorios disponibles.
|
||||
Es posible que necesites crearlo primero.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="git-info-loading" *ngIf="loadingGitData">
|
||||
<span>Cargando información de Git...</span>
|
||||
</div>
|
||||
<div class="git-info-empty" *ngIf="!loadingGitData && !hasValidGitData">
|
||||
<span>No se encontró información de Git para esta partición</span>
|
||||
<div class="git-info-loading" *ngIf="loadingGitData">
|
||||
<span>Cargando información de Git...</span>
|
||||
</div>
|
||||
<div class="git-info-empty" *ngIf="!loadingGitData && !hasValidGitData">
|
||||
<span>No se encontró información de Git para esta partición</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Checkbox para forzar push cuando estamos en modo modificar imagen git -->
|
||||
<div class="force-push-container" *ngIf="imageType === 'git' && hasValidGitData">
|
||||
<mat-checkbox [(ngModel)]="forcePush" class="force-push-checkbox">
|
||||
Opción 'Forzar push'
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="force-push-container" *ngIf="imageType === 'git' && hasValidGitData">
|
||||
<mat-checkbox [(ngModel)]="forcePush" class="force-push-checkbox">
|
||||
Opción 'Forzar push'
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="repository-header" *ngIf="!hasValidGitData">
|
||||
<button *ngIf="imageType === 'git'"
|
||||
class="create-repository-button"
|
||||
|
|
@ -140,80 +178,24 @@
|
|||
<span>Crear nuevo repositorio / SO</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="selector" *ngIf="!hasValidGitData">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Seleccionar repositorio Git</mat-label>
|
||||
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositorySelected($event.value)" required>
|
||||
<mat-option [value]="null">Seleccionar repositorio git / SO</mat-option>
|
||||
<mat-option *ngFor="let repo of gitRepositories" [value]="repo">{{ repo.name }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-spinner *ngIf="loadingGitRepositories" matSuffix diameter="20"></mat-spinner>
|
||||
<mat-hint>
|
||||
<mat-icon>info</mat-icon>
|
||||
Selecciona el repositorio git para obtener las imágenes disponibles.
|
||||
<span *ngIf="gitRepositories.length === 0" class="no-repositories-hint">
|
||||
No hay repositorios disponibles. Crea uno nuevo para continuar.
|
||||
</span>
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<button class="create-branch-button"
|
||||
*ngIf="selectedGitRepository"
|
||||
(click)="openCreateBranchModal()"
|
||||
[disabled]="loadingBranches"
|
||||
matTooltip="Crear nueva rama"
|
||||
style="display: none;">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>Crear rama</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-section" *ngIf="imageType === 'monolithic'">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>image</mat-icon>
|
||||
Configuración de imagen monolítica
|
||||
</div>
|
||||
|
||||
<div class="action-chips-container">
|
||||
<mat-chip-listbox [(ngModel)]="monolithicAction" required class="action-chip-listbox">
|
||||
<mat-chip-option value="create" class="action-chip create-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'create'})">
|
||||
<span>Crear imagen</span>
|
||||
</mat-chip-option>
|
||||
<mat-chip-option value="update" class="action-chip update-chip firmware-chip" (click)="onMonolithicActionSelected({value: 'update'})">
|
||||
<span>Actualizar imagen</span>
|
||||
</mat-chip-option>
|
||||
</mat-chip-listbox>
|
||||
</div>
|
||||
<div class="action-hint">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span *ngIf="monolithicAction === 'create'">Crea una nueva imagen con el nombre especificado</span>
|
||||
<span *ngIf="monolithicAction === 'update'">Actualiza una imagen existente seleccionada</span>
|
||||
</div>
|
||||
|
||||
<div class="selector" *ngIf="monolithicAction === 'create'">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Nombre canónico</mat-label>
|
||||
<input matInput [(ngModel)]="name" placeholder="Nombre canónico. En minúscula y sin espacios" required>
|
||||
<mat-hint>Introduce el nombre para la nueva imagen que se creará.</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="selector" *ngIf="monolithicAction === 'update'">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Seleccione imagen</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required>
|
||||
<mat-option [value]="null">Seleccionar imagen para actualizar</mat-option>
|
||||
<mat-option *ngFor="let image of images" [value]="image">{{ image?.name }}</mat-option>
|
||||
</mat-select>
|
||||
<button *ngIf="selectedImage" mat-icon-button matSuffix aria-label="Clear client search"
|
||||
(click)="selectedImage = null; resetCanonicalName()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint>Selecciona la imagen existente que quieres actualizar.</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="selector" *ngIf="!hasValidGitData">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccionar repositorio Git</mat-label>
|
||||
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositorySelected($event.value)" required>
|
||||
<mat-option [value]="null">Seleccionar repositorio git / SO</mat-option>
|
||||
<mat-option *ngFor="let repo of gitRepositories" [value]="repo">{{ repo.name }}</mat-option>
|
||||
</mat-select>
|
||||
<mat-spinner *ngIf="loadingGitRepositories" matSuffix diameter="20"></mat-spinner>
|
||||
<mat-hint>
|
||||
<mat-icon>info</mat-icon>
|
||||
Selecciona el repositorio git para obtener las imágenes disponibles.
|
||||
<span *ngIf="gitRepositories.length === 0" class="no-repositories-hint">
|
||||
No hay repositorios disponibles. Crea uno nuevo para continuar.
|
||||
</span>
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px 32px;
|
||||
padding: 16px 24px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
|
|
@ -55,37 +55,71 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
background: #e3f2fd !important;
|
||||
color: #1976d2 !important;
|
||||
}
|
||||
|
||||
.secondary-button:hover:not(:disabled) {
|
||||
background: #bbdefb !important;
|
||||
}
|
||||
|
||||
/* Contenedor principal */
|
||||
.select-container {
|
||||
background: white !important;
|
||||
margin-top: 20px;
|
||||
margin-top: 12px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
padding: 0 20px 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.config-container {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Secciones del formulario */
|
||||
.form-section {
|
||||
background: white !important;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.compact-section {
|
||||
padding: 20px !important;
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 20px;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
padding-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #f8f9fa;
|
||||
}
|
||||
|
||||
/* Layout horizontal: imagen + partición */
|
||||
.deploy-config-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.image-config-section {
|
||||
flex: 0 0 320px;
|
||||
}
|
||||
|
||||
.partition-select-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.form-section-title mat-icon {
|
||||
color: #667eea;
|
||||
font-size: 24px;
|
||||
|
|
@ -93,14 +127,6 @@
|
|||
height: 24px;
|
||||
}
|
||||
|
||||
/* Selectores */
|
||||
.selector {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
|
|
@ -276,9 +302,9 @@ mat-form-field {
|
|||
/* Tabla de particiones */
|
||||
.partition-table-container {
|
||||
background: white !important;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-top: 20px;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
|
|
@ -296,24 +322,48 @@ table {
|
|||
background: white !important;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
padding: 16px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-cell {
|
||||
padding: 16px;
|
||||
padding: 8px 12px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Opciones avanzadas */
|
||||
.input-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
/* Resumen de configuración avanzada */
|
||||
.method-config-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: #f0f4ff;
|
||||
border: 1px solid #d0d9ff;
|
||||
border-radius: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
.summary-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #667eea;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Mensajes de error */
|
||||
|
|
@ -568,25 +618,13 @@ table {
|
|||
gap: 40px;
|
||||
}
|
||||
|
||||
.monolithic-row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.deploy-config-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.monolithic-row .half-width {
|
||||
flex: 1 1 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.monolithic-row .full-width {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.monolithic-row .full-width {
|
||||
flex: 2;
|
||||
.image-config-section {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,15 +19,9 @@
|
|||
<button class="action-button" id="execute-button"
|
||||
[disabled]="!isFormValid()"
|
||||
(click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="generateOgInstructions()">
|
||||
<button class="action-button secondary-button" (click)="generateOgInstructions()">
|
||||
Generar instrucciones
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" color="accent"
|
||||
[disabled]="!isFormValid()"
|
||||
(click)="openScheduleModal()">
|
||||
|
|
@ -95,58 +89,143 @@
|
|||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<mat-divider style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
<!-- Sección: Configuración de imagen -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>image</mat-icon>
|
||||
Configuración de imagen
|
||||
</div>
|
||||
|
||||
<div class="selector">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<div class="select-container config-container">
|
||||
<div class="deploy-config-row">
|
||||
<!-- Sección: Configuración de imagen -->
|
||||
<div class="form-section compact-section image-config-section">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>image</mat-icon>
|
||||
Configuración de imagen
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Tipo de imagen</mat-label>
|
||||
<mat-select [(ngModel)]="imageType" (selectionChange)="onImageTypeSelected($event.value)">
|
||||
<mat-option [value]="'monolithic'">Monolítica</mat-option>
|
||||
<mat-option [value]="'git'">Git</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Campos monolítica -->
|
||||
<div *ngIf="imageType === 'monolithic'">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Método de deploy</mat-label>
|
||||
<mat-select [(ngModel)]="selectedMethod" name="selectedMethod" (selectionChange)="validateImageSize()">
|
||||
<mat-option *ngFor="let method of allMethods" [value]="method.value">{{ method.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione imagen</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage" [disabled]="!imageType">
|
||||
<mat-option *ngFor="let image of images" [value]="image">
|
||||
<div class="unit-name">{{ image.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ image.description }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Campos Git -->
|
||||
<div *ngIf="imageType === 'git'">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Repositorio</mat-label>
|
||||
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositoryChange($event.value)" [disabled]="loadingRepositories">
|
||||
<mat-option *ngFor="let repo of repositories" [value]="repo">
|
||||
{{ repo }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon matSuffix *ngIf="loadingRepositories">hourglass_empty</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Rama</mat-label>
|
||||
<mat-select [(ngModel)]="selectedBranch" (selectionChange)="onGitBranchChange()" [disabled]="loadingBranches || !selectedRepository">
|
||||
<mat-option *ngFor="let branch of branches" [value]="branch">
|
||||
{{ branch }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon matSuffix *ngIf="loadingBranches">hourglass_empty</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Filtrar por tag</mat-label>
|
||||
<mat-select [(ngModel)]="showOnlyTagged" (selectionChange)="onTagFilterChange()" [disabled]="!commits.length">
|
||||
<mat-option [value]="false">No</mat-option>
|
||||
<mat-option [value]="true">Sí</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="methodConfigured && selectedMethod && needsAdvancedConfig(selectedMethod)" class="method-config-summary">
|
||||
<div class="summary-content">
|
||||
<mat-icon class="summary-icon">tune</mat-icon>
|
||||
<span class="summary-text">{{ getMethodConfigSummary() }}</span>
|
||||
</div>
|
||||
<button mat-icon-button matTooltip="Editar configuración" (click)="openMethodConfigDialog()">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selectores Git (solo si imageType === 'git') -->
|
||||
<div *ngIf="imageType === 'git'" class="filters-row git-gap">
|
||||
<mat-form-field appearance="fill" style="width: 300px;">
|
||||
<mat-label>Seleccionar Repositorio</mat-label>
|
||||
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositoryChange($event.value)" [disabled]="loadingRepositories">
|
||||
<mat-option *ngFor="let repo of repositories" [value]="repo">
|
||||
{{ repo }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon matSuffix *ngIf="loadingRepositories">hourglass_empty</mat-icon>
|
||||
</mat-form-field>
|
||||
<!-- Sección: Selección de partición -->
|
||||
<div class="form-section compact-section partition-select-section" id="partition-selection">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>storage</mat-icon>
|
||||
Selección de partición
|
||||
</div>
|
||||
|
||||
<div class="partition-table-container">
|
||||
<div *ngIf="showInstructions" class="instructions-box">
|
||||
<mat-card class="instructions-card">
|
||||
<mat-card-title>
|
||||
Instrucciones generadas
|
||||
<button mat-icon-button (click)="showInstructions = false" style="float: right;">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<pre>{{ ogInstructions }}</pre>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="fill" style="width: 300px;">
|
||||
<mat-label>Seleccionar Rama</mat-label>
|
||||
<mat-select [(ngModel)]="selectedBranch" (selectionChange)="onGitBranchChange()" [disabled]="loadingBranches || !selectedRepository">
|
||||
<mat-option *ngFor="let branch of branches" [value]="branch">
|
||||
{{ branch }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-icon matSuffix *ngIf="loadingBranches">hourglass_empty</mat-icon>
|
||||
</mat-form-field>
|
||||
<table mat-table [dataSource]="filteredPartitions">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: start">Seleccionar</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group [(ngModel)]="selectedPartition" name="selectedPartition">
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<mat-form-field appearance="fill" style="width: 200px;">
|
||||
<mat-label>Tag</mat-label>
|
||||
<mat-select [(ngModel)]="showOnlyTagged" (selectionChange)="onTagFilterChange()" [disabled]="!commits.length">
|
||||
<mat-option [value]="false">No</mat-option>
|
||||
<mat-option [value]="true">Sí</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
{{ column.cell(image) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de commits Git (solo cuando hay commits cargados) -->
|
||||
<div *ngIf="imageType === 'git' && filteredCommits.length > 0" class="form-section compact-section">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>history</mat-icon>
|
||||
Selección de commit
|
||||
</div>
|
||||
|
||||
<div *ngIf="imageType === 'git' && filteredCommits.length > 0" class="commits-table-container">
|
||||
<div class="commits-table-container">
|
||||
<table mat-table [dataSource]="filteredCommits" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>Seleccionar</th>
|
||||
|
|
@ -188,144 +267,8 @@
|
|||
<tr mat-row *matRowDef="let row; columns: ['select','hexsha','message','committed_date','tags'];"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="monolithic-row" *ngIf="imageType === 'monolithic'">
|
||||
<mat-form-field appearance="fill" class="half-width">
|
||||
<mat-label>Seleccione método de deploy</mat-label>
|
||||
<mat-select [(ngModel)]="selectedMethod" name="selectedMethod" (selectionChange)="validateImageSize()">
|
||||
<mat-option *ngFor="let method of allMethods" [value]="method.value">{{ method.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione imagen</mat-label>
|
||||
<mat-select [(ngModel)]="selectedImage" name="selectedImage" [disabled] = "!imageType" >
|
||||
<mat-option *ngFor="let image of images" [value]="image">
|
||||
<div class="unit-name"> {{ image.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ image.description }}</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección: Selección de partición -->
|
||||
<div class="form-section" id="partition-selection">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>storage</mat-icon>
|
||||
Selección de partición
|
||||
</div>
|
||||
|
||||
<div class="partition-table-container">
|
||||
<div *ngIf="showInstructions" class="instructions-box">
|
||||
<mat-card class="instructions-card">
|
||||
<mat-card-title>
|
||||
Instrucciones generadas
|
||||
<button mat-icon-button (click)="showInstructions = false" style="float: right;">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<pre>{{ ogInstructions }}</pre>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="filteredPartitions">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: start">Seleccionar partición</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group [(ngModel)]="selectedPartition" name="selectedPartition">
|
||||
<mat-radio-button [value]="row">
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
{{ column.cell(image) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sección: Opciones avanzadas -->
|
||||
<div class="form-section" id="advanced-options" *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct') || isMethod('p2p')">
|
||||
<div class="form-section-title">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">Opciones multicast</span>
|
||||
<span *ngIf="isMethod('p2p')">Opciones torrent</span>
|
||||
</div>
|
||||
|
||||
<div class="input-group" *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Puerto</mat-label>
|
||||
<input matInput [(ngModel)]="mcastPort" name="mcastPort" type="number"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Dirección</mat-label>
|
||||
<input matInput [(ngModel)]="mcastIp" name="mcastIp"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label i18n="@@mcastModeLabel">Modo Multicast</mat-label>
|
||||
<mat-select [(ngModel)]="mcastMode" name="mcastMode"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Velocidad</mat-label>
|
||||
<input matInput [(ngModel)]="mcastSpeed" name="mcastSpeed" type="number"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Máximo Clientes</mat-label>
|
||||
<input matInput [(ngModel)]="mcastMaxClients" name="mcastMaxClients" type="number"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Tiempo Máximo de Espera</mat-label>
|
||||
<input matInput [(ngModel)]="mcastMaxTime" name="mcastMaxTime" type="number"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="input-group" *ngIf="isMethod('p2p')">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
|
||||
<mat-select [(ngModel)]="p2pMode" name="p2pMode"
|
||||
[required]="isMethod('p2p')">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field" *ngIf="p2pMode === 'seeder'">
|
||||
<mat-label>Semilla (minutos)</mat-label>
|
||||
<input matInput [(ngModel)]="p2pTime" name="p2pTime" type="number"
|
||||
[required]="isMethod('p2p') && p2pMode === 'seeder'">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-scroll-to-top
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ConfigService } from '@services/config.service';
|
|||
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
|
||||
import {DeployMethodConfigDialogComponent} from "./deploy-method-config-dialog/deploy-method-config-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-deploy-image',
|
||||
|
|
@ -111,6 +112,7 @@ export class DeployImageComponent implements OnInit{
|
|||
|
||||
showOnlyTagged: boolean = false;
|
||||
filteredCommits: any[] = [];
|
||||
methodConfigured: boolean = false;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
|
|
@ -152,9 +154,6 @@ export class DeployImageComponent implements OnInit{
|
|||
this.loadGitRepositories();
|
||||
}
|
||||
this.loadImages();
|
||||
setTimeout(() => {
|
||||
this.scrollToPartitionSection();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
get runScriptTitle(): string {
|
||||
|
|
@ -178,9 +177,6 @@ export class DeployImageComponent implements OnInit{
|
|||
|
||||
onModelSelected(client: any) {
|
||||
this.loadPartitions(client);
|
||||
setTimeout(() => {
|
||||
this.scrollToPartitionSection();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
toggleClientSelection(client: any) {
|
||||
|
|
@ -270,22 +266,74 @@ export class DeployImageComponent implements OnInit{
|
|||
validateImageSize() {
|
||||
if (this.selectedImage && this.selectedPartition) {
|
||||
if ((this.selectedImage.datasize / 1024) / 1024 > this.selectedPartition.size) {
|
||||
|
||||
this.errorMessage = "El tamaño de la imagen seleccionada excede el tamaño de la partición.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.errorMessage = "";
|
||||
|
||||
if (this.selectedMethod) {
|
||||
setTimeout(() => {
|
||||
this.scrollToPartitionSection();
|
||||
}, 300);
|
||||
|
||||
if (this.selectedMethod && this.needsAdvancedConfig(this.selectedMethod)) {
|
||||
this.openMethodConfigDialog();
|
||||
} else {
|
||||
this.methodConfigured = false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
needsAdvancedConfig(method: string): boolean {
|
||||
return ['udpcast', 'uftp', 'udpcast-direct', 'p2p'].includes(method);
|
||||
}
|
||||
|
||||
openMethodConfigDialog(): void {
|
||||
const dialogRef = this.dialog.open(DeployMethodConfigDialogComponent, {
|
||||
width: '540px',
|
||||
disableClose: false,
|
||||
data: {
|
||||
method: this.selectedMethod,
|
||||
mcastIp: this.mcastIp,
|
||||
mcastPort: this.mcastPort,
|
||||
mcastMode: this.mcastMode,
|
||||
mcastSpeed: this.mcastSpeed,
|
||||
mcastMaxClients: this.mcastMaxClients,
|
||||
mcastMaxTime: this.mcastMaxTime,
|
||||
p2pMode: this.p2pMode,
|
||||
p2pTime: this.p2pTime,
|
||||
multicastModeOptions: this.multicastModeOptions,
|
||||
p2pModeOptions: this.p2pModeOptions,
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.mcastIp = result.mcastIp;
|
||||
this.mcastPort = result.mcastPort;
|
||||
this.mcastMode = result.mcastMode;
|
||||
this.mcastSpeed = result.mcastSpeed;
|
||||
this.mcastMaxClients = result.mcastMaxClients;
|
||||
this.mcastMaxTime = result.mcastMaxTime;
|
||||
this.p2pMode = result.p2pMode;
|
||||
this.p2pTime = result.p2pTime;
|
||||
this.methodConfigured = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getMethodConfigSummary(): string {
|
||||
if (!this.selectedMethod) return '';
|
||||
if (['udpcast', 'uftp', 'udpcast-direct'].includes(this.selectedMethod)) {
|
||||
return `${this.mcastIp}:${this.mcastPort} · ${this.mcastMode} · ${this.mcastSpeed} Mbps`;
|
||||
}
|
||||
if (this.selectedMethod === 'p2p') {
|
||||
let summary = this.p2pMode || '';
|
||||
if (this.p2pMode === 'seeder' && this.p2pTime) {
|
||||
summary += ` · ${this.p2pTime} min`;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
save(): void {
|
||||
|
||||
if (!this.selectedClients.length) {
|
||||
|
|
@ -519,28 +567,7 @@ export class DeployImageComponent implements OnInit{
|
|||
this.ogInstructions = `og-deploy-image --image ${this.selectedImage.name} --partition ${this.selectedPartition.partitionNumber} --method ${this.selectedMethod}`;
|
||||
}
|
||||
|
||||
scrollToPartitionSection() {
|
||||
const partitionSection = document.getElementById('partition-selection');
|
||||
if (partitionSection) {
|
||||
partitionSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
console.log('Scroll ejecutado');
|
||||
}
|
||||
}
|
||||
|
||||
scrollToAdvancedOptions() {
|
||||
const advancedOptions = document.getElementById('advanced-options');
|
||||
|
||||
if (advancedOptions) {
|
||||
advancedOptions.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
console.log('Scroll hacia opciones avanzadas ejecutado');
|
||||
}
|
||||
}
|
||||
|
||||
get selectedPartition(): any {
|
||||
return this._selectedPartition;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
h2[mat-dialog-title] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 0 0 8px 0;
|
||||
padding: 20px 24px 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.dialog-title-icon {
|
||||
color: #667eea;
|
||||
font-size: 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px 20px;
|
||||
padding: 16px 0 8px;
|
||||
}
|
||||
|
||||
mat-dialog-content {
|
||||
min-width: 460px;
|
||||
padding: 8px 24px !important;
|
||||
}
|
||||
|
||||
mat-dialog-actions {
|
||||
padding: 16px 24px 20px !important;
|
||||
margin: 0 !important;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-dialog-actions button {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
mat-dialog-content {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<h2 mat-dialog-title>
|
||||
<mat-icon class="dialog-title-icon">settings</mat-icon>
|
||||
Configuración de {{ getMethodLabel() }}
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<!-- Multicast options -->
|
||||
<div *ngIf="isMulticast()" class="config-grid">
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Dirección</mat-label>
|
||||
<input matInput [(ngModel)]="mcastIp" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Puerto</mat-label>
|
||||
<input matInput [(ngModel)]="mcastPort" type="number" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Modo Multicast</mat-label>
|
||||
<mat-select [(ngModel)]="mcastMode" required>
|
||||
<mat-option *ngFor="let option of data.multicastModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Velocidad</mat-label>
|
||||
<input matInput [(ngModel)]="mcastSpeed" type="number" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Máx. Clientes</mat-label>
|
||||
<input matInput [(ngModel)]="mcastMaxClients" type="number" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Tiempo máx. espera</mat-label>
|
||||
<input matInput [(ngModel)]="mcastMaxTime" type="number" required>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- P2P options -->
|
||||
<div *ngIf="isP2P()" class="config-grid">
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Modo P2P</mat-label>
|
||||
<mat-select [(ngModel)]="p2pMode" required>
|
||||
<mat-option *ngFor="let option of data.p2pModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" *ngIf="p2pMode === 'seeder'">
|
||||
<mat-label>Semilla (minutos)</mat-label>
|
||||
<input matInput [(ngModel)]="p2pTime" type="number" required>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="onCancel()">Cancelar</button>
|
||||
<button mat-flat-button color="primary" [disabled]="!isValid()" (click)="onSave()">
|
||||
Guardar configuración
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
export interface DeployMethodConfigData {
|
||||
method: string;
|
||||
mcastIp: string;
|
||||
mcastPort: number;
|
||||
mcastMode: string;
|
||||
mcastSpeed: number;
|
||||
mcastMaxClients: number;
|
||||
mcastMaxTime: number;
|
||||
p2pMode: string;
|
||||
p2pTime: number;
|
||||
multicastModeOptions: { name: string; value: string }[];
|
||||
p2pModeOptions: { name: string; value: string }[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-deploy-method-config-dialog',
|
||||
templateUrl: './deploy-method-config-dialog.component.html',
|
||||
styleUrls: ['./deploy-method-config-dialog.component.css']
|
||||
})
|
||||
export class DeployMethodConfigDialogComponent {
|
||||
mcastIp: string;
|
||||
mcastPort: number;
|
||||
mcastMode: string;
|
||||
mcastSpeed: number;
|
||||
mcastMaxClients: number;
|
||||
mcastMaxTime: number;
|
||||
p2pMode: string;
|
||||
p2pTime: number;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DeployMethodConfigDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: DeployMethodConfigData
|
||||
) {
|
||||
this.mcastIp = data.mcastIp;
|
||||
this.mcastPort = data.mcastPort;
|
||||
this.mcastMode = data.mcastMode;
|
||||
this.mcastSpeed = data.mcastSpeed;
|
||||
this.mcastMaxClients = data.mcastMaxClients;
|
||||
this.mcastMaxTime = data.mcastMaxTime;
|
||||
this.p2pMode = data.p2pMode;
|
||||
this.p2pTime = data.p2pTime;
|
||||
}
|
||||
|
||||
isMulticast(): boolean {
|
||||
return ['udpcast', 'uftp', 'udpcast-direct'].includes(this.data.method);
|
||||
}
|
||||
|
||||
isP2P(): boolean {
|
||||
return this.data.method === 'p2p';
|
||||
}
|
||||
|
||||
getMethodLabel(): string {
|
||||
const labels: Record<string, string> = {
|
||||
'udpcast': 'Multicast',
|
||||
'uftp': 'UFTP',
|
||||
'udpcast-direct': 'Multicast (directo)',
|
||||
'p2p': 'Torrent (P2P)',
|
||||
};
|
||||
return labels[this.data.method] || this.data.method;
|
||||
}
|
||||
|
||||
isValid(): boolean {
|
||||
if (this.isMulticast()) {
|
||||
return !!(this.mcastPort && this.mcastIp && this.mcastMode && this.mcastSpeed && this.mcastMaxClients && this.mcastMaxTime);
|
||||
}
|
||||
if (this.isP2P()) {
|
||||
if (!this.p2pMode) return false;
|
||||
if (this.p2pMode === 'seeder' && !this.p2pTime) return false;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onSave(): void {
|
||||
this.dialogRef.close({
|
||||
mcastIp: this.mcastIp,
|
||||
mcastPort: this.mcastPort,
|
||||
mcastMode: this.mcastMode,
|
||||
mcastSpeed: this.mcastSpeed,
|
||||
mcastMaxClients: this.mcastMaxClients,
|
||||
mcastMaxTime: this.mcastMaxTime,
|
||||
p2pMode: this.p2pMode,
|
||||
p2pTime: this.p2pTime,
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,21 @@
|
|||
.partition-assistant {
|
||||
padding: 32px;
|
||||
margin: 20px;
|
||||
padding: 16px 20px;
|
||||
margin: 0 20px 20px;
|
||||
background: white !important;
|
||||
border-radius: 16px;
|
||||
border-radius: 0 0 16px 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #bbdefb;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px 32px;
|
||||
padding: 16px 24px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
|
|
@ -114,76 +115,138 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
background: #e3f2fd !important;
|
||||
color: #1976d2 !important;
|
||||
}
|
||||
|
||||
.secondary-button:hover:not(:disabled) {
|
||||
background: #bbdefb !important;
|
||||
}
|
||||
|
||||
.row-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.disk-selector-card {
|
||||
padding: 32px;
|
||||
margin: 20px;
|
||||
background: white !important;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #bbdefb;
|
||||
overflow: hidden;
|
||||
.disk-and-partitions-container {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
.disk-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24px 32px;
|
||||
background: transparent;
|
||||
color: #1976d2;
|
||||
border-bottom: 1.5px solid #1976d2;
|
||||
gap: 16px;
|
||||
padding: 12px 20px;
|
||||
background: white;
|
||||
border-radius: 16px 16px 0 0;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #bbdefb;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 16px;
|
||||
color: #1976d2;
|
||||
.disk-toolbar-select {
|
||||
flex: 0 0 280px;
|
||||
margin-bottom: -1.25em;
|
||||
}
|
||||
|
||||
.card-title h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 20px;
|
||||
.disk-toolbar-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.disk-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.disk-stat-label {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.disk-stat-value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-title p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
color: #1976d2;
|
||||
.disk-stat-value.used {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 24px 32px;
|
||||
.disk-stat-value.free {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.disk-select-field {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.selected-disk-hint {
|
||||
.disk-usage-bar-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #2e7d32;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.hint-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #4caf50;
|
||||
.disk-usage-bar-inline .usage-bar-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.usage-percent-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.disk-toolbar-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
background: #f0f4ff;
|
||||
color: #495057;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid #d0d9ff;
|
||||
}
|
||||
|
||||
.meta-chip mat-icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.add-partition-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
white-space: nowrap;
|
||||
margin: 0 !important;
|
||||
padding: 8px 16px !important;
|
||||
}
|
||||
|
||||
.add-partition-btn mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Opciones del select */
|
||||
|
|
@ -386,11 +449,11 @@
|
|||
}
|
||||
|
||||
.select-container {
|
||||
margin-top: 20px;
|
||||
margin-top: 12px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
padding: 0 20px 12px;
|
||||
}
|
||||
|
||||
/* Expansion panel */
|
||||
|
|
@ -503,8 +566,8 @@
|
|||
}
|
||||
|
||||
.form-container {
|
||||
flex: 0 0 65%;
|
||||
max-width: 65%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 20px;
|
||||
box-sizing: border-box;
|
||||
background: white;
|
||||
|
|
@ -512,73 +575,6 @@
|
|||
border: none;
|
||||
}
|
||||
|
||||
.disk-space-info-container {
|
||||
margin: 24px 0;
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.disk-space-card {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.space-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.space-icon {
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.used-icon {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.free-icon {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.total-icon {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.space-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.space-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.space-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.disk-usage-bar {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.usage-bar-container {
|
||||
width: 100%;
|
||||
|
|
@ -596,13 +592,6 @@
|
|||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.usage-percentage {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.partition-table {
|
||||
width: 100%;
|
||||
|
|
@ -656,10 +645,9 @@
|
|||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 0 0 35%;
|
||||
max-width: 35%;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
flex: 0 0 280px;
|
||||
max-width: 280px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
|
|
@ -1263,13 +1251,17 @@
|
|||
padding: 20px;
|
||||
}
|
||||
|
||||
.disk-space-card {
|
||||
.disk-toolbar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.space-info-item {
|
||||
min-width: auto;
|
||||
|
||||
.disk-toolbar-select {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.disk-toolbar-info {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row {
|
||||
|
|
@ -1293,7 +1285,7 @@
|
|||
}
|
||||
|
||||
.partition-validation-indicator {
|
||||
margin: 16px 0;
|
||||
margin: 8px 0;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
|
@ -1432,45 +1424,6 @@
|
|||
::ng-deep .model-selector .mat-radio-outer-circle { border-color: #667eea !important; }
|
||||
::ng-deep .model-selector .mat-radio-inner-circle { background-color: #667eea !important; }
|
||||
|
||||
/* Overrides: info badges sin fondo ni borde, estilo neutro */
|
||||
.info-badge {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.info-badge:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.info-badge:first-of-type,
|
||||
.info-badge:last-of-type {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.info-badge:first-of-type .info-icon,
|
||||
.info-badge:last-of-type .info-icon,
|
||||
.info-badge:first-of-type .info-label,
|
||||
.info-badge:last-of-type .info-label,
|
||||
.info-badge:first-of-type .info-value,
|
||||
.info-badge:last-of-type .info-value {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Colores neutros en badges */
|
||||
.info-badge .info-label {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-badge .info-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-badge .info-icon {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -17,15 +17,9 @@
|
|||
</div>
|
||||
<div class="button-row">
|
||||
<button class="action-button" [disabled]="!selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0 || partitionValidationStatus === 'error'" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="generateInstructions()">
|
||||
<button class="action-button secondary-button" (click)="generateInstructions()">
|
||||
Generar instrucciones
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" color="accent" [disabled]="!selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="openScheduleModal()">
|
||||
Opciones de programación
|
||||
</button>
|
||||
|
|
@ -108,18 +102,10 @@
|
|||
|
||||
|
||||
<mat-dialog-content>
|
||||
<div class="disk-selector-card" id="disk-selector">
|
||||
<div class="card-header">
|
||||
<mat-icon class="card-icon">storage</mat-icon>
|
||||
<div class="card-title">
|
||||
<h3>Selección de Disco</h3>
|
||||
<p>Elige el disco donde se realizarán las operaciones de particionado</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<mat-form-field appearance="fill" class="disk-select-field">
|
||||
<mat-label>Seleccionar disco</mat-label>
|
||||
<div class="disk-and-partitions-container" id="disk-selector">
|
||||
<div class="disk-toolbar">
|
||||
<mat-form-field appearance="fill" class="disk-toolbar-select">
|
||||
<mat-label>Disco</mat-label>
|
||||
<mat-select [(ngModel)]="selectedDiskNumber" (selectionChange)="onDiskSelectionChange()">
|
||||
<mat-option *ngFor="let disk of disks" [value]="disk.diskNumber">
|
||||
<div class="disk-option">
|
||||
|
|
@ -133,84 +119,57 @@
|
|||
</div>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-hint *ngIf="!selectedDisk">Selecciona el disco que deseas particionar</mat-hint>
|
||||
<mat-hint *ngIf="selectedDisk" class="selected-disk-hint">
|
||||
<mat-icon class="hint-icon">check_circle</mat-icon>
|
||||
Disco {{ selectedDisk.diskNumber }} seleccionado - {{ (selectedDisk.totalDiskSize / 1024).toFixed(2) }} GB
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="no-disks-message" *ngIf="!disks || disks.length === 0">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<div class="message-text">
|
||||
<span class="message-title">No hay discos disponibles</span>
|
||||
<span class="message-subtitle">Asegúrate de que el cliente modelo tenga discos configurados</span>
|
||||
|
||||
<div class="disk-toolbar-info" *ngIf="selectedDisk">
|
||||
<div class="disk-stat">
|
||||
<span class="disk-stat-label">Usado</span>
|
||||
<span class="disk-stat-value used">{{ selectedDisk.used | number:'1.0-0' }} MB</span>
|
||||
</div>
|
||||
<div class="disk-stat">
|
||||
<span class="disk-stat-label">Libre</span>
|
||||
<span class="disk-stat-value free" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">{{ (selectedDisk.totalDiskSize - selectedDisk.used) | number:'1.0-0' }} MB</span>
|
||||
</div>
|
||||
<div class="disk-stat">
|
||||
<span class="disk-stat-label">Total</span>
|
||||
<span class="disk-stat-value">{{ selectedDisk.totalDiskSize | number:'1.0-0' }} MB</span>
|
||||
</div>
|
||||
<div class="disk-usage-bar-inline">
|
||||
<div class="usage-bar-container">
|
||||
<div class="usage-bar-fill"
|
||||
[style.width.%]="(selectedDisk.used / selectedDisk.totalDiskSize) * 100"
|
||||
[ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}"></div>
|
||||
</div>
|
||||
<span class="usage-percent-label" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">{{ ((selectedDisk.used / selectedDisk.totalDiskSize) * 100) | number:'1.0-0' }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="disk-toolbar-meta" *ngIf="selectedDisk">
|
||||
<span class="meta-chip" *ngIf="selectedModelClient?.firmwareType">
|
||||
<mat-icon>memory</mat-icon> {{ selectedModelClient.firmwareType }}
|
||||
</span>
|
||||
<span class="meta-chip" *ngIf="partitionCode">
|
||||
<mat-icon>storage</mat-icon> {{ partitionCode }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button class="action-button add-partition-btn" *ngIf="selectedDisk" (click)="addPartition(selectedDisk.diskNumber)">
|
||||
<mat-icon>add</mat-icon> Añadir partición
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="no-disks-message" *ngIf="!disks || disks.length === 0">
|
||||
<mat-icon class="warning-icon">warning</mat-icon>
|
||||
<div class="message-text">
|
||||
<span class="message-title">No hay discos disponibles</span>
|
||||
<span class="message-subtitle">Asegúrate de que el cliente modelo tenga discos configurados</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="partition-assistant" *ngIf="selectedDisk">
|
||||
<div class="row-button">
|
||||
<button class="action-button" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
|
||||
|
||||
<div class="info-badge" *ngIf="selectedModelClient.firmwareType">
|
||||
<mat-icon class="info-icon">memory</mat-icon>
|
||||
<div class="info-content">
|
||||
<span class="info-label">Firmware</span>
|
||||
<span class="info-value">{{ selectedModelClient.firmwareType }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-badge" *ngIf="partitionCode">
|
||||
<mat-icon class="info-icon">storage</mat-icon>
|
||||
<div class="info-content">
|
||||
<span class="info-label">Tabla de particiones</span>
|
||||
<span class="info-value">{{ partitionCode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-divider style="padding: 10px;"></mat-divider>
|
||||
|
||||
<div class="row">
|
||||
<div class="form-container">
|
||||
<div class="disk-space-info-container" id="disk-info">
|
||||
<div class="disk-space-card">
|
||||
<div class="space-info-item">
|
||||
<mat-icon class="space-icon used-icon">storage</mat-icon>
|
||||
<div class="space-details">
|
||||
<span class="space-label">Espacio usado</span>
|
||||
<span class="space-value">{{ selectedDisk.used | number:'1.2-2' }} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-info-item">
|
||||
<mat-icon class="space-icon free-icon" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">cloud_done</mat-icon>
|
||||
<div class="space-details">
|
||||
<span class="space-label">Espacio libre</span>
|
||||
<span class="space-value" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">{{ (selectedDisk.totalDiskSize - selectedDisk.used) | number:'1.2-2' }} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-info-item">
|
||||
<mat-icon class="space-icon total-icon">dns</mat-icon>
|
||||
<div class="space-details">
|
||||
<span class="space-label">Espacio total</span>
|
||||
<span class="space-value">{{ selectedDisk.totalDiskSize | number:'1.2-2' }} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="disk-usage-bar">
|
||||
<div class="usage-bar-container">
|
||||
<div class="usage-bar-fill"
|
||||
[style.width.%]="(selectedDisk.used / selectedDisk.totalDiskSize) * 100"
|
||||
[ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}"></div>
|
||||
</div>
|
||||
<span class="usage-percentage" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">{{ ((selectedDisk.used / selectedDisk.totalDiskSize) * 100) | number:'1.1-1' }}% usado</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Indicador de validación de particiones -->
|
||||
<div class="partition-validation-indicator" *ngIf="selectedDisk && selectedDisk.partitions.length > 0">
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ table {
|
|||
padding: 24px 32px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
|
@ -389,13 +390,13 @@ table {
|
|||
.new-command-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
background-color: #eaeff6;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 15px;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.new-command-container mat-form-field {
|
||||
|
|
@ -407,43 +408,10 @@ table {
|
|||
resize: vertical;
|
||||
}
|
||||
|
||||
.new-command-container .action-button {
|
||||
align-self: flex-end;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Secciones del formulario */
|
||||
.form-section {
|
||||
background: white !important;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #bbdefb;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-section-title mat-icon {
|
||||
color: #2196f3;
|
||||
}
|
||||
.toggle-options {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
</h2>
|
||||
<div class="destination-info">
|
||||
<div class="destination-badge">
|
||||
<mat-icon class="destination-icon">cloud_download</mat-icon>
|
||||
<mat-icon class="destination-icon">terminal</mat-icon>
|
||||
<div class="destination-content">
|
||||
<span class="destination-label">Destino</span>
|
||||
<span class="destination-value">{{ runScriptTitle }}</span>
|
||||
|
|
@ -25,7 +25,6 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="select-container">
|
||||
<mat-expansion-panel>
|
||||
|
|
@ -65,7 +64,7 @@
|
|||
class="client-image" />
|
||||
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name }}</span>
|
||||
<span class="client-name">{{ client.name | slice:0:20 }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
</div>
|
||||
|
|
@ -83,6 +82,12 @@
|
|||
Configuración de script
|
||||
</div>
|
||||
|
||||
<div class="run-as-admin-container">
|
||||
<mat-checkbox [(ngModel)]="runAsAdmin" color="primary">
|
||||
Ejecutar como administrador
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<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'">
|
||||
|
|
@ -190,7 +195,7 @@
|
|||
|
||||
<mat-card-content>
|
||||
<div class="script-content-wrapper">
|
||||
<div class="script-preview" [innerHTML]="scriptContent"></div>
|
||||
<pre class="script-preview">{{ scriptContent }}</pre>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export class RunScriptAssistantComponent implements OnInit{
|
|||
parameterNames: string[] = Object.keys(this.parameters);
|
||||
runScriptContext: any = null;
|
||||
newScriptParameters: string[] = [];
|
||||
runAsAdmin: boolean = false;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
|
|
@ -200,7 +201,8 @@ export class RunScriptAssistantComponent implements OnInit{
|
|||
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
|
||||
queue: result,
|
||||
runAsAdmin: this.runAsAdmin
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Script ejecutado correctamente');
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
|
|
@ -125,18 +125,19 @@
|
|||
|
||||
.groups-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
gap: 8px;
|
||||
padding-right: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.clients-view-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-top: 0.25rem;
|
||||
align-items: center;
|
||||
padding-right: 1rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
|
@ -147,10 +148,10 @@
|
|||
}
|
||||
|
||||
.clients-title-name {
|
||||
font-size: x-large;
|
||||
font-size: 1.15rem;
|
||||
display: block;
|
||||
padding: 1rem 1rem 1rem 13px;
|
||||
margin-left: 0.6rem;
|
||||
padding: 0.5rem 0.5rem 0.5rem 12px;
|
||||
margin-left: 0.4rem;
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
@ -189,6 +190,10 @@
|
|||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.cards-select-all-inline {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.list-view {
|
||||
overflow-x: auto;
|
||||
display: flex;
|
||||
|
|
@ -229,13 +234,11 @@
|
|||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
justify-content: flex-end;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.cards-paginator {
|
||||
width: 100%;
|
||||
margin-left: 1rem;
|
||||
background-color: #fafafa;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
|
@ -422,7 +425,8 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
.filters-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1em;
|
||||
padding: 8px 12px;
|
||||
gap: 2px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
|
@ -433,6 +437,24 @@ mat-tree mat-tree-node.disabled:hover {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::ng-deep .filters-container .mat-form-field-wrapper {
|
||||
padding-bottom: 4px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
::ng-deep .filters-container .mat-form-field-infix {
|
||||
padding: 6px 0 !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
::ng-deep .filters-container .mat-form-field-appearance-outline .mat-form-field-outline {
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
::ng-deep .filters-container .mat-form-field-label {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.filters-panel {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -962,9 +984,9 @@ mat-button-toggle-group {
|
|||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 10px;
|
||||
padding: 0.5rem 1rem 1rem 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(135px, 1fr));
|
||||
gap: 8px;
|
||||
padding: 0.25rem 0.75rem 0.75rem 0.75rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
</h2>
|
||||
</div>
|
||||
<div class="groups-button-row">
|
||||
<div joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}" style="display: flex; gap: 15px;">
|
||||
<div joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}" style="display: flex; gap: 8px;">
|
||||
<button class="action-button" (click)="addOU($event)" *ngIf="auth.userCategory !== 'ou-minimal'"
|
||||
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'newOrganizationalUnitButton' | translate }}
|
||||
|
|
@ -294,6 +294,12 @@
|
|||
<strong>{{ selectedNode?.name }}</strong>
|
||||
</span>
|
||||
<div class="view-type-container">
|
||||
<mat-checkbox *ngIf="currentView === 'card'" class="cards-select-all-inline"
|
||||
(change)="toggleAllCards()"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()"
|
||||
matTooltip="Seleccionar todos">
|
||||
</mat-checkbox>
|
||||
<button class="action-button" [disabled]="selection.selected.length === 0" (click)="changeParent($event)"
|
||||
matTooltip="{{ 'moveClientsTooltip' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'changeOU' | translate }}
|
||||
|
|
@ -370,10 +376,6 @@
|
|||
<!-- Cards view -->
|
||||
<div *ngIf="currentView === 'card'">
|
||||
<section class="cards-view">
|
||||
<mat-checkbox class="cards-select-all" (change)="toggleAllCards()"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()">
|
||||
</mat-checkbox>
|
||||
<div class="clients-grid">
|
||||
<div *ngFor="let client of arrayClients" class="client-item" [ngClass]="'status-' + client.status">
|
||||
<div class="client-card">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
.asset-actions-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.assets-list-container {
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(255,255,255,0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.folder-link {
|
||||
font-weight: bold;
|
||||
color: #1976d2;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.folder-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<h2 mat-dialog-title>
|
||||
Gestionar Assets: public/assets/<strong>{{ currentPath }}</strong>
|
||||
</h2>
|
||||
<mat-dialog-content>
|
||||
<div class="asset-actions-bar">
|
||||
<!-- NAVEGACIÓN -->
|
||||
<button mat-icon-button (click)="goUp()" [disabled]="!currentPath || currentPath === ''" title="Subir nivel">
|
||||
<mat-icon>arrow_upward</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="loadAssets()" title="Refrescar">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- SUBIDA DE ARCHIVOS -->
|
||||
<span class="spacer"></span>
|
||||
<input type="file" #fileInput (change)="onFileSelected($event)" style="display: none;">
|
||||
<button mat-stroked-button color="primary" (click)="fileInput.click()" [disabled]="isUploading">
|
||||
<mat-icon>upload_file</mat-icon>
|
||||
Subir Fichero
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- MOSTRAR PROGRESO SI ESTÁ SUBIENDO -->
|
||||
@if (isUploading) {
|
||||
<mat-progress-bar mode="indeterminate" style="margin-bottom: 10px;"></mat-progress-bar>
|
||||
}
|
||||
|
||||
<!-- TABLA DE ARCHIVOS -->
|
||||
<div class="assets-list-container">
|
||||
@if (isLoading) {
|
||||
<div class="loading-overlay">Cargando...</div>
|
||||
}
|
||||
|
||||
<table mat-table [dataSource]="assets" class="mat-elevation-z1">
|
||||
<!-- Icon/Type Column -->
|
||||
<ng-container matColumnDef="icon">
|
||||
<th mat-header-cell *matHeaderCellDef> Tipo </th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<mat-icon [color]="element.type === 'folder' ? 'accent' : 'primary'">
|
||||
{{ element.type === 'folder' ? 'folder' : 'insert_drive_file' }}
|
||||
</mat-icon>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> Nombre </th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
@if (element.type === 'folder') {
|
||||
<a href="javascript:void(0)" (click)="enterFolder(element.name)" class="folder-link">
|
||||
{{ element.name }}
|
||||
</a>
|
||||
} @else {
|
||||
<span>{{ element.name }}</span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Size Column -->
|
||||
<ng-container matColumnDef="size">
|
||||
<th mat-header-cell *matHeaderCellDef> Tamaño </th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
{{ element.type === 'file' ? formatBytes(element.size) : '--' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: right;"> Acciones </th>
|
||||
<td mat-cell *matCellDef="let element" style="text-align: right;">
|
||||
@if (element.type === 'file') {
|
||||
<button mat-icon-button (click)="copyPath(element.path)" title="Copiar ruta al portapapeles">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
}
|
||||
<button mat-icon-button color="warn" (click)="deleteAsset(element.path)" title="Eliminar">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
<tr class="mat-row" *matNoDataRow>
|
||||
<td class="mat-cell empty-state" colspan="4">No hay archivos en este directorio</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="close()">Cerrar</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AssetManagerModalComponent } from './asset-manager-modal.component';
|
||||
|
||||
describe('AssetManagerModalComponent', () => {
|
||||
let component: AssetManagerModalComponent;
|
||||
let fixture: ComponentFixture<AssetManagerModalComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AssetManagerModalComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AssetManagerModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { MatDialogRef, MatDialog } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from '../data.service';
|
||||
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
|
||||
export interface AssetNode {
|
||||
name: string;
|
||||
type: 'folder' | 'file';
|
||||
size: number | null;
|
||||
path: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-asset-manager-modal',
|
||||
templateUrl: './asset-manager-modal.component.html',
|
||||
styleUrls: ['./asset-manager-modal.component.css']
|
||||
})
|
||||
export class AssetManagerModalComponent implements OnInit {
|
||||
currentPath: string = '';
|
||||
assets: AssetNode[] = [];
|
||||
isLoading: boolean = false;
|
||||
isUploading: boolean = false;
|
||||
displayedColumns: string[] = ['icon', 'name', 'size', 'actions'];
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<AssetManagerModalComponent>,
|
||||
private dialog: MatDialog,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadAssets();
|
||||
}
|
||||
|
||||
loadAssets(): void {
|
||||
this.isLoading = true;
|
||||
this.dataService.getAssets(this.currentPath).subscribe({
|
||||
next: (data) => {
|
||||
this.assets = data;
|
||||
this.isLoading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.isLoading = false;
|
||||
this.toastService.error('Error al cargar la ruta actual.');
|
||||
console.error(err);
|
||||
|
||||
// If we failed traversing down, fallback to root
|
||||
if (this.currentPath !== '') {
|
||||
this.currentPath = '';
|
||||
this.loadAssets();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enterFolder(folderName: string): void {
|
||||
if (this.currentPath === '') {
|
||||
this.currentPath = folderName;
|
||||
} else {
|
||||
this.currentPath = this.currentPath + '/' + folderName;
|
||||
}
|
||||
this.loadAssets();
|
||||
}
|
||||
|
||||
goUp(): void {
|
||||
if (this.currentPath === '') return;
|
||||
|
||||
const parts = this.currentPath.split('/');
|
||||
parts.pop();
|
||||
this.currentPath = parts.join('/');
|
||||
this.loadAssets();
|
||||
}
|
||||
|
||||
onFileSelected(event: any): void {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const isZip = file.name.toLowerCase().endsWith('.zip');
|
||||
this.isUploading = true;
|
||||
|
||||
this.dataService.uploadAssets(file, this.currentPath, isZip).subscribe({
|
||||
next: (response) => {
|
||||
this.isUploading = false;
|
||||
this.toastService.success(response.message || 'Fichero subido correctamente.');
|
||||
this.loadAssets(); // Refresh directory
|
||||
},
|
||||
error: (error) => {
|
||||
this.isUploading = false;
|
||||
this.toastService.error(error?.error?.message || 'Error al subir el fichero.');
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reset input so it triggers (change) next time even if it's the same file name
|
||||
event.target.value = '';
|
||||
}
|
||||
|
||||
deleteAsset(path: string): void {
|
||||
this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: path }
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.isLoading = true;
|
||||
this.dataService.deleteAsset(path).subscribe({
|
||||
next: (res) => {
|
||||
this.toastService.success('Elemento eliminado correctamente.');
|
||||
this.loadAssets();
|
||||
},
|
||||
error: (err) => {
|
||||
this.isLoading = false;
|
||||
this.toastService.error(err?.error?.message || 'No se pudo eliminar el elemento.');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
copyPath(path: string): void {
|
||||
// Generate an absolute-like path starting with /assets/
|
||||
const fullUrl = `/assets/${path}`;
|
||||
navigator.clipboard.writeText(fullUrl).then(() => {
|
||||
this.toastService.info(`Enlace copiado: ${fullUrl}`);
|
||||
}).catch(err => {
|
||||
console.error('Error al copiar al portapapeles', err);
|
||||
});
|
||||
}
|
||||
|
||||
formatBytes(bytes: number | null): string {
|
||||
if (bytes === null || bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = 2;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -26,9 +26,10 @@
|
|||
.iframe-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
max-width: 50%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.iframe {
|
||||
|
|
@ -39,6 +40,26 @@
|
|||
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.editor-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.html-editor {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
resize: vertical;
|
||||
background-color: #fcfcfc;
|
||||
box-shadow: inset 0px 2px 5px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
|
|||
|
|
@ -15,13 +15,21 @@
|
|||
<input matInput formControlName="publicUrl" name="description">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="form-field" (click)="fileInput.click()"
|
||||
style="cursor: pointer; margin-top: 15px;">
|
||||
<mat-label>Suba el fichero que se usará como menú</mat-label>
|
||||
<input matInput [value]="selectedFile ? selectedFile.name : 'Ningún fichero seleccionado'" readonly
|
||||
placeholder="Seleccionar fichero" style="cursor: pointer; pointer-events: none;" tabindex="-1">
|
||||
<input hidden (change)="onFileSelected($event)" #fileInput type="file">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="form-field">
|
||||
<mat-label>Resolución</mat-label>
|
||||
<mat-select formControlName="resolution" name="resolution">
|
||||
<mat-option value="769">640×480 (8-bit - 256 colores)</mat-option>
|
||||
<mat-option value="771">800×600 (8-bit - 256 colores)</mat-option>
|
||||
<mat-option value="773">1024×768 (8-bit - 256 colores)</mat-option>
|
||||
<mat-option value="775">1280×1024 (8-bit - 256 colores)</mat-option>
|
||||
<mat-option value="775">1280×1024 (8-bit - 256 colores)</mat-option>
|
||||
<mat-option value="784">640×480 (16-bit - 65k colores)</mat-option>
|
||||
<mat-option value="787">800×600 (16-bit - 65k colores)</mat-option>
|
||||
<mat-option value="791">1024×768 (16-bit - 65k colores)</mat-option>
|
||||
|
|
@ -41,12 +49,38 @@
|
|||
<mat-checkbox formControlName="isDefault" name="isDefault">
|
||||
{{ 'defaultMenuLabel' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Iframe -->
|
||||
<div class="iframe-container" *ngIf="safeUrl">
|
||||
<iframe [src]="safeUrl" class="iframe"></iframe>
|
||||
</div>
|
||||
<!-- Iframe o Editor HTML -->
|
||||
@if (safeUrl || isEditingHtml) {
|
||||
<div class="iframe-container">
|
||||
|
||||
<!-- Zona de Controles del Editor -->
|
||||
<div class="editor-controls">
|
||||
@if (isEditingHtml) {
|
||||
<button type="button" class="ordinary-button edit-btn" (click)="cancelHtmlChanges()">
|
||||
Cancelar Edición
|
||||
</button>
|
||||
<button type="button" class="submit-button apply-btn" (click)="applyHtmlChanges()">
|
||||
Aplicar Cambios
|
||||
</button>
|
||||
} @else if (safeUrl) {
|
||||
<button type="button" class="ordinary-button edit-btn" (click)="editHtml()">
|
||||
<mat-icon>edit</mat-icon> Editar HTML
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Reproductor / Previsualizador o Editor -->
|
||||
@if (isEditingHtml) {
|
||||
<textarea class="html-editor" [(ngModel)]="htmlEditorContent" spellcheck="false"></textarea>
|
||||
} @else if (safeUrl) {
|
||||
<iframe [src]="safeUrl" class="iframe"></iframe>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ export class CreateMenuComponent implements OnInit {
|
|||
menuId: string | null = null;
|
||||
softwareProfiles: any[] = [];
|
||||
safeUrl: SafeResourceUrl | null = null;
|
||||
selectedFile: File | null = null;
|
||||
fileContent: string | null = null;
|
||||
isEditingHtml: boolean = false;
|
||||
htmlEditorContent: string = '';
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
|
|
@ -48,6 +52,100 @@ export class CreateMenuComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
onFileSelected(event: any): void {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.selectedFile = file;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e: any) => {
|
||||
// e.target.result será un string en formato 'data:text/html;base64,.....'
|
||||
// Nos quedamos solo con la parte Base64 dividiendo por la coma
|
||||
const base64String = e.target.result.split(',')[1];
|
||||
this.fileContent = base64String;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
this.selectedFile = null;
|
||||
this.fileContent = null;
|
||||
}
|
||||
}
|
||||
|
||||
editHtml(): void {
|
||||
if (this.fileContent) {
|
||||
// Decode Base64 content to text
|
||||
try {
|
||||
// Modern approach to decode Base64 ensuring UTF-8
|
||||
const binaryStr = window.atob(this.fileContent);
|
||||
const bytes = new Uint8Array(binaryStr.length);
|
||||
for (let i = 0; i < binaryStr.length; i++) {
|
||||
bytes[i] = binaryStr.charCodeAt(i);
|
||||
}
|
||||
this.htmlEditorContent = new TextDecoder('utf-8').decode(bytes);
|
||||
} catch (e) {
|
||||
console.error('Failed to decode previous fileContent Base64', e);
|
||||
this.htmlEditorContent = '<!-- Error decodificando HTML anterior -->';
|
||||
}
|
||||
this.isEditingHtml = true;
|
||||
} else if (this.menuForm.get('publicUrl')?.value) {
|
||||
// Fetch the actual raw HTML/Twig template if it exists and no file was manually uploaded yet
|
||||
const publicUrl = this.menuForm.get('publicUrl')?.value;
|
||||
this.dataService.getRawMenuTemplate(publicUrl).subscribe({
|
||||
next: (htmlStr) => {
|
||||
this.htmlEditorContent = htmlStr;
|
||||
this.isEditingHtml = true;
|
||||
},
|
||||
error: (err) => {
|
||||
this.toastService.error('No se pudo descargar el HTML/Twig original del menú para editarlo.');
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
applyHtmlChanges(): void {
|
||||
if (this.htmlEditorContent) {
|
||||
// Encode the drafted Twig code back into Base64 enforcing UTF-8 compatibility
|
||||
try {
|
||||
const bytes = new TextEncoder().encode(this.htmlEditorContent);
|
||||
const binaryStr = Array.from(bytes).map(byte => String.fromCharCode(byte)).join('');
|
||||
const base64Content = window.btoa(binaryStr);
|
||||
|
||||
// This holds the RAW twig that will be sent via PUT/POST on save
|
||||
this.fileContent = base64Content;
|
||||
|
||||
// Ask the backend to compile a preview using this encoded Twig
|
||||
this.dataService.previewMenuTemplate(base64Content).subscribe({
|
||||
next: (renderedHtml) => {
|
||||
// Now encode the definitively rendered HTML to Base64 to populate the iframe preview
|
||||
const renderedBytes = new TextEncoder().encode(renderedHtml);
|
||||
const renderedBinaryStr = Array.from(renderedBytes).map(byte => String.fromCharCode(byte)).join('');
|
||||
const renderedBase64 = window.btoa(renderedBinaryStr);
|
||||
|
||||
this.isEditingHtml = false;
|
||||
const dataUrl = `data:text/html;base64,${renderedBase64}`;
|
||||
this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(dataUrl);
|
||||
this.toastService.success('Vista previa generada. Pulsa Guardar para aplicar la plantilla remota permanentemente.');
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to preview generated HTML', err);
|
||||
const msg = err.error?.message || 'Error compilando la plantilla Twig.';
|
||||
this.toastService.error(msg);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error('Failed to encode updated HTML to Base64', e);
|
||||
this.toastService.error('Hubo un problema procesando los caracteres especiales del HTML.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancelHtmlChanges(): void {
|
||||
this.isEditingHtml = false;
|
||||
this.htmlEditorContent = '';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.data) {
|
||||
this.load()
|
||||
|
|
@ -74,7 +172,7 @@ export class CreateMenuComponent implements OnInit {
|
|||
}
|
||||
|
||||
save(): void {
|
||||
const payload = {
|
||||
const payload: any = {
|
||||
name: this.menuForm.value.name,
|
||||
publicUrl: this.menuForm.value.publicUrl,
|
||||
resolution: this.menuForm.value.resolution,
|
||||
|
|
@ -82,6 +180,10 @@ export class CreateMenuComponent implements OnInit {
|
|||
isDefault: this.menuForm.value.isDefault,
|
||||
};
|
||||
|
||||
if (this.fileContent) {
|
||||
payload.menuHtmlContent = this.fileContent;
|
||||
}
|
||||
|
||||
if (this.menuId) {
|
||||
this.http.put(`${this.baseUrl}${this.menuId}`, payload).subscribe(
|
||||
(response) => {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,26 @@ export class DataService {
|
|||
this.apiUrl = `${this.baseUrl}/menus?page=1&itemsPerPage=1000`;
|
||||
}
|
||||
|
||||
getRawMenuTemplate(publicUrl: string): Observable<string> {
|
||||
const url = `${this.baseUrl}/menu-raw/${publicUrl}`;
|
||||
return this.http.get(url, { responseType: 'text' }).pipe(
|
||||
catchError(error => {
|
||||
console.error('Error fetching raw menu template', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
previewMenuTemplate(base64Template: string): Observable<string> {
|
||||
const url = `${this.baseUrl}/menu-preview`;
|
||||
return this.http.post(url, { templateBase64: base64Template }, { responseType: 'text' }).pipe(
|
||||
catchError(error => {
|
||||
console.error('Error previewing menu template', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getMenus(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
|
||||
const params = new HttpParams({ fromObject: filters });
|
||||
|
||||
|
|
@ -32,7 +52,7 @@ export class DataService {
|
|||
}),
|
||||
catchError(error => {
|
||||
console.error('Error fetching commands', error);
|
||||
return throwError(error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -48,10 +68,44 @@ export class DataService {
|
|||
}),
|
||||
catchError(error => {
|
||||
console.error('Error fetching menus', error);
|
||||
return throwError(error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getAssets(path: string = ''): Observable<any[]> {
|
||||
const params = new HttpParams().set('path', path);
|
||||
return this.http.get<any[]>(`${this.baseUrl}/ogassets/list`, { params }).pipe(
|
||||
catchError(error => {
|
||||
console.error('Error fetching assets', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
uploadAssets(file: File, path: string = '', extractZip: boolean = false): Observable<any> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('path', path);
|
||||
formData.append('extractZip', String(extractZip));
|
||||
|
||||
return this.http.post<any>(`${this.baseUrl}/ogassets/upload`, formData).pipe(
|
||||
catchError(error => {
|
||||
console.error('Error uploading assets', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteAsset(path: string): Observable<any> {
|
||||
return this.http.delete<any>(`${this.baseUrl}/ogassets/delete`, {
|
||||
body: { path }
|
||||
}).pipe(
|
||||
catchError(error => {
|
||||
console.error('Error deleting asset', error);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
Administrar menús</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" (click)="openAssetManager()" style="margin-right: 10px;">Gestionar assets</button>
|
||||
<button class="action-button" (click)="addMenu()" joyrideStep="addStep"
|
||||
text="Utiliza este botón para añadir un nuevo menu.">Añadir menú</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { JoyrideService } from "ngx-joyride";
|
|||
import { Router } from "@angular/router";
|
||||
import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import { CreateMenuComponent } from "./create-menu/create-menu.component";
|
||||
import { AssetManagerModalComponent } from "./asset-manager-modal/asset-manager-modal.component";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
|
|
@ -85,6 +86,17 @@ export class MenusComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
openAssetManager(): void {
|
||||
const dialogRef = this.dialog.open(AssetManagerModalComponent, {
|
||||
width: '800px',
|
||||
height: '600px'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
// Nothing specific to do upon close for now
|
||||
});
|
||||
}
|
||||
|
||||
search(): void {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background: white;
|
||||
color: #333;
|
||||
|
|
@ -56,10 +56,10 @@
|
|||
}
|
||||
|
||||
.stats-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 10px 0;
|
||||
padding: 0 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -119,12 +119,16 @@
|
|||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
padding: 8px 16px;
|
||||
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;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
border-left: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat-card.clickable {
|
||||
|
|
@ -149,8 +153,8 @@
|
|||
}
|
||||
|
||||
.stat-card.clickable:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stat-card.clickable:active {
|
||||
|
|
@ -238,6 +242,28 @@
|
|||
color: #212529;
|
||||
}
|
||||
|
||||
.stat-pending {
|
||||
border-left-color: #ff9800;
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-pending .stat-number,
|
||||
.stat-pending .stat-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-cancelled {
|
||||
border-left-color: #9e9e9e;
|
||||
background: #9e9e9e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-cancelled .stat-number,
|
||||
.stat-cancelled .stat-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-sent {
|
||||
border-left-color: #b19cd9;
|
||||
background: #b19cd9;
|
||||
|
|
@ -250,23 +276,25 @@
|
|||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filters-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
margin: 8px 0;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
|
|
@ -274,7 +302,7 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.filters-header-actions {
|
||||
|
|
@ -315,25 +343,25 @@
|
|||
.search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
gap: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-container.expanded {
|
||||
gap: 20px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.advanced-filters {
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding-top: 15px;
|
||||
margin-top: 10px;
|
||||
padding-top: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.calendar-button-row {
|
||||
|
|
@ -360,22 +388,22 @@ table {
|
|||
|
||||
.search-string {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-date {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
|
|
@ -395,7 +423,7 @@ table {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
padding: 6px 16px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
|
@ -505,8 +533,8 @@ table {
|
|||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin: 20px 0;
|
||||
justify-content: flex-end;
|
||||
margin: 8px 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
|
|
@ -667,7 +695,7 @@ button.cancel-button {
|
|||
}
|
||||
|
||||
.stats-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,20 @@
|
|||
<div class="stat-number">{{ getStatusCount('in-progress') }}</div>
|
||||
<div class="stat-label">{{ 'inProgress' | translate }}</div>
|
||||
</div>
|
||||
<div class="stat-card stat-pending clickable"
|
||||
[class.active-filter]="isFilterActive('pending')"
|
||||
(click)="filterByPending()"
|
||||
matTooltip="Ver trazas pendientes">
|
||||
<div class="stat-number">{{ getStatusCount('pending') }}</div>
|
||||
<div class="stat-label">{{ 'pending' | translate }}</div>
|
||||
</div>
|
||||
<div class="stat-card stat-cancelled clickable"
|
||||
[class.active-filter]="isFilterActive('cancelled')"
|
||||
(click)="filterByCancelled()"
|
||||
matTooltip="Ver trazas canceladas">
|
||||
<div class="stat-number">{{ getStatusCount('cancelled') }}</div>
|
||||
<div class="stat-label">{{ 'cancelled' | translate }}</div>
|
||||
</div>
|
||||
<div class="stat-card stat-sent clickable"
|
||||
[class.active-filter]="isFilterActive('sent')"
|
||||
(click)="filterBySent()"
|
||||
|
|
@ -103,7 +117,6 @@
|
|||
(click)="clearClientFilter($event, commandClientInput)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint>{{ 'enterClientName' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-select">
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
filteredCommands!: Observable<any[]>;
|
||||
commandControl = new FormControl();
|
||||
activeFilter: string = '';
|
||||
private eventSource: EventSource | null = null;
|
||||
|
||||
constructor(private http: HttpClient,
|
||||
private joyrideService: JoyrideService,
|
||||
|
|
@ -145,10 +146,10 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
map(name => (name ? this._filterClients(name) : this.clients.slice()))
|
||||
);
|
||||
|
||||
const eventSource = new EventSource(`${this.mercureUrl}?topic=`
|
||||
this.eventSource = new EventSource(`${this.mercureUrl}?topic=`
|
||||
+ encodeURIComponent(`traces`));
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
this.eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data && data['@id']) {
|
||||
this.updateTracesStatus(data['@id'], data.status, data.progress);
|
||||
|
|
@ -157,6 +158,9 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
toggleFilters(): void {
|
||||
|
|
@ -693,6 +697,20 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
this.loadTraces();
|
||||
}
|
||||
|
||||
filterByPending(): void {
|
||||
this.resetAllFilters();
|
||||
this.filters['status'] = 'pending';
|
||||
this.activeFilter = 'pending';
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
filterByCancelled(): void {
|
||||
this.resetAllFilters();
|
||||
this.filters['status'] = 'cancelled';
|
||||
this.activeFilter = 'cancelled';
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
filterBySent(): void {
|
||||
this.resetAllFilters();
|
||||
this.filters['status'] = 'sent';
|
||||
|
|
@ -716,6 +734,10 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
return 'Trazas fallidas';
|
||||
case 'in-progress':
|
||||
return 'Trazas en progreso';
|
||||
case 'pending':
|
||||
return 'Trazas pendientes';
|
||||
case 'cancelled':
|
||||
return 'Trazas canceladas';
|
||||
case 'sent':
|
||||
return 'Trazas enviadas';
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,18 @@ export class CustomInterceptor implements HttpInterceptor {
|
|||
const token = localStorage.getItem('loginToken');
|
||||
let newCloneRequest;
|
||||
|
||||
if (req.url.includes('login')) {
|
||||
// Usamos el objeto URL para ser quirúrgicos y detectar si el PATH es exactamente "/auth/login":
|
||||
let isLoginPath = false;
|
||||
try {
|
||||
// req.url puede ser relativa o absoluta
|
||||
const url = new URL(req.url, window.location.origin);
|
||||
isLoginPath = url.pathname === '/auth/login';
|
||||
} catch (e) {
|
||||
// Fallback por si la URL es inválida, aunque no debería ocurrir
|
||||
isLoginPath = req.url.includes('/auth/login') && !req.url.includes('menu-raw');
|
||||
}
|
||||
|
||||
if (isLoginPath) {
|
||||
newCloneRequest = req.clone({
|
||||
setHeaders: {
|
||||
Authorization: `${token}`
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
mat-toolbar {
|
||||
/*height: 7vh;*/
|
||||
min-height: 65px;
|
||||
min-width: 375px;
|
||||
background: rgba(226, 232, 240, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
color: black;
|
||||
position: fixed;
|
||||
position: sticky;
|
||||
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;
|
||||
|
|
@ -20,6 +21,15 @@ mat-toolbar {
|
|||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.sidebar-toggle-button {
|
||||
flex-shrink: 0;
|
||||
color: #3f51b5 !important;
|
||||
}
|
||||
|
||||
.sidebar-toggle-button .navbar-icon {
|
||||
color: #3f51b5 !important;
|
||||
}
|
||||
|
||||
/* Estilos específicos para el botón del sidebar */
|
||||
.navbar-icon {
|
||||
color: #3f51b5;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
<mat-toolbar>
|
||||
<button mat-icon-button (click)="onToggleSidebar()" matTooltip="Abrir o cerrar la barra lateral"
|
||||
matTooltipShowDelay="1000" class="sidebar-toggle-button">
|
||||
<mat-icon class="navbar-icon">menu</mat-icon>
|
||||
</button>
|
||||
|
||||
<span class="navbar-title hide-on-small" matTooltip="Consola web de administración de Opengnsys"
|
||||
matTooltipShowDelay="1000">
|
||||
</span>
|
||||
|
||||
<div class="navbar-actions-row" *ngIf="!isSmallScreen">
|
||||
<button mat-icon-button (click)="onToggleSidebar()" matTooltip="Abrir o cerrar la barra lateral"
|
||||
matTooltipShowDelay="1000">
|
||||
<mat-icon class="navbar-icon">menu</mat-icon>
|
||||
</button>
|
||||
<button routerLink="/commands-logs" mat-button>
|
||||
<mat-icon class="trace-button" >notifications</mat-icon>
|
||||
</button>
|
||||
|
|
@ -36,10 +37,6 @@
|
|||
|
||||
<!-- Menú desplegable para pantallas pequeñas -->
|
||||
<div *ngIf="isSmallScreen" class="isSmallScreenButtons">
|
||||
<button mat-icon-button (click)="onToggleSidebar()" matTooltip="Abrir o cerrar la barra lateral"
|
||||
matTooltipShowDelay="1000">
|
||||
<mat-icon class="navbar-icon">menu</mat-icon>
|
||||
</button>
|
||||
<button class="trace-button" routerLink="/commands-logs" mat-button>
|
||||
<mat-icon>notifications</mat-icon>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ html, body {
|
|||
|
||||
.container {
|
||||
height: 100%;
|
||||
padding-top: 65px; /* Asegurar que el contenido no se superponga con el header fijo */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
|
@ -31,9 +30,7 @@ html, body {
|
|||
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 */
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,13 @@
|
|||
<span>{{ 'tasks' | translate }}</span>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item routerLink="/commands-logs" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_TRACE_LOGS' | translate }}"
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">chevron_right</mat-icon>
|
||||
<span>{{ 'traceLogs' | translate }}</span>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
|
||||
<mat-list-item routerLink="/subnets" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_SUBNETS' | translate }}"
|
||||
|
|
|
|||
|
|
@ -426,6 +426,75 @@
|
|||
"TOOLTIP_COMMAND_GROUPS": "Manage command groups",
|
||||
"tasks": "Tasks",
|
||||
"TOOLTIP_TASKS": "View and manage scheduled tasks",
|
||||
"traceLogs": "Trace logs",
|
||||
"TOOLTIP_TRACE_LOGS": "View trace execution history",
|
||||
"schedules": "Schedules",
|
||||
"taskActions": "Actions",
|
||||
"taskActionsTitle": "Task actions",
|
||||
"taskActionsSequentialHint": "Actions are executed sequentially. If one action fails, subsequent actions are automatically cancelled.",
|
||||
"newAction": "New Action",
|
||||
"searchScriptContent": "Search script content",
|
||||
"stepLabel": "Step",
|
||||
"viewParameters": "View parameters",
|
||||
"close": "Close",
|
||||
"addActionTo": "Add action to",
|
||||
"basicActions": "Basic Actions",
|
||||
"selectBasicAction": "Select a basic system action",
|
||||
"actionType": "Action Type",
|
||||
"powerOnDevice": "Power On",
|
||||
"shutdownDevice": "Shutdown",
|
||||
"rebootDevice": "Reboot",
|
||||
"loginSession": "Login",
|
||||
"executionOrder": "Execution order",
|
||||
"runScript": "Run Script",
|
||||
"runCustomScript": "Run a custom script or create a new one",
|
||||
"newScript": "New Script",
|
||||
"savedScript": "Saved Script",
|
||||
"enterScript": "Enter script",
|
||||
"writeScriptHere": "Write your script here",
|
||||
"saveScript": "Save Script",
|
||||
"selectScriptToRun": "Select script to run",
|
||||
"scriptLabel": "Script",
|
||||
"enterParameters": "Enter parameters",
|
||||
"valueFor": "Value for",
|
||||
"manageTaskSchedules": "Manage task schedules for",
|
||||
"newSchedule": "New Schedule",
|
||||
"editSchedule": "Edit",
|
||||
"createSchedule": "Create",
|
||||
"taskSchedule": "Task Schedule",
|
||||
"repetition": "Repetition",
|
||||
"executionDate": "Execution date",
|
||||
"executionTime": "Execution time",
|
||||
"timeFormatHint": "Time in 24h format (e.g. 14:30)",
|
||||
"weekDaysLabel": "Days of the week",
|
||||
"monthsLabel": "Months",
|
||||
"startDate": "Start date",
|
||||
"endDate": "End date",
|
||||
"enableSchedule": "Enable schedule",
|
||||
"scheduleSummaryTitle": "Schedule Summary",
|
||||
"nextExecution": "Next execution",
|
||||
"willExecuteTimes": "Will execute {{count}} times",
|
||||
"notCalculated": "Not calculated",
|
||||
"notDefined": "Not defined",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"editScheduleTooltip": "Edit schedule",
|
||||
"deleteScheduleTooltip": "Delete schedule",
|
||||
"noActionsConfigured": "No actions configured.",
|
||||
"searchScheduleLabel": "Search schedule",
|
||||
"createTaskLabel": "Create task",
|
||||
"addToExistingTask": "Add to existing task",
|
||||
"selectTask": "Select a task",
|
||||
"enterOrder": "Enter order",
|
||||
"scopeLabel": "Scope",
|
||||
"scopeOrganizationalUnit": "Organizational Unit",
|
||||
"scopeClassroomsGroup": "Classrooms group",
|
||||
"scopeClassroom": "Classrooms",
|
||||
"scopeClientsGroup": "Client groups",
|
||||
"scopeClients": "Clients",
|
||||
"selectedClients": "Selected clients",
|
||||
"preSelectedInfo": "Clients have been pre-selected from the image deployment component.",
|
||||
"scheduleAfterCreate": "Do you want to schedule the task after creation?",
|
||||
"dhcp": "DHCP",
|
||||
"TOOLTIP_DHCP": "Configure and manage DHCP",
|
||||
"TOOLTIP_DHCP_STATUS": "Current status of the DHCP service",
|
||||
|
|
@ -600,5 +669,27 @@
|
|||
"manageHardwareTypes": "Manage hardware types",
|
||||
"resolution": "Resolution",
|
||||
"resolutionError": "Resolution is required",
|
||||
"resolutionLabel": "Select resolution"
|
||||
"resolutionLabel": "Select resolution",
|
||||
"catImageOps": "Image operations",
|
||||
"catImageOpsDesc": "Deploy or create images on devices",
|
||||
"catPartition": "Partitioning",
|
||||
"catPartitionDesc": "Partition and format disks",
|
||||
"catSystemControl": "System control",
|
||||
"catSystemControlDesc": "Power on, shut down, reboot or log in",
|
||||
"catCustomScript": "Custom script",
|
||||
"catCustomScriptDesc": "Run a new or saved script",
|
||||
"deployImage": "Deploy image",
|
||||
"createImage": "Create image",
|
||||
"partitionAndFormat": "Partition and format",
|
||||
"backToCategories": "Back to categories",
|
||||
"systemActionHint": "This action will be executed directly on the devices assigned to the task. No additional configuration is needed.",
|
||||
"assistantRequiredTitle": "Use the assistant from the device view",
|
||||
"assistantRequiredDesc": "These operations need the device context to be configured correctly (disk size, existing partitions, available images). Use the dedicated assistants from the device or classroom view.",
|
||||
"assistantStep1": "Go to a device or classroom in the navigation tree",
|
||||
"assistantStep2": "Open the corresponding assistant (partitioning, deploy or create image)",
|
||||
"assistantStep3": "In the assistant, select \"Queue\" to add the action to a scheduled task",
|
||||
"supportedOpsLabel": "Operations available from the assistants",
|
||||
"selectActionTypeError": "You must select an action type",
|
||||
"executionOrderRequired": "You must specify the execution order",
|
||||
"actionAddedSuccess": "Action added successfully"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -427,6 +427,75 @@
|
|||
"TOOLTIP_COMMAND_GROUPS": "Gestionar grupos de comandos",
|
||||
"tasks": "Tareas",
|
||||
"TOOLTIP_TASKS": "Ver y gestionar tareas programadas",
|
||||
"traceLogs": "Logs de trazas",
|
||||
"TOOLTIP_TRACE_LOGS": "Ver el historial de ejecución de trazas",
|
||||
"schedules": "Programaciones",
|
||||
"taskActions": "Acciones",
|
||||
"taskActionsTitle": "Acciones de la tarea",
|
||||
"taskActionsSequentialHint": "Las acciones se ejecutan en orden secuencial. Si una acción falla, las siguientes se cancelan automáticamente.",
|
||||
"newAction": "Nueva Acción",
|
||||
"searchScriptContent": "Buscar contenido de script",
|
||||
"stepLabel": "Paso",
|
||||
"viewParameters": "Ver parámetros",
|
||||
"close": "Cerrar",
|
||||
"addActionTo": "Añadir acción a",
|
||||
"basicActions": "Acciones Básicas",
|
||||
"selectBasicAction": "Selecciona una acción básica del sistema",
|
||||
"actionType": "Tipo de Acción",
|
||||
"powerOnDevice": "Encender Equipo",
|
||||
"shutdownDevice": "Apagar Equipo",
|
||||
"rebootDevice": "Reiniciar Equipo",
|
||||
"loginSession": "Iniciar Sesión",
|
||||
"executionOrder": "Orden de ejecución",
|
||||
"runScript": "Ejecutar Script",
|
||||
"runCustomScript": "Ejecuta un script personalizado o crea uno nuevo",
|
||||
"newScript": "Nuevo Script",
|
||||
"savedScript": "Script Guardado",
|
||||
"enterScript": "Ingrese el script",
|
||||
"writeScriptHere": "Escriba su script aquí",
|
||||
"saveScript": "Guardar Script",
|
||||
"selectScriptToRun": "Seleccione script a ejecutar",
|
||||
"scriptLabel": "Script",
|
||||
"enterParameters": "Ingrese los parámetros",
|
||||
"valueFor": "Valor para",
|
||||
"manageTaskSchedules": "Gestionar programaciones de tareas en",
|
||||
"newSchedule": "Nueva Programación",
|
||||
"editSchedule": "Editar",
|
||||
"createSchedule": "Crear",
|
||||
"taskSchedule": "Programación de Tarea",
|
||||
"repetition": "Repetición",
|
||||
"executionDate": "Fecha de ejecución",
|
||||
"executionTime": "Hora de ejecución",
|
||||
"timeFormatHint": "Hora en formato 24h (ej: 14:30)",
|
||||
"weekDaysLabel": "Días de la semana",
|
||||
"monthsLabel": "Meses",
|
||||
"startDate": "Fecha de inicio",
|
||||
"endDate": "Fecha de fin",
|
||||
"enableSchedule": "Activar programación",
|
||||
"scheduleSummaryTitle": "Resumen de la Programación",
|
||||
"nextExecution": "Próxima ejecución",
|
||||
"willExecuteTimes": "Se ejecutará {{count}} veces",
|
||||
"notCalculated": "No calculada",
|
||||
"notDefined": "No definido",
|
||||
"active": "Activo",
|
||||
"inactive": "Inactivo",
|
||||
"editScheduleTooltip": "Editar programación",
|
||||
"deleteScheduleTooltip": "Eliminar programación",
|
||||
"noActionsConfigured": "No hay acciones configuradas.",
|
||||
"searchScheduleLabel": "Buscar programación",
|
||||
"createTaskLabel": "Crear tarea",
|
||||
"addToExistingTask": "Introducir en tarea existente",
|
||||
"selectTask": "Seleccione una tarea",
|
||||
"enterOrder": "Introduce el orden",
|
||||
"scopeLabel": "Ámbito",
|
||||
"scopeOrganizationalUnit": "Unidad Organizativa",
|
||||
"scopeClassroomsGroup": "Grupo de aulas",
|
||||
"scopeClassroom": "Aulas",
|
||||
"scopeClientsGroup": "Grupos de clientes",
|
||||
"scopeClients": "Clientes",
|
||||
"selectedClients": "Clientes seleccionados",
|
||||
"preSelectedInfo": "Los clientes han sido pre-seleccionados desde el componente de despliegue de imágenes.",
|
||||
"scheduleAfterCreate": "¿Quieres programar la tarea al finalizar su creación?",
|
||||
"dhcp": "DHCP",
|
||||
"TOOLTIP_DHCP": "Configurar y administrar DHCP",
|
||||
"TOOLTIP_DHCP_STATUS": "Estado actual del servicio DHCP",
|
||||
|
|
@ -605,6 +674,28 @@
|
|||
"manageHardwareTypes": "Administrar tipos de hardware",
|
||||
"resolution": "Resolución",
|
||||
"resolutionError": "La resolución es requerida",
|
||||
"resolutionLabel": "Seleccionar resolución"
|
||||
"resolutionLabel": "Seleccionar resolución",
|
||||
"catImageOps": "Operaciones de imagen",
|
||||
"catImageOpsDesc": "Desplegar o crear imágenes en los equipos",
|
||||
"catPartition": "Particionado",
|
||||
"catPartitionDesc": "Particionar y formatear discos",
|
||||
"catSystemControl": "Control del equipo",
|
||||
"catSystemControlDesc": "Encender, apagar, reiniciar o iniciar sesión",
|
||||
"catCustomScript": "Script personalizado",
|
||||
"catCustomScriptDesc": "Ejecutar un script nuevo o guardado",
|
||||
"deployImage": "Desplegar imagen",
|
||||
"createImage": "Crear imagen",
|
||||
"partitionAndFormat": "Particionar y formatear",
|
||||
"backToCategories": "Volver a categorías",
|
||||
"systemActionHint": "Esta acción se ejecutará directamente sobre los equipos asignados a la tarea. No requiere configuración adicional.",
|
||||
"assistantRequiredTitle": "Usa el asistente desde la vista del equipo",
|
||||
"assistantRequiredDesc": "Estas operaciones necesitan el contexto del equipo para configurarse correctamente (tamaño de disco, particiones existentes, imágenes disponibles). Usa los asistentes dedicados desde la vista del equipo o aula.",
|
||||
"assistantStep1": "Ve a la vista de un equipo o aula en el árbol de navegación",
|
||||
"assistantStep2": "Abre el asistente correspondiente (particionado, deploy o crear imagen)",
|
||||
"assistantStep3": "En el asistente, selecciona \"Encolar\" para añadir la acción a una tarea programada",
|
||||
"supportedOpsLabel": "Operaciones disponibles desde los asistentes",
|
||||
"selectActionTypeError": "Debe seleccionar un tipo de acción",
|
||||
"executionOrderRequired": "Debe especificar el orden de ejecución",
|
||||
"actionAddedSuccess": "Acción añadida con éxito"
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue