Merge pull request 'Añadida opción en Administración para importar una base de datos OG 1.1 en la nueva version. Permite subir fichero .sql o .zip o usar parametros de conexión a la base de datos de OG 1.1' (#92) from import-legacy into main
Reviewed-on: #92disk-logs^2
commit
d6349dd3f4
|
|
@ -1,5 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
## [1.6.0] - 2026-05-08
|
||||
### Added
|
||||
- Añadida opción en Administración para importar una base de datos OG 1.1 en la nueva version. Permite subir fichero .sql o .zip o usar parametros de conexión a la base de datos de OG 1.1
|
||||
|
||||
## [1.5.9] - 2026-05-07
|
||||
### Fixed
|
||||
- Asistente de particionado: en discos GPT se elimina el límite de 4 particiones primarias, se oculta la opción de añadir partición lógica/extendida y se suprime el estilo visual de partición lógica (borde amarillo e icono └)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
} from "./components/repositories/main-repository-view/main-repository-view.component";
|
||||
import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component";
|
||||
import { MenusComponent } from "./components/menus/menus.component";
|
||||
import { ImportLegacyComponent } from './components/admin/import-legacy/import-legacy.component';
|
||||
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||
import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component';
|
||||
import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component';
|
||||
|
|
@ -55,6 +56,7 @@ const routes: Routes = [
|
|||
{ path: 'users', component: UsersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'env-vars', component: EnvVarsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'roles', component: RolesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'import-legacy', component: ImportLegacyComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'groups', component: GroupsComponent },
|
||||
{ path: 'pxe-images', component: PXEimagesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'pxe', component: PxeComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
}
|
||||
|
||||
import { ImportLegacyComponent } from './components/admin/import-legacy/import-legacy.component';
|
||||
import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component';
|
||||
|
||||
export function initializeApp(configService: ConfigService) {
|
||||
|
|
@ -343,6 +344,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
GlobalOgbootStatusComponent,
|
||||
OgrepositoryStatusComponent,
|
||||
OgdhcpStatusComponent,
|
||||
ImportLegacyComponent,
|
||||
PageNotFoundComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
.main-content {
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
max-width: 400px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
.file-input-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
color: #f44336;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.success-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 64px;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: #ffebee;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ef9a9a;
|
||||
}
|
||||
|
||||
.error-banner h3 {
|
||||
color: #c62828;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.error-banner p {
|
||||
color: #c62828;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.loader-box {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.loader-box mat-spinner {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.unified-log-box {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
margin-top: 30px;
|
||||
background-color: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.unified-log-box h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #4caf50;
|
||||
border-bottom: 1px solid #333;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.unified-log-box pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.full-width-card {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<div class="main-content">
|
||||
<div class="header-container">
|
||||
<div class="header-container-title">
|
||||
<h2>{{ 'importLegacyDbTitle' | translate }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<mat-card class="full-width-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'importLegacyDbDesc' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
@if (isLoading) {
|
||||
<div class="loader-box">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
<h3>{{ 'migrationInProgress' | translate }}</h3>
|
||||
<p class="warning-text">{{ 'migrationWaitWarning' | translate }}</p>
|
||||
</div>
|
||||
} @else if (migrationStatus === 'success') {
|
||||
<div class="success-container">
|
||||
<mat-icon color="primary" class="success-icon">check_circle</mat-icon>
|
||||
<h3>{{ 'migrationSuccess' | translate }}</h3>
|
||||
<p>Los datos han sido migrados correctamente a la nueva versión.</p>
|
||||
<button mat-raised-button color="primary" (click)="resetForm()">Realizar otra importación</button>
|
||||
</div>
|
||||
} @else {
|
||||
@if (migrationStatus === 'error') {
|
||||
<div class="error-banner">
|
||||
<mat-icon color="warn" class="error-icon">error</mat-icon>
|
||||
<h3>{{ 'migrationError' | translate }}</h3>
|
||||
<p>Revisa el log en la parte inferior para más detalles o corrige los parámetros e inténtalo de nuevo.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-radio-group [(ngModel)]="importMode" class="radio-group">
|
||||
<mat-radio-button value="db">{{ 'fromRemoteDb' | translate }}</mat-radio-button>
|
||||
<mat-radio-button value="file">{{ 'fromSqlFile' | translate }}</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
|
||||
@if (importMode === 'db') {
|
||||
<form [formGroup]="dbForm" (ngSubmit)="onSubmit()" class="form-container">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'hostLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="legacyHost" placeholder="192.168.1.100">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'userLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="legacyUser" placeholder="root">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'passLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="legacyPassword" type="password">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>{{ 'dbLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="legacyDb" placeholder="opengnsys">
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-raised-button color="primary" type="submit" [disabled]="dbForm.invalid">
|
||||
<mat-icon>cloud_download</mat-icon> {{ 'importButton' | translate }}
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (importMode === 'file') {
|
||||
<div class="form-container">
|
||||
<p class="help-text">{{ 'uploadSqlOrZip' | translate }}</p>
|
||||
<div class="file-input-container">
|
||||
<button mat-stroked-button type="button" (click)="fileInput.click()">
|
||||
<mat-icon>attach_file</mat-icon> {{ 'sqlFileLabel' | translate }}
|
||||
</button>
|
||||
<input hidden (change)="onFileSelected($event)" #fileInput type="file" accept=".sql,.zip">
|
||||
@if (selectedFile) {
|
||||
<span class="file-name">{{ selectedFile.name }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="onSubmit()" [disabled]="!selectedFile" style="margin-top: 15px;">
|
||||
<mat-icon>upload_file</mat-icon> {{ 'importButton' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (outputLog) {
|
||||
<div class="unified-log-box">
|
||||
<h4>Consola en vivo:</h4>
|
||||
<pre>{{ outputLog }}</pre>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ConfigService } from '../../../services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-import-legacy',
|
||||
templateUrl: './import-legacy.component.html',
|
||||
styleUrls: ['./import-legacy.component.css']
|
||||
})
|
||||
export class ImportLegacyComponent {
|
||||
importMode: 'file' | 'db' = 'db';
|
||||
dbForm: FormGroup;
|
||||
selectedFile: File | null = null;
|
||||
isLoading = false;
|
||||
outputLog = '';
|
||||
migrationStatus: 'idle' | 'success' | 'error' = 'idle';
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private http: HttpClient,
|
||||
private snackBar: MatSnackBar,
|
||||
private translate: TranslateService,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
this.dbForm = this.fb.group({
|
||||
legacyHost: ['', Validators.required],
|
||||
legacyUser: ['', Validators.required],
|
||||
legacyPassword: [''],
|
||||
legacyDb: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.checkMigrationStatus();
|
||||
}
|
||||
|
||||
checkMigrationStatus() {
|
||||
this.http.get<any>(`${this.configService.apiUrl}/migration/import/status`).subscribe({
|
||||
next: (response) => {
|
||||
if (response.isRunning) {
|
||||
this.isLoading = true;
|
||||
this.outputLog = response.log || 'Recuperando estado de migración...';
|
||||
this.connectToMercure();
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error checking migration status:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private connectToMercure() {
|
||||
const url = new URL(this.configService.mercureUrl);
|
||||
url.searchParams.append('topic', '/migration');
|
||||
const eventSource = new EventSource(url.toString());
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'migration_progress') {
|
||||
this.outputLog = data.output;
|
||||
}
|
||||
else if (data.type === 'migration_result') {
|
||||
this.isLoading = false;
|
||||
this.migrationStatus = data.status;
|
||||
this.outputLog = data.output;
|
||||
|
||||
if (data.status === 'success') {
|
||||
this.snackBar.open(this.translate.instant('migrationSuccess'), 'OK', { duration: 5000 });
|
||||
} else {
|
||||
this.snackBar.open(this.translate.instant('migrationError'), 'OK', { duration: 5000 });
|
||||
}
|
||||
eventSource.close();
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
console.error("Mercure connection error", err);
|
||||
};
|
||||
}
|
||||
|
||||
onFileSelected(event: any) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.selectedFile = file;
|
||||
}
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
this.migrationStatus = 'idle';
|
||||
this.outputLog = '';
|
||||
this.selectedFile = null;
|
||||
this.dbForm.reset();
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.importMode === 'file' && !this.selectedFile) {
|
||||
this.snackBar.open('Please select a file', 'Close', { duration: 3000 });
|
||||
} else if ((this.importMode === 'db' && this.dbForm.valid) || (this.importMode === 'file' && this.selectedFile)) {
|
||||
this.isLoading = true;
|
||||
this.outputLog = '';
|
||||
this.migrationStatus = 'idle';
|
||||
|
||||
let request;
|
||||
if (this.importMode === 'file') {
|
||||
const formData = new FormData();
|
||||
formData.append('sql_file', this.selectedFile!);
|
||||
request = this.http.post<any>(`${this.configService.apiUrl}/migration/import`, formData);
|
||||
} else {
|
||||
request = this.http.post<any>(`${this.configService.apiUrl}/migration/import`, this.dbForm.value);
|
||||
}
|
||||
|
||||
request.subscribe({
|
||||
next: (response) => {
|
||||
if (response.status === 'started') {
|
||||
this.outputLog = 'Proceso de migración en curso. Esperando respuesta del servidor en segundo plano...';
|
||||
this.connectToMercure();
|
||||
} else {
|
||||
this.isLoading = false;
|
||||
this.migrationStatus = 'success';
|
||||
this.outputLog = response.output || response.message;
|
||||
this.snackBar.open(this.translate.instant('migrationSuccess'), 'OK', { duration: 5000 });
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.isLoading = false;
|
||||
this.migrationStatus = 'error';
|
||||
const details = error.error?.error || error.error?.details || error.message || 'Unknown error';
|
||||
this.outputLog = details;
|
||||
this.snackBar.open(this.translate.instant('migrationError'), 'OK', { duration: 5000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,5 +110,8 @@
|
|||
<button mat-menu-item routerLink="/env-vars" [matTooltip]="'manageEnvVars' | translate" matTooltipShowDelay="1000">
|
||||
{{ 'labelEnvVars' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/import-legacy" [matTooltip]="'importLegacyDbDesc' | translate" matTooltipShowDelay="1000">
|
||||
{{ 'importLegacyDbTitle' | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</mat-toolbar>
|
||||
|
|
|
|||
|
|
@ -1524,5 +1524,20 @@
|
|||
"youMustSelectAtLeastOne1": "Has de seleccionar almenys un dia de la setmana",
|
||||
"youMustSelectAtLeastOne2": "Has de seleccionar almenys un mes",
|
||||
"youMustSpecifyStartDate": "Has d'especificar data d'inici i data de fi",
|
||||
"youWantToPropagateTheParametersOf": "Vols propagar els paràmetres de xarxa (ogLive, repositori, menú, plantilla PXE, perfil de hardware) a tots els clients d'aquesta unitat organitzativa?"
|
||||
}
|
||||
"youWantToPropagateTheParametersOf": "Vols propagar els paràmetres de xarxa (ogLive, repositori, menú, plantilla PXE, perfil de hardware) a tots els clients d'aquesta unitat organitzativa?",
|
||||
"importLegacyDbTitle": "Importar des d'OG 1.1",
|
||||
"importLegacyDbDesc": "Migra les dades d'una instal·lació OpenGnsys 1.1 a la versió 3.0",
|
||||
"fromSqlFile": "Des de fitxer SQL",
|
||||
"fromRemoteDb": "Des de Base de Dades remota",
|
||||
"importButton": "Importar",
|
||||
"sqlFileLabel": "Fitxer (.sql/.zip)",
|
||||
"hostLabel": "Host de la Base de Dades",
|
||||
"userLabel": "Usuari",
|
||||
"passLabel": "Contrasenya (Opcional)",
|
||||
"dbLabel": "Nom de la BD",
|
||||
"migrationInProgress": "Migració en curs...",
|
||||
"migrationWaitWarning": "Aquest procés pot trigar uns minuts. Pots navegar per altres pestanyes o recarregar la pàgina; la migració continuarà en segon pla.",
|
||||
"migrationSuccess": "Migració completada amb èxit",
|
||||
"migrationError": "Error durant la migració",
|
||||
"uploadSqlOrZip": "Puja el fitxer .sql o comprimit en zip (.zip)"
|
||||
}
|
||||
|
|
@ -1526,5 +1526,20 @@
|
|||
"youMustSelectAtLeastOne1": "You must select at least one day of the week",
|
||||
"youMustSelectAtLeastOne2": "You must select at least one month",
|
||||
"youMustSpecifyStartDate": "You must specify start date and end date",
|
||||
"youWantToPropagateTheParametersOf": "Do you want to propagate the network parameters (ogLive, repository, menu, PXE template, hardware profile) to all clients in this organizational unit?"
|
||||
}
|
||||
"youWantToPropagateTheParametersOf": "Do you want to propagate the network parameters (ogLive, repository, menu, PXE template, hardware profile) to all clients in this organizational unit?",
|
||||
"importLegacyDbTitle": "Import from OG 1.1",
|
||||
"importLegacyDbDesc": "Migrates data from an OpenGnsys 1.1 installation to version 3.0",
|
||||
"fromSqlFile": "From SQL File",
|
||||
"fromRemoteDb": "From Remote Database",
|
||||
"importButton": "Import",
|
||||
"sqlFileLabel": "File (.sql/.zip)",
|
||||
"hostLabel": "Database Host",
|
||||
"userLabel": "Username",
|
||||
"passLabel": "Password (Optional)",
|
||||
"dbLabel": "Database Name",
|
||||
"migrationInProgress": "Migration in progress...",
|
||||
"migrationWaitWarning": "This process can take several minutes. You can navigate to other tabs or reload the page; the migration will continue in the background.",
|
||||
"migrationSuccess": "Migration completed successfully",
|
||||
"migrationError": "Error during migration",
|
||||
"uploadSqlOrZip": "Upload the .sql file or compressed in zip (.zip)"
|
||||
}
|
||||
|
|
@ -1526,5 +1526,20 @@
|
|||
"youMustSelectAtLeastOne1": "Debe seleccionar al menos un día de la semana",
|
||||
"youMustSelectAtLeastOne2": "Debe seleccionar al menos un mes",
|
||||
"youMustSpecifyStartDate": "Debe especificar fecha de inicio y fecha de fin",
|
||||
"youWantToPropagateTheParametersOf": "¿Desea propagar los parámetros de red (ogLive, repositorio, menú, plantilla PXE, perfil de hardware) a todos los clientes de esta unidad organizativa?"
|
||||
}
|
||||
"youWantToPropagateTheParametersOf": "¿Desea propagar los parámetros de red (ogLive, repositorio, menú, plantilla PXE, perfil de hardware) a todos los clientes de esta unidad organizativa?",
|
||||
"importLegacyDbTitle": "Importar desde OG 1.1",
|
||||
"importLegacyDbDesc": "Migra los datos de una instalación OpenGnsys 1.1 a la versión 3.0",
|
||||
"fromSqlFile": "Desde fichero SQL",
|
||||
"fromRemoteDb": "Desde Base de Datos remota",
|
||||
"importButton": "Importar",
|
||||
"sqlFileLabel": "Fichero (.sql/.zip)",
|
||||
"hostLabel": "Host de la Base de Datos",
|
||||
"userLabel": "Usuario",
|
||||
"passLabel": "Contraseña (Opcional)",
|
||||
"dbLabel": "Nombre de la BD",
|
||||
"migrationInProgress": "Migración en curso...",
|
||||
"migrationWaitWarning": "Este proceso puede tardar varios minutos. Puedes navegar por otras pestañas o recargar la página; la migración continuará en segundo plano.",
|
||||
"migrationSuccess": "Migración completada con éxito",
|
||||
"migrationError": "Error durante la migración",
|
||||
"uploadSqlOrZip": "Sube el fichero .sql o comprimido en zip (.zip)"
|
||||
}
|
||||
Loading…
Reference in New Issue