ogRepo new endpoints. Export/Import image
testing/ogGui-multibranch/pipeline/head This commit looks good Details

pull/12/head
Manuel Aranda Rosales 2025-01-31 09:10:02 +01:00
parent b2bf6b8c96
commit bdbb16d3fd
20 changed files with 558 additions and 197 deletions

View File

@ -124,6 +124,8 @@ import { MatSortModule } from '@angular/material/sort';
import { MenusComponent } from './components/menus/menus.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';
import {ImportImageComponent} from "./components/repositories/import-image/import-image.component";
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
}
@ -206,6 +208,8 @@ export function HttpLoaderFactory(http: HttpClient) {
MenusComponent,
CreateMenuComponent,
CreateMultipleClientComponent,
ExportImageComponent,
ImportImageComponent,
],
bootstrap: [AppComponent],
imports: [BrowserModule,

View File

@ -0,0 +1,22 @@
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100px;
}
mat-form-field {
width: 100%;
}
mat-dialog-actions {
display: flex;
justify-content: flex-end;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 10px;
}

View File

@ -0,0 +1,15 @@
<h2 mat-dialog-title>Exportar imagen {{data.image?.name}}</h2>
<mat-dialog-content>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione repositorio destino</mat-label>
<mat-select [(value)]="selectedRepository">
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">{{ repository.name }}</mat-option>
</mat-select>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="close()">Cancelar</button>
<button mat-button (click)="save()">Continuar</button>
</mat-dialog-actions>

View File

@ -0,0 +1,58 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExportImageComponent } from './export-image.component';
import {FormBuilder, ReactiveFormsModule} from "@angular/forms";
import {ToastrModule, ToastrService} from "ngx-toastr";
import {provideHttpClient} from "@angular/common/http";
import {provideHttpClientTesting} from "@angular/common/http/testing";
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {MatButtonModule} from "@angular/material/button";
import {MatSelectModule} from "@angular/material/select";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {TranslateModule} from "@ngx-translate/core";
describe('ExportImageComponent', () => {
let component: ExportImageComponent;
let fixture: ComponentFixture<ExportImageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ExportImageComponent],
imports: [
ReactiveFormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatSelectModule,
BrowserAnimationsModule,
ToastrModule.forRoot(),
TranslateModule.forRoot()
],
providers: [
FormBuilder,
ToastrService,
provideHttpClient(),
provideHttpClientTesting(),
{
provide: MatDialogRef,
useValue: {}
},
{
provide: MAT_DIALOG_DATA,
useValue: {}
}]
})
.compileComponents();
fixture = TestBed.createComponent(ExportImageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,59 @@
import {Component, Inject, OnInit} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-export-image',
templateUrl: './export-image.component.html',
styleUrl: './export-image.component.css'
})
export class ExportImageComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
loading: boolean = true;
repositories: any[] = [];
selectedRepository: string = '';
constructor(
private http: HttpClient,
public dialogRef: MatDialogRef<ExportImageComponent>,
private toastService: ToastrService,
@Inject(MAT_DIALOG_DATA) public data: { image: any }
) {
}
ngOnInit(): void {
this.loading = true;
this.loadRepositories();
}
loadRepositories() {
this.http.get<any>(`${this.baseUrl}/image-repositories?page=1&itemsPerPage=50`).subscribe(
response => {
this.repositories = response['hydra:member'];
this.loading = false;
},
error => console.error('Error fetching organizational units:', error)
);
}
save() {
this.http.post<any>(`${this.baseUrl}${this.selectedRepository}/export-image`, {
images: [this.data.image['@id']]
}).subscribe({
next: (response) => {
this.toastService.success('Imagen exportada correctamente');
this.dialogRef.close();
},
error: error => {
console.error('Error al exportar imagen:', error);
this.toastService.error('Error al exportar imagen');
}
});
}
close() {
this.dialogRef.close();
}
}

View File

