ref 3007. Software profile in versioned image

pull/97/head
Manuel Aranda Rosales 2026-07-13 10:42:33 +02:00
parent 24517a8649
commit 9797b14038
5 changed files with 106 additions and 31 deletions

View File

@ -32,15 +32,6 @@
<input matInput formControlName="comments" name="comments">
</mat-form-field>
<mat-form-field appearance="fill" class="form-field">
<mat-label>{{ 'softwareProfile' | translate }}</mat-label>
<mat-select formControlName="softwareProfile">
<mat-option *ngFor="let profile of softwareProfiles" [value]="profile['@id']">
{{ profile.description }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox formControlName="remotePc">
{{ 'labelRemotePc' | translate }}
</mat-checkbox>

View File

@ -18,7 +18,6 @@ export class CreateImageComponent implements OnInit {
baseUrl: string;
imageForm: FormGroup<any>;
imageId: string | null = null;
softwareProfiles: any[] = [];
repositories: any[] = [];
loading: boolean = false;
isEditMode: boolean = false;
@ -41,7 +40,6 @@ export class CreateImageComponent implements OnInit {
comments: [''],
remotePc: [false],
isGlobal: [false],
softwareProfile: [''],
imageRepositories: [[], Validators.required],
});
}
@ -54,7 +52,6 @@ export class CreateImageComponent implements OnInit {
} else {
this.loading = false;
}
this.fetchSoftwareProfiles();
this.fetchRepositories();
}
@ -67,7 +64,6 @@ export class CreateImageComponent implements OnInit {
comments: response.comments,
remotePc: response.remotePc,
isGlobal: response.isGlobal,
softwareProfile: response.softwareProfile ? response.softwareProfile['@id'] : null,
imageRepositories: response.imageRepositories ? response.imageRepositories.map((r: any) => r.imageRepository['@id']) : [],
});
this.imageId = response['@id'];
@ -85,18 +81,6 @@ export class CreateImageComponent implements OnInit {
this.showWarning = this.imageForm.get('isGlobal')?.value;
}
fetchSoftwareProfiles() {
const url = `${this.baseUrl}/software-profiles`;
this.http.get(url).subscribe({
next: (response: any) => {
this.softwareProfiles = response['hydra:member'];
},
error: (error) => {
this.toastService.error(this.translate.instant('errorGettingProfiles'));
}
});
}
fetchRepositories() {
const url = `${this.baseUrl}/image-repositories`;
this.http.get(url).subscribe({
@ -117,7 +101,6 @@ export class CreateImageComponent implements OnInit {
remotePc: this.imageForm.value.remotePc,
isGlobal: this.imageForm.value.isGlobal,
imageRepositories: this.imageForm.value.imageRepositories,
...(this.imageForm.value.softwareProfile ? { softwareProfile: this.imageForm.value.softwareProfile } : {}),
};
if (this.imageId) {

View File

@ -108,3 +108,36 @@ table {
margin: 0 auto;
align-self: center;
}
.software-profile-select {
width: 150px;
font-size: 0.8em;
}
.software-profile-select ::ng-deep .mat-mdc-text-field-wrapper {
padding: 0 8px;
}
.software-profile-select ::ng-deep .mat-mdc-form-field-infix {
min-height: unset;
padding-top: 8px;
padding-bottom: 8px;
}
.software-profile-caret {
cursor: pointer;
color: rgba(0, 0, 0, 0.54);
font-size: 20px;
width: 20px;
height: 20px;
}
::ng-deep .software-profile-panel {
max-width: 320px;
}
::ng-deep .software-profile-panel .mat-mdc-option {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -83,8 +83,25 @@
{{ getStatusLabel(image[column.columnDef]) }}
</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef === 'softwareProfile'">
<mat-form-field appearance="outline" class="software-profile-select" subscriptSizing="dynamic">
<input matInput #profileInput="matAutocompleteTrigger" [ngModel]="image.profileValue"
(ngModelChange)="image.profileValue = $event" [matAutocomplete]="profileAuto"
(blur)="resetProfileSearchIfInvalid(image)" [placeholder]="'noSoftwareProfileAssigned' | translate">
<mat-icon matSuffix class="software-profile-caret" (click)="profileInput.openPanel()">arrow_drop_down</mat-icon>
<mat-autocomplete #profileAuto="matAutocomplete" [displayWith]="displayProfile" panelWidth="auto"
class="software-profile-panel" (optionSelected)="onProfileSelected(image, $event.option.value)">
<mat-option [value]="null">{{ 'noSoftwareProfileAssigned' | translate }}</mat-option>
<mat-option *ngFor="let profile of filterProfiles(image.profileValue)" [value]="profile">
{{ profile.description }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</ng-container>
<ng-container
*ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status' && column.columnDef !== 'isGlobal' && column.columnDef !== 'name' && column.columnDef !== 'datasize'">
*ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status' && column.columnDef !== 'isGlobal' && column.columnDef !== 'name' && column.columnDef !== 'datasize' && column.columnDef !== 'softwareProfile'">
{{ column.cell(image) }}
</ng-container>
</td>

View File

@ -37,6 +37,7 @@ export class ShowMonoliticImagesComponent implements OnInit {
filters: { [key: string]: string } = {};
alertMessage: string | null = null;
repository: any = {};
softwareProfiles: any[] = [];
datePipe: DatePipe = new DatePipe('es-ES');
columns = [
{
@ -70,9 +71,9 @@ export class ShowMonoliticImagesComponent implements OnInit {
cell: (image: any) => this.formatSize(image.datasize)
},
{
columnDef: 'description',
header: 'labelDescription',
cell: (image: any) => image.description
columnDef: 'softwareProfile',
header: 'softwareProfile',
cell: (image: any) => image.softwareProfile ? image.softwareProfile['@id'] : null
},
{
columnDef: 'createdAt',
@ -100,13 +101,63 @@ export class ShowMonoliticImagesComponent implements OnInit {
if (this.data) {
this.loadData();
}
this.loadSoftwareProfiles();
}
loadSoftwareProfiles(): void {
this.http.get<any>(`${this.baseUrl}/software-profiles?page=1&itemsPerPage=10000`).subscribe({
next: (response) => {
this.softwareProfiles = response['hydra:member'];
},
error: (error) => {
console.error(this.translate.instant('errorGettingProfiles'), error);
}
});
}
filterProfiles(value: any): any[] {
const query = typeof value === 'string' ? value : (value?.description || '');
const normalized = query.toLowerCase();
return this.softwareProfiles.filter(profile => (profile.description || '').toLowerCase().includes(normalized));
}
displayProfile(value: any): string {
return value?.description || '';
}
onProfileSelected(image: any, selectedProfile: any | null): void {
const previousProfile = image.softwareProfile;
// Optimistic update: reflect the choice immediately so the blur/reset
// handler (which reads image.softwareProfile) doesn't race the PATCH
// response and briefly flash back to the old value.
image.softwareProfile = selectedProfile;
image.profileValue = selectedProfile;
this.http.patch(`${this.baseUrl}${image['@id']}`, { softwareProfile: selectedProfile ? selectedProfile['@id'] : null }).subscribe({
next: () => {
this.toastService.success(this.translate.instant('successfullyEditedSoftware'));
},
error: (error) => {
image.softwareProfile = previousProfile;
image.profileValue = previousProfile;
this.toastService.error(error.error['hydra:description']);
}
});
}
resetProfileSearchIfInvalid(image: any): void {
image.profileValue = image.softwareProfile || null;
}
loadData(): void {
this.loading = true;
this.http.get<any>(`${this.apiUrl}?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&repository.id=${this.data.repositoryId}`, { params: this.filters }).subscribe(
data => {
this.dataSource.data = data['hydra:member'];
this.dataSource.data = (data['hydra:member'] || []).map((item: any) => ({
...item,
profileValue: item.softwareProfile || null
}));
this.length = data['hydra:totalItems'];
this.loading = false;
},