refs #918. Image component finish

develop-jenkins
Manuel Aranda Rosales 2024-10-18 09:23:40 +02:00
parent c4845e4899
commit fdd37f64d9
8 changed files with 252 additions and 120 deletions

View File

@ -1,47 +1,46 @@
.dialog-content { .dialog-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; /* Espacio entre los elementos del formulario */ gap: 16px; /* Espacio entre los elementos del formulario */
} }
.image-form { .image-form {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.form-field {
width: 100%;
margin-bottom: 16px;
}
/* Botones alineados al final, con margen superior */
.dialog-actions {
display: flex;
justify-content: flex-end;
margin-top: 24px;
}
button {
margin-left: 8px; /* Espacio entre los botones */
}
/* Responsividad para pantallas pequeñas */
@media (max-width: 600px) {
.form-field { .form-field {
width: 100%; width: 100%;
margin-bottom: 16px;
} }
/* Botones alineados al final, con margen superior */ /* Alineación vertical para botones en pantallas pequeñas */
.dialog-actions { .dialog-actions {
display: flex; flex-direction: column;
justify-content: flex-end; align-items: stretch;
margin-top: 24px;
} }
button { button {
margin-left: 8px; /* Espacio entre los botones */ width: 100%;
margin-left: 0;
margin-bottom: 8px;
} }
}
/* Responsividad para pantallas pequeñas */
@media (max-width: 600px) {
.form-field {
width: 100%;
}
/* Alineación vertical para botones en pantallas pequeñas */
.dialog-actions {
flex-direction: column;
align-items: stretch;
}
button {
width: 100%;
margin-left: 0;
margin-bottom: 8px;
}
}

View File

@ -1,30 +1,37 @@
<h2 mat-dialog-title>Añadir nueva imagen</h2> <h2 mat-dialog-title>Añadir nueva imagen</h2>
<mat-dialog-content class="dialog-content"> <mat-dialog-content class="dialog-content">
<form class="image-form"> <form [formGroup]="imageForm" (ngSubmit)="saveImage()" class="image-form">
<mat-form-field appearance="fill" class="form-field"> <mat-form-field appearance="fill" class="form-field">
<mat-label>Nombre de la imagen</mat-label> <mat-label>Nombre de la imagen</mat-label>
<input matInput [(ngModel)]="imagePayload.name" name="name" required> <input matInput formControlName="name" required>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="fill" class="form-field"> <mat-form-field appearance="fill" class="form-field">
<mat-label>Descripción</mat-label> <mat-label>Descripción</mat-label>
<input matInput [(ngModel)]="imagePayload.description" name="description"> <input matInput formControlName="description" name="description">
</mat-form-field> </mat-form-field>
<mat-form-field appearance="fill" class="form-field"> <mat-form-field appearance="fill" class="form-field">
<mat-label>Comentarios</mat-label> <mat-label>Comentarios</mat-label>
<input matInput [(ngModel)]="imagePayload.comments" name="comments"> <input matInput formControlName="comments" name="comments">
</mat-form-field> </mat-form-field>
<mat-form-field appearance="fill" class="form-field"> <mat-form-field appearance="fill" class="form-field">
<mat-label>Perfil de software</mat-label> <mat-label>Perfil de software</mat-label>
<mat-select [(ngModel)]="imagePayload.softwareProfile" name="softwareProfile"> <mat-select formControlName="softwareProfile" required>
<mat-option *ngFor="let profile of softwareProfiles" [value]="profile['@id']"> <mat-option *ngFor="let profile of softwareProfiles" [value]="profile['@id']">
{{ profile.description }} {{ profile.description }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-checkbox
formControlName="remotePc"
class="example-margin"
>
Remote Pc
</mat-checkbox>
</form> </form>
</mat-dialog-content> </mat-dialog-content>

View File

@ -1,22 +1,9 @@
import { Component, OnInit } from '@angular/core'; import {Component, Inject, OnInit} from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
interface ImagePayload { import {DataService} from "../data.service";
[key: string]: any;
name: string | null;
description: string | null;
comments: string | null;
type: string | null;
path: string | null;
revision: string | null;
info: string | null;
size: number | null;
client: string | null;
softwareProfile: string | null;
}
@Component({ @Component({
selector: 'app-create-image', selector: 'app-create-image',
templateUrl: './create-image.component.html', templateUrl: './create-image.component.html',
@ -24,33 +11,54 @@ interface ImagePayload {
}) })
export class CreateImageComponent implements OnInit { export class CreateImageComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
imagePayload: ImagePayload = { imageForm: FormGroup<any>;
name: null, imageId: string | null = null;
description: null,
comments: null,
type: null,
path: null,
revision: null,
info: null,
size: null,
client: null,
softwareProfile: null
};
softwareProfiles: any[] = []; softwareProfiles: any[] = [];
constructor( constructor(
public dialogRef: MatDialogRef<CreateImageComponent>, private fb: FormBuilder,
private http: HttpClient, private http: HttpClient,
private toastService: ToastrService public dialogRef: MatDialogRef<CreateImageComponent>,
) {} private toastService: ToastrService,
private dataService: DataService,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.imageForm = this.fb.group({
name: ['', Validators.required],
description: [''],
comments: [''],
remotePc: [false],
softwareProfile: ['', Validators.required],
});
}
ngOnInit() { ngOnInit() {
if (this.data) {
this.load()
}
this.fetchSoftwareProfiles(); this.fetchSoftwareProfiles();
} }
load(): void {
this.dataService.getImage(this.data).subscribe({
next: (response) => {
this.imageForm = this.fb.group({
name: [response.name, Validators.required],
description: [response.description],
comments: [response.comments],
remotePc: [response.remotePc],
softwareProfile: [response.softwareProfile, Validators.required],
});
this.imageId = response['@id'];
},
error: (err) => {
console.error('Error fetching remote calendar:', err);
}
});
}
fetchSoftwareProfiles() { fetchSoftwareProfiles() {
const url = 'http://127.0.0.1:8001/software-profiles?page=1&itemsPerPage=30'; const url = `${this.baseUrl}/software-profiles`;
this.http.get(url).subscribe({ this.http.get(url).subscribe({
next: (response: any) => { next: (response: any) => {
this.softwareProfiles = response['hydra:member']; this.softwareProfiles = response['hydra:member'];
@ -63,24 +71,37 @@ export class CreateImageComponent implements OnInit {
} }
saveImage(): void { saveImage(): void {
const payload = { ...this.imagePayload }; const payload = {
Object.keys(payload).forEach(key => { name: this.imageForm.value.name,
if (payload[key] == null) { description: this.imageForm.value.description,
delete payload[key]; comments: this.imageForm.value.comments,
} remotePc: this.imageForm.value.remotePc,
}); softwareProfile: this.imageForm.value.softwareProfile
};
console.log('Payload:', payload); if (this.imageId) {
this.http.post(`${this.baseUrl}/images`, payload).subscribe({ this.http.put(`${this.baseUrl}${this.imageId}`, payload).subscribe(
next: () => { (response) => {
this.toastService.success('Imagen creada con éxito'); this.toastService.success('Imagen editada correctamente');
this.dialogRef.close(true); this.dialogRef.close();
}, },
error: (error) => { (error) => {
console.error('Error al crear la imagen:', error); this.toastService.error(error['error']['hydra:description']);
this.toastService.error('Error al crear la imagen'); console.error('Error al editar la imagen', error);
} }
}); );
} else {
this.http.post(`${this.baseUrl}/images`, payload).subscribe(
(response) => {
this.toastService.success('Imagen añadida correctamente');
this.dialogRef.close();
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
console.error('Error al añadir la imagen', error);
}
);
}
} }
close(): void { close(): void {

View File

@ -0,0 +1,54 @@
import { Injectable } from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class DataService {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
private apiUrl = `${this.baseUrl}/images?page=1&itemsPerPage=1000`;
constructor(private http: HttpClient) {}
getImages(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
const params = new HttpParams({ fromObject: filters });
return this.http.get<any>(this.apiUrl, { params }).pipe(
map(response => {
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
return {
data: response['hydra:member'],
totalItems: response['hydra:totalItems']
}
} else {
throw new Error('Unexpected response format');
}
}),
catchError(error => {
console.error('Error fetching commands', error);
return throwError(error);
})
);
}
getImage(id: string): Observable<any> {
return this.http.get<any>(`${this.baseUrl}${id}`).pipe(
map(response => {
if (response.name) {
return response;
} else {
throw new Error('Unexpected response format');
}
}),
catchError(error => {
console.error('Error fetching user group', error);
return throwError(error);
})
);
}
}

View File

@ -69,9 +69,7 @@ table {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
height: 100px;
padding: 10px; padding: 10px;
margin-top: 16px;
} }
.mat-elevation-z8 { .mat-elevation-z8 {

View File

@ -18,9 +18,27 @@
<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"> <ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th> <th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image"> {{ column.cell(image) }} </td> <td mat-cell *matCellDef="let image" >
<ng-container *ngIf="column.columnDef === 'remotePc'">
<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.cell(image) }}
</ng-container>
</td>
</ng-container> </ng-container>
<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)="editImage($event, client)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="deleteImage($event, client)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table> </table>
@ -33,4 +51,3 @@
(page)="onPageChange($event)"> (page)="onPageChange($event)">
</mat-paginator> </mat-paginator>
</div> </div>

View File

@ -5,6 +5,8 @@ import { MatTableDataSource } from '@angular/material/table';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { CreateImageComponent } from './create-image/create-image.component'; import { CreateImageComponent } from './create-image/create-image.component';
import {CreateCommandComponent} from "../commands/main-commands/create-command/create-command.component";
import {DeleteModalComponent} from "../../shared/delete_modal/delete-modal/delete-modal.component";
@Component({ @Component({
selector: 'app-images', selector: 'app-images',
@ -16,15 +18,15 @@ export class ImagesComponent implements OnInit {
dataSource = new MatTableDataSource<any>(); dataSource = new MatTableDataSource<any>();
length: number = 0; length: number = 0;
itemsPerPage: number = 10; itemsPerPage: number = 10;
page: number = 1; page: number = 0;
loading: boolean = false; loading: boolean = false;
filters: { [key: string]: string } = {}; filters: { [key: string]: string } = {};
datePipe: DatePipe = new DatePipe('es-ES'); datePipe: DatePipe = new DatePipe('es-ES');
columns = [ columns = [
{ {
columnDef: 'uuid', columnDef: 'id',
header: 'UUID', header: 'Id',
cell: (image: any) => `${image.uuid}` cell: (image: any) => `${image.id}`
}, },
{ {
columnDef: 'name', columnDef: 'name',
@ -32,9 +34,14 @@ export class ImagesComponent implements OnInit {
cell: (image: any) => `${image.name}` cell: (image: any) => `${image.name}`
}, },
{ {
columnDef: 'downloadUrl', columnDef: 'softwareProfile',
header: 'Url descarga', header: 'Perfil de software',
cell: (image: any) => `${image.downloadUrl}` cell: (image: any) => `${image.softwareProfile?.description}`
},
{
columnDef: 'remotePc',
header: 'Acceso remoto',
cell: (image: any) => `${image.remotePc}`
}, },
{ {
columnDef: 'createdAt', columnDef: 'createdAt',
@ -42,7 +49,7 @@ export class ImagesComponent implements OnInit {
cell: (image: any) => `${this.datePipe.transform(image.createdAt, 'dd/MM/yyyy hh:mm:ss')}` cell: (image: any) => `${this.datePipe.transform(image.createdAt, 'dd/MM/yyyy hh:mm:ss')}`
} }
]; ];
displayedColumns = [...this.columns.map(column => column.columnDef)]; displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/images`; private apiUrl = `${this.baseUrl}/images`;
@ -58,7 +65,7 @@ export class ImagesComponent implements OnInit {
addImage(): void { addImage(): void {
const dialogRef = this.dialog.open(CreateImageComponent, { const dialogRef = this.dialog.open(CreateImageComponent, {
width: '400px' width: '600px'
}); });
dialogRef.afterClosed().subscribe(() => { dialogRef.afterClosed().subscribe(() => {
@ -68,7 +75,7 @@ export class ImagesComponent implements OnInit {
search(): void { search(): void {
this.loading = true; this.loading = true;
this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}&filters=${JSON.stringify(this.filters)}`).subscribe( this.http.get<any>(`${this.apiUrl}?page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
data => { data => {
this.dataSource.data = data['hydra:member']; this.dataSource.data = data['hydra:member'];
this.length = data['hydra:totalItems']; this.length = data['hydra:totalItems'];
@ -81,9 +88,38 @@ export class ImagesComponent implements OnInit {
); );
} }
editImage(event: MouseEvent, image: any): void {
event.stopPropagation();
this.dialog.open(CreateImageComponent, {
width: '600px',
data: image['@id']
}).afterClosed().subscribe(() => this.search());
}
deleteImage(event: MouseEvent,command: any): void {
event.stopPropagation();
this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: command.name },
}).afterClosed().subscribe((result) => {
if (result) {
this.http.delete(`${this.apiUrl}/${command.uuid}`).subscribe({
next: () => {
this.toastService.success('Imagen eliminada con éxito');
this.search();
},
error: (error) => {
console.error('Error al eliminar la imagen:', error);
}
});
}
});
}
onPageChange(event: any): void { onPageChange(event: any): void {
this.page = event.pageIndex; this.page = event.pageIndex;
this.itemsPerPage = event.pageSize; this.itemsPerPage = event.pageSize;
this.length = event.length;
this.search(); this.search();
} }
} }

View File

@ -148,6 +148,13 @@
</mat-list-item> </mat-list-item>
</mat-nav-list> </mat-nav-list>
<mat-list-item routerLink="/images">
<span class="entry">
<mat-icon class="icon">photo</mat-icon>
<span i18n="@@repositories">imágenes</span>
</span>
</mat-list-item>
<mat-list-item class="disabled"> <mat-list-item class="disabled">
<span class="entry"> <span class="entry">
<mat-icon class="icon">warehouse</mat-icon> <mat-icon class="icon">warehouse</mat-icon>
@ -155,13 +162,6 @@
</span> </span>
</mat-list-item> </mat-list-item>
<mat-list-item class="disabled" routerLink="/images">
<span class="entry">
<mat-icon class="icon">photo</mat-icon>
<span i18n="@@repositories">imágenes</span>
</span>
</mat-list-item>
<mat-list-item class="disabled"> <mat-list-item class="disabled">
<span class="entry"> <span class="entry">
<mat-icon class="icon">list</mat-icon> <mat-icon class="icon">list</mat-icon>