@ -91,3 +91,9 @@ table {
color: white;
}
.header-container-title {
flex-grow: 1;
text-align: left;
margin-left: 1em;
}

View File

@ -3,7 +3,11 @@
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title">{{ 'imagesTitle' | translate }}</h2>
<div class="header-container-title">
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
{{ 'imagesTitle' | translate }}
</h2>
</div>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="addImage()">
{{ 'addImageButton' | translate }}
@ -62,7 +66,7 @@
<button mat-menu-item (click)="toggleAction(image, 'get-aux')">Obtener ficheros auxiliares</button>
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'success'" (click)="toggleAction(image, 'delete-trash')">Eliminar imagen temporalmente</button>
<button mat-menu-item [disabled]="!image.imageFullsum || image.status !== 'trash'" (click)="toggleAction(image, 'recover')">Recuperar imagen de la papelera</button>
<button mat-menu-item (click)="toggleAction(image, 'export')">Exportar imagen</button>
</mat-menu>
</td>
</ng-container>

View File

@ -11,6 +11,7 @@ import {ServerInfoDialogComponent} from "../ogdhcp/og-dhcp-subnets/server-info-d
import {Observable} from "rxjs";
import {InfoImageComponent} from "../ogboot/pxe-images/info-image/info-image/info-image.component";
import { JoyrideService } from 'ngx-joyride';
import {ExportImageComponent} from "./export-image/export-image.component";
@Component({
selector: 'app-images',
@ -216,6 +217,14 @@ export class ImagesComponent implements OnInit {
}
});
break;
case 'export':
this.dialog.open(ExportImageComponent, {
width: '600px',
data: {
image: image
}
});
break;
default:
console.error('Acción no soportada:', action);
break;

View File

@ -0,0 +1,22 @@
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100px;
}
mat-form-field {
width: 100%;
}
mat-dialog-actions {
display: flex;
justify-content: flex-end;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 10px;
}

View File

@ -0,0 +1,15 @@
<h2 mat-dialog-title>Importar imagenes a {{data.repository?.name}}</h2>
<mat-dialog-content>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione imagenes a importar</mat-label>
<mat-select [(value)]="selectedClients" multiple>
<mat-option *ngFor="let image of images" [value]="image['@id']">{{ image.name }}</mat-option>
</mat-select>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="close()">Cancelar</button>
<button mat-button (click)="save()">Continuar</button>
</mat-dialog-actions>

View File

@ -0,0 +1,59 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ImportImageComponent } from './import-image.component';
import {FormBuilder, ReactiveFormsModule} from "@angular/forms";
import {ToastrModule, ToastrService} from "ngx-toastr";
import {DataService} from "../../calendar/data.service";
import {provideHttpClient} from "@angular/common/http";
import {provideHttpClientTesting} from "@angular/common/http/testing";
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
import {MatFormFieldModule} from "@angular/material/form-field";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {TranslateModule} from "@ngx-translate/core";
import {MatButtonModule} from "@angular/material/button";
import {MatInputModule} from "@angular/material/input";
import {MatSelectModule} from "@angular/material/select";
describe('ImportImageComponent', () => {
let component: ImportImageComponent;
let fixture: ComponentFixture<ImportImageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ImportImageComponent],
imports: [
ReactiveFormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatSelectModule,
BrowserAnimationsModule,
ToastrModule.forRoot(),
TranslateModule.forRoot()
],
providers: [
FormBuilder,
ToastrService,
provideHttpClient(),
provideHttpClientTesting(),
{
provide: MatDialogRef,
useValue: {}
},
{
provide: MAT_DIALOG_DATA,
useValue: {}
}]
})
.compileComponents();
fixture = TestBed.createComponent(ImportImageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,58 @@
import {Component, Inject, OnInit} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-import-image',
templateUrl: './import-image.component.html',
styleUrl: './import-image.component.css'
})
export class ImportImageComponent implements OnInit{
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
loading: boolean = true;
images: any[] = [];
selectedClients: any[] = [];
constructor(
private http: HttpClient,
public dialogRef: MatDialogRef<ImportImageComponent>,
private toastService: ToastrService,
@Inject(MAT_DIALOG_DATA) public data: { repository: any }
) {
}
ngOnInit(): void {
this.loading = true;
this.loadImages();
}
loadImages() {
this.http.get<any>(`${this.baseUrl}/images?page=1&itemsPerPage=50`).subscribe(
response => {
this.images = response['hydra:member'];
this.loading = false;
},
error => console.error('Error fetching organizational units:', error)
);
}
save() {
this.http.post<any>(`${this.baseUrl}${this.data.repository['@id']}/import-image`, {
images: this.selectedClients
}).subscribe({
next: (response) => {
this.toastService.success('Peticion de importacion de imagen enviada correctamente');
this.dialogRef.close();
},
error: error => {
this.toastService.error('Error al importar imagenes');
}
});
}
close() {
this.dialogRef.close();
}
}

View File

@ -135,6 +135,10 @@
min-height: 400px;
}
.main-container {
margin-top: 15px;
}
.mat-tab-body-wrapper {
min-height: inherit;
}
@ -210,13 +214,6 @@ p {
align-items: center;
}
.status-led {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 10px;
}
.status-led.active {
background-color: green;
@ -304,4 +301,85 @@ table {
}
.dashboard {
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.row {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
}
.top-row {
display: flex;
justify-content: center;
gap: 20px;
}
.top-row .card {
flex: 1;
}
.card {
background: white;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
flex: 1;
min-width: 300px;
}
.status-led {
width: 10px;
height: 10px;
display: inline-block;
border-radius: 50%;
margin-right: 5px;
}
.active {
background-color: green;
}
.inactive {
background-color: red;
}
.cpu-usage-bar {
background: lightgray;
width: 100%;
height: 20px;
border-radius: 5px;
overflow: hidden;
}
.cpu-bar {
height: 100%;
background: green;
}
.cpu-bar.high {
background: red;
}
@media (max-width: 900px) {
.top-row {
flex-direction: column;
}
.top-row .card {
max-width: 100%;
}
}

View File

@ -1,51 +1,91 @@
<mat-tab-group dynamicHeight>
<mat-tab-group class="main-container" dynamicHeight>
<mat-tab label="Estado servidor">
<div class="dashboard">
<h2>OgRepository server Status</h2>
<div class="disk-usage-container">
<div class="disk-usage">
<h3>Uso de disco</h3>
<h2>OgRepository Server Status</h2>
<div class="row top-row">
<div class="card">
<h3>Uso de Disco</h3>
<ngx-charts-pie-chart
[view]="view"
[scheme]="colorScheme"
[results]="diskUsageChartData"
[gradient]="gradient"
[doughnut]="isDoughnut"
[labels]="showLabels"
[legend]="showLegend">
[labels]="showLabels" >
</ngx-charts-pie-chart>
<div class="disk-usage-info">
<div class="info">
<p>Total: {{ diskUsage.total }}</p>
<p>Ocupado: {{ diskUsage.used }}</p>
<p>Disponible: {{ diskUsage.available }}</p>
<p>Ocupado ( % ): {{ diskUsage.percentage }}</p>
<p>Ocupado (%): {{ diskUsage.percentage }}</p>
</div>
</div>
<div class="services-status">
<div class="card">
<h3>Uso de RAM</h3>
<ngx-charts-pie-chart
[view]="view"
[scheme]="colorScheme"
[results]="ramUsageChartData"
[gradient]="gradient"
[doughnut]="isDoughnut"
[labels]="showLabels">
</ngx-charts-pie-chart>
<div class="info">
<p>Total: {{ ramUsage.total }}</p>
<p>Ocupado: {{ ramUsage.used }}</p>
<p>Disponible: {{ ramUsage.available }}</p>
<p>Ocupado (%): {{ ramUsage.percentage }}</p>
</div>
</div>
</div>
<div class="row bottom-row">
<div class="card">
<h3>Uso de CPU</h3>
<div class="cpu-usage-bar">
<div class="cpu-bar" [style.width]="cpuUsage.percentage" [ngClass]="{'high': cpuUsage.percentage > '80%'}"></div>
</div>
<p>Usado: {{ cpuUsage.percentage }}</p>
</div>
<div class="card">
<h3>Servicios</h3>
<ul>
<li *ngFor="let service of getServices()">
<span
class="status-led"
[ngClass]="{
'active': service.status === 'active',
'inactive': service.status === 'stopped' || service.status === 'status not accesible'
}"
></span>
<span class="status-led" [ngClass]="{
'active': service.status === 'active',
'inactive': service.status === 'stopped' || service.status === 'status not accesible'
}"></span>
{{ service.name }}:
<span [ngSwitch]="service.status">
<span *ngSwitchCase="'active'">Activo</span>
<span *ngSwitchCase="'stopped'">Detenido</span>
<span *ngSwitchCase="'status not accesible'">No accesible</span>
<span *ngSwitchDefault>{{ service.status }}</span>
</span>
<span *ngSwitchCase="'active'">Activo</span>
<span *ngSwitchCase="'stopped'">Detenido</span>
<span *ngSwitchCase="'status not accesible'">No accesible</span>
<span *ngSwitchDefault>{{ service.status }}</span>
</span>
</li>
</ul>
</div>
<div class="card">
<h3>Procesos</h3>
<ul>
<li *ngFor="let process of getProcesses()">
<span class="status-led" [ngClass]="{
'active': process.status === 'running',
'inactive': process.status === 'stopped'
}"></span>
{{ process.name }}: {{ process.status }}
</li>
</ul>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="Datos generales">
<div class="dashboard">
<div class="header-button-container">
@ -83,59 +123,6 @@
</mat-tab>
<mat-tab label="Listado de imágenes">
<div class="dashboard">
<h2>Imágenes</h2>
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-label>Buscar nombre de imagen</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="searchImages()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
</div>
<table 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 image" >
<ng-container *ngIf="column.columnDef === 'remotePc' || column.columnDef === 'created'">
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
</mat-icon>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'created'">
{{ column.cell(image) }}
</ng-container>
</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 image" style="text-align: center;">
<button mat-icon-button color="info" (click)="showImageInfo($event, image)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" (click)="editImage($event, image)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="toggleAction(image, 'delete')">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item [disabled]="!image.imageFullsum" (click)="toggleAction(image, 'get-aux')">Obtener ficheros auxiliares</button>
</mat-menu>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div class="paginator-container">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"
[pageIndex]="page"
[pageSizeOptions]="[5, 10, 20, 40, 100]"
(page)="onPageChange($event)">
</mat-paginator>
</div>
</div>
<app-images />
</mat-tab>
</mat-tab-group>

View File

@ -1,4 +1,4 @@
import {Component, Inject} from '@angular/core';
import {Component, Inject, OnInit} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
@ -17,7 +17,7 @@ import {MatDialog} from "@angular/material/dialog";
templateUrl: './main-repository-view.component.html',
styleUrl: './main-repository-view.component.css'
})
export class MainRepositoryViewComponent {
export class MainRepositoryViewComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
repositoryForm: FormGroup<any>;
repositoryId: string | null = null;
@ -25,16 +25,20 @@ export class MainRepositoryViewComponent {
loading: boolean = true;
diskUsage: any = {};
servicesStatus: any = {};
processesStatus: any = {};
diskUsageChartData: any[] = [];
ramUsageChartData: any[] = [];
ramUsage: any = {};
cpuUsage: any = {};
alertMessage: string | null = null;
length: number = 0;
itemsPerPage: number = 10;
page: number = 0;
view: [number, number] = [800, 500];
gradient: boolean = true;
showLegend: boolean = true;
showLabels: boolean = true;
isDoughnut: boolean = true;
status: boolean = false;
repositoryData: any = {};
colorScheme: any = {
domain: ['#FF6384', '#3f51b5']
@ -115,7 +119,6 @@ export class MainRepositoryViewComponent {
comments: [response.comments],
});
this.loading = false;
// Llamar searchImages() solo cuando la data de repository esté cargada
this.searchImages();
},
(error) => {
@ -157,28 +160,33 @@ export class MainRepositoryViewComponent {
loadStatus(): void {
this.http.get<any>(`${this.baseUrl}/image-repositories/server/${this.repositoryId}/status`).subscribe(data => {
const diskData = data.output.disk;
const servicesData = data.output.services;
if (!data.success) {
console.error('Error: No se pudo obtener los datos del servidor');
this.status = false;
return;
}
this.diskUsage = {
total: diskData.total,
used: diskData.used,
available: diskData.available,
percentage: diskData.used_percentage
};
this.status = true;
const { disk, services, ram, cpu, processes } = data.output;
this.diskUsage = { ...disk };
this.diskUsageChartData = [
{
name: 'Usado',
value: parseFloat(diskData.used)
},
{
name: 'Disponible',
value: parseFloat(diskData.available)
}
{ name: 'Usado', value: parseFloat(disk.used.replace('GB', '')) },
{ name: 'Disponible', value: parseFloat(disk.available.replace('GB', '')) }
];
this.servicesStatus = servicesData;
this.ramUsage = { ...ram };
this.ramUsageChartData = [
{ name: 'Usado', value: parseFloat(ram.used.replace('GB', '')) },
{ name: 'Disponible', value: parseFloat(ram.available.replace('GB', '')) }
];
this.cpuUsage = { percentage: cpu.used_percentage };
this.servicesStatus = Object.entries(services).map(([name, status]) => ({ name, status }));
this.processesStatus = Object.entries(processes).map(([name, status]) => ({ name, status }));
}, error => {
console.error('Error fetching status', error);
@ -186,10 +194,17 @@ export class MainRepositoryViewComponent {
}
getServices(): { name: string, status: string }[] {
return Object.keys(this.servicesStatus).map(key => ({
name: key,
status: this.servicesStatus[key]
}));
if (!this.status) {
return [];
}
return this.servicesStatus ? this.servicesStatus : [];
}
getProcesses(): { name: string, status: string }[] {
if (!this.status) {
return [];
}
return this.processesStatus ? this.processesStatus : [];
}
searchImages(): void {
@ -207,64 +222,6 @@ export class MainRepositoryViewComponent {
);
}
editImage(event: MouseEvent, image: any): void {
event.stopPropagation();
this.dialog.open(CreateImageComponent, {
width: '800px',
data: image['@id']
}).afterClosed().subscribe(() => this.searchImages());
}
deleteImage(image: any): void {
this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: image.name },
}).afterClosed().subscribe((result) => {
if (result) {
this.http.delete(`${this.apiUrl}/server/${image.uuid}/delete`).subscribe({
next: () => {
this.toastService.success('Imagen eliminada con éxito');
this.searchImages();
},
error: (error) => {
this.toastService.error(error.error['hydra:description']);
console.error('Error al eliminar la imagen:', error);
}
});
}
});
}
loadImageAlert(image: any): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/server/${image.uuid}/get`, {});
}
showImageInfo(event: MouseEvent, image:any) {
event.stopPropagation();
this.loadImageAlert(image).subscribe(
response => {
this.alertMessage = response.output;
this.dialog.open(ServerInfoDialogComponent, {
width: '600px',
data: {
message: this.alertMessage
}
});
},
error => {
this.toastService.error(error.error['hydra:description']);
}
);
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.length = event.length;
this.searchImages();
}
loadAlert(): Observable<any> {
return this.http.get<any>(`${this.baseUrl}/image-repositories/server/get-collection`);
}
@ -280,29 +237,6 @@ export class MainRepositoryViewComponent {
});
}
toggleAction(image: any, action:string): void {
switch (action) {
case 'get-aux':
this.http.post(`${this.baseUrl}/images/server/${image.uuid}/create-aux-files`, {}).subscribe({
next: () => {
this.toastService.success('Petición de creación de archivos auxiliares enviada');
this.searchImages()
},
error: (error) => {
this.toastService.error(error.error['hydra:description']);
}
});
break;
case 'delete':
this.deleteImage(image);
break;
default:
console.error('Acción no soportada:', action);
break;
}
}
openImageInfoDialog() {
this.loadAlert().subscribe(
response => {

View File

@ -100,3 +100,10 @@ table {
margin: 8px 8px 8px 0;
}
.header-container-title {
flex-grow: 1;
text-align: left;
margin-left: 1em;
}

View File

@ -2,7 +2,11 @@
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" joyrideStep="titleStep" text="Desde esta pantalla podrás ver y administrar los respositioros exitentes.">Administrar repositorios</h2>
<div class="header-container-title">
<h2 joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
{{ 'repositoryTitle' | translate }}
</h2>
</div>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="addImage()" joyrideStep="addStep" text="Utiliza este botón para añadir un nuevo repositorio.">Añadir repositorio</button>
</div>
@ -11,14 +15,20 @@
<div class="search-container">
<mat-form-field appearance="fill" class="search-string">
<mat-label>Buscar nombre de imagen</mat-label>
<mat-label>Buscar nombre de repositorio</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-string">
<mat-label>Buscar IP de repositorio</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<table 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 repository" >
@ -30,9 +40,10 @@
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let client" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editRepository($event, client)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="deleteRepository($event, client)">
<td mat-cell *matCellDef="let repository" style="text-align: center;">
<button mat-icon-button color="primary" (click)="importImage($event, repository)" i18n="@@editImage"> <mat-icon>download</mat-icon></button>
<button mat-icon-button color="primary" (click)="editRepository($event, repository)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="deleteRepository($event, repository)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</td>

View File

@ -8,6 +8,7 @@ import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delet
import { JoyrideService } from 'ngx-joyride';
import {CreateRepositoryComponent} from "./create-repository/create-repository.component";
import { Router } from '@angular/router';
import {ImportImageComponent} from "./import-image/import-image.component";
@Component({
selector: 'app-repositories',
@ -91,6 +92,16 @@ export class RepositoriesComponent {
this.router.navigate(['repository', repository.uuid]);
}
importImage(event: MouseEvent, repository: any): void {
event.stopPropagation();
this.dialog.open(ImportImageComponent, {
width: '600px',
data: { repository }
}).afterClosed().subscribe(() => {
this.search();
});
}
deleteRepository(event: MouseEvent,command: any): void {
event.stopPropagation();
this.dialog.open(DeleteModalComponent, {

View File

@ -275,6 +275,7 @@
"ogliveColumn": "OgLive",
"selectImageOption": "Select image",
"selectOgLiveOption": "Select OgLive",
"repositoryTitle": "Admin Repository",
"saveAssociationsButton": "Save Associations",
"partitionAssistantTitle": "Partition assistant",
"diskSizeLabel": "Size",

View File

@ -300,6 +300,7 @@
"internalUnits": "Unidades internas",
"noResultsMessage": "No hay resultados para mostrar.",
"imagesTitle": "Administrar imágenes",
"repositoryTitle": "Administrar repositorios",
"addImageButton": "Añadir imagen",
"searchNameDescription": "Busca imágenes por nombre para encontrar rápidamente una imagen específica.",
"searchDefaultDescription": "Filtra las imágenes para mostrar solo las imágenes por defecto o no por defecto.",