refs #1091. CreateImage logic

oggui/translations
Manuel Aranda Rosales 2024-11-15 12:57:45 +01:00
parent 8152d310f8
commit 4dfc45b125
41 changed files with 2035 additions and 451 deletions

View File

@ -1,12 +1,12 @@
{
"name": "og-webconsole",
"version": "0.0.0",
"version": "0.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "og-webconsole",
"version": "0.0.0",
"version": "0.5.0",
"dependencies": {
"@angular/animations": "^18.0.0",
"@angular/cdk": "~18.0.0",

View File

@ -23,7 +23,6 @@ import { TaskLogsComponent } from './components/commands/commands-task/task-logs
import { StatusComponent } from "./components/ogdhcp/og-dhcp-subnets/status/status.component";
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
import { ImagesComponent } from './components/images/images.component';
import { RestoreImageComponent } from './components/groups/components/client-main-view/restore-image/restore-image.component';
import {SoftwareComponent} from "./components/software/software.component";
import {SoftwareProfileComponent} from "./components/software-profile/software-profile.component";
import {OperativeSystemComponent} from "./components/operative-system/operative-system.component";
@ -31,6 +30,15 @@ import {
PartitionAssistantComponent
} from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component";
import {RepositoriesComponent} from "./components/repositories/repositories.component";
import {
CreateImageComponent
} from "./components/groups/components/client-main-view/create-image/create-image.component";
import {
DeployImageComponent
} from "./components/groups/components/client-main-view/deploy-image/deploy-image.component";
import {
MainRepositoryViewComponent
} from "./components/repositories/main-repository-view/main-repository-view.component";
const routes: Routes = [
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
{
@ -54,11 +62,13 @@ const routes: Routes = [
{ path: 'commands-task', component: CommandsTaskComponent },
{ path: 'commands-logs', component: TaskLogsComponent },
{ path: 'calendars', component: CalendarComponent },
{ path: 'client/:id', component: ClientMainViewComponent },
{ path: 'client/:id/partition-assistant', component: PartitionAssistantComponent },
{ path: 'clients/:id', component: ClientMainViewComponent },
{ path: 'clients/:id/partition-assistant', component: PartitionAssistantComponent },
{ path: 'clients/:id/create-image', component: CreateImageComponent },
{ path: 'clients/:id/deploy-image', component: DeployImageComponent },
{ path: 'images', component: ImagesComponent },
{ path: 'repositories', component: RepositoriesComponent },
{ path: 'restore-image', component: RestoreImageComponent},
{ path: 'repository/:id', component: MainRepositoryViewComponent },
{ path: 'software', component: SoftwareComponent },
{ path: 'software-profiles', component: SoftwareProfileComponent },
{ path: 'operative-systems', component: OperativeSystemComponent },

View File

@ -106,7 +106,6 @@ import { ClientMainViewComponent } from './components/groups/components/client-m
import { ImagesComponent } from './components/images/images.component';
import { CreateImageComponent } from './components/images/create-image/create-image.component';
import { PartitionAssistantComponent } from './components/groups/components/client-main-view/partition-assistant/partition-assistant.component';
import { RestoreImageComponent } from './components/groups/components/client-main-view/restore-image/restore-image.component';
import { SoftwareComponent } from './components/software/software.component';
import { CreateSoftwareComponent } from './components/software/create-software/create-software.component';
import { SoftwareProfileComponent } from './components/software-profile/software-profile.component';
@ -119,6 +118,8 @@ import { ClientsComponent } from './components/ogboot/pxe/clients/clients.compon
import { RepositoriesComponent } from './components/repositories/repositories.component';
import { CreateRepositoryComponent } from './components/repositories/create-repository/create-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 { MainRepositoryViewComponent } from './components/repositories/main-repository-view/main-repository-view.component';
@NgModule({
declarations: [
AppComponent,
@ -182,7 +183,6 @@ import { ExecuteCommandComponent } from './components/commands/main-commands/exe
ImagesComponent,
CreateImageComponent,
PartitionAssistantComponent,
RestoreImageComponent,
SoftwareComponent,
CreateSoftwareComponent,
SoftwareProfileComponent,
@ -195,6 +195,8 @@ import { ExecuteCommandComponent } from './components/commands/main-commands/exe
RepositoriesComponent,
CreateRepositoryComponent,
ExecuteCommandComponent,
DeployImageComponent,
MainRepositoryViewComponent,
],
bootstrap: [AppComponent],
imports: [BrowserModule,

View File

@ -1,6 +1,6 @@
<h2 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Añadir' }} calendario</h2>
<mat-dialog-content class="form-container">
<mat-slide-toggle [(ngModel)]="isRemoteAvailable" class="example-margin">¿Disponibilidad remoto?</mat-slide-toggle>
<mat-slide-toggle [(ngModel)]="isRemoteAvailable" class="example-margin">¿Remote PC?</mat-slide-toggle>
<div *ngIf="!isRemoteAvailable" class="form-group">
<mat-label>Selecciona los días de la semana</mat-label>

View File

@ -70,13 +70,18 @@ table {
margin-bottom: 30px;
}
.mat-chip-readonly-true {
background-color: #4CAF50 !important;
color: white !important;
.chip-failed {
background-color: #f15d5d !important;
color: white;
}
.mat-chip-readonly-false {
background-color: #F44336 !important;
color: white !important;
.chip-success {
background-color: #2ea22e !important;
color: white;
}
.chip-pending {
background-color: orange !important;
color: black;
}

View File

@ -1,5 +1,5 @@
<div class="header-container">
<h2 class="title" i18n="@@adminCommandsTitle">Trazas de comandos y procedimientos</h2>
<h2 class="title" i18n="@@adminCommandsTitle">Trazas</h2>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="resetFilters()">Reiniciar filtros</button>
</div>
@ -24,20 +24,46 @@
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field appearance="fill" class="search-boolean">
<mat-label i18n="@@searchLabel">Estado</mat-label>
<mat-select [(ngModel)]="filters['status']" (selectionChange)="loadTraces()" placeholder="Seleccionar opción" >
<mat-option [value]="'failed'">Fallido</mat-option>
<mat-option [value]="'pending'">Pendiente de ejecutar</mat-option>
<mat-option [value]="'success'">Completado con éxito</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- Indicador de carga -->
<div *ngIf="loading" class="loading-container">
<mat-spinner></mat-spinner>
</div>
<!-- Tabla de trazas -->
<div *ngIf="!loading">
<table mat-table [dataSource]="traces" 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 trace">
{{ column.cell(trace) }}
<ng-container *ngIf="column.columnDef === 'status'; else defaultCell">
<mat-chip [ngClass]="{
'chip-failed': trace.status === 'failed',
'chip-success': trace.status === 'success',
'chip-pending': trace.status === 'pending'
}">
{{
trace.status === 'failed' ? 'Fallido' :
trace.status === 'success' ? 'Finalizado con éxito' :
trace.status === 'pending' ? 'Pendiente de ejecutar' :
trace.status
}}
</mat-chip>
</ng-container>
<ng-template #defaultCell>
{{ column.cell(trace) }}
</ng-template>
</td>
</ng-container>
@ -46,6 +72,7 @@
</table>
</div>
<div class="paginator-container">
<mat-paginator [length]="length"
[pageSize]="itemsPerPage"

View File

@ -44,16 +44,21 @@ export class TaskLogsComponent implements OnInit {
header: 'Estado',
cell: (trace: any) => `${trace.status}`
},
{
columnDef: 'jobId',
header: 'Hilo de trabajo',
cell: (trace: any) => `${trace.jobId}`
},
{
columnDef: 'executedAt',
header: 'Programación de ejecución',
cell: (trace: any) => `${this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss')}`,
},
{
columnDef: 'createdAt',
header: 'Fecha de creación',
cell: (trace: any) => `${this.datePipe.transform(trace.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
}
columnDef: 'finishedAt',
header: 'Finalización',
cell: (trace: any) => `${this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss')}`,
},
];
displayedColumns = [...this.columns.map(column => column.columnDef)];

View File

@ -293,3 +293,31 @@ mat-card {
.result-list {
height: auto;
}
.chip-busy {
background-color: red !important;
color: black;
}
.chip-og-live {
background-color: yellow !important;
color: black;
}
.chip-windows,
.chip-windows-session,
.chip-macos {
background-color: blue !important;
color: white;
}
.chip-linux,
.chip-linux-session {
background-color: purple !important;
color: white;
}
.chip-off {
background-color: grey !important;
color: white;
}

View File

@ -56,7 +56,18 @@
<p class="result-type">{{ result.type !== 'client' ? result.type : '' }}</p>
<p class="result-ip" *ngIf="result.type === 'client'">{{ result.ip }}</p>
<p class="result-mac" *ngIf="result.type === 'client'">{{ result.mac }}</p>
<p class="result-status" *ngIf="result.type === 'client'">{{ result.status }}</p>
<mat-chip *ngIf="result.type === 'client'" [ngClass]="{
'chip-og-live': result.status === 'og-live',
'chip-busy': result.status === 'busy',
'chip-windows': result.status === 'windows' || result.status === 'windows-session',
'chip-linux': result.status === 'linux' || result.status === 'linux-session',
'chip-macos': result.status === 'macos',
'chip-off': result.status === 'off'
}">
{{ result.status }}
</mat-chip>
<p *ngIf="result.type !== 'client'" i18n="@@internalUnits" class="result-internal-units">Unidades internas: {{ result.children.length }}</p>
<p *ngIf="result.type !== 'client'" i18n="@@clients" class="result-clients">Clientes: {{ result.clients.length }}</p>
</mat-card-content>

View File

@ -4,90 +4,77 @@
<button mat-flat-button color="primary" [matMenuTriggerFor]="commandMenu">Comandos</button>
</div>
<mat-menu #commandMenu="matMenu">
<button mat-menu-item *ngFor="let command of commands" (click)="onCommandSelect(command)">
<button mat-menu-item *ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)">
{{ command.name }}
</button>
</mat-menu>
</div>
<mat-divider class="divider"></mat-divider>
<div *ngIf="loading" class="loading-container">
<mat-spinner></mat-spinner>
</div>
<mat-tab-group dynamicHeight>
<mat-tab label="Datos generales">
<div *ngIf="loading" class="loading-container">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="!loading" class="client-info">
<div class="info-section">
<mat-tab-group dynamicHeight>
<mat-tab label="Datos generales">
<div *ngIf="!loading" class="client-info">
<div class="info-section">
<div class="two-column-table">
<div class="table-row" *ngFor="let clientData of generalData">
<div class="column property">{{ clientData?.property }}</div>
<div class="column value">{{ clientData?.value }}</div>
</div>
</div>
</mat-tab>
<mat-tab label="Propiedades de red">
<div class="two-column-table">
<div class="table-row" *ngFor="let clientData of networkData">
<div class="column property">{{ clientData?.property }}</div>
<div class="column value">{{ clientData?.value }}</div>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
<div class="header-container">
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
</div>
<div class="table-container">
<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 !== 'size'">
{{ column.cell(image) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'size'">
<mat-chip color="primary" >
{{ (image.size / 1024).toFixed(2) }} GB
</mat-chip>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<div class="charts-wrapper">
<ng-container *ngIf="diskUsageData && diskUsageData.length > 0">
<div class="charts-row">
<div *ngFor="let disk of diskUsageData" class="disk-usage">
<h3>Disco {{ disk.diskNumber }}</h3>
<div class="chart">
<svg viewBox="0 0 36 36" class="circular-chart">
<path
class="circle-bg"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
<ng-container *ngFor="let partition of disk.partitions; let i = index">
<path
class="circle partition-{{ i }}"
[attr.stroke-dasharray]="(partition.size / 1024).toFixed(2) + ', 100'"
[attr.stroke-dashoffset]="getStrokeOffset(disk.partitions, i)"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
</ng-container>
<text x="18" y="20.35" class="percentage">{{ (disk.used / disk.total * 100).toFixed(0) }}%</text>
</svg>
</div>
<p>Usado: {{ disk.used }} GB ({{ disk.percentage }}%)</p>
<p>Total: {{ disk.total }} GB</p>
</div>
</div>
</ng-container>
</div>
</div>
</mat-tab>
<mat-tab label="Discos/Particiones">
<div class="header-container">
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
</div>
</div>
<div class="table-container">
<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 !== 'size'">
{{ column.cell(image) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'size'">
<mat-chip color="primary" >
{{ (image.size / 1024).toFixed(2) }} GB
</mat-chip>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<div class="charts-wrapper">
<ng-container *ngIf="diskUsageData && diskUsageData.length > 0">
<div class="charts-row">
<div *ngFor="let disk of chartDisk" class="disk-usage">
<ngx-charts-pie-chart
[view]="view"
[results]="disk.chartData"
[legend]="showLegend">
</ngx-charts-pie-chart>
<h3>Disco {{ disk.diskNumber }}</h3>
<p>Usado: {{ disk.used }} GB ({{ disk.percentage }}%)</p>
<p>Total: {{ disk.total }} GB</p>
</div>
</div>
</ng-container>
</div>
</mat-tab>
</mat-tab-group>

View File

@ -31,6 +31,24 @@ export class ClientMainViewComponent implements OnInit {
diskUsageData: any[] = [];
partitions: any[] = [];
commands: any[] = [];
chartDisk: any[] = [];
view: [number, number] = [600, 300];
showLegend: boolean = true;
arrayCommands: any[] = [
{name: 'Enceder', slug: 'power-on'},
{name: 'Apagar', slug: 'power-off'},
{name: 'Reiniciar', slug: 'reboot'},
{name: 'Iniciar Sesión', slug: 'login'},
{name: 'Crear Image', slug: 'create-image'},
{name: 'Deploy Image', slug: 'deploy-image'},
{name: 'Eliminar Imagen Cache', slug: 'delete-image-cache'},
{name: 'Particionar y Formatear', slug: 'partition'},
{name: 'Inventario Software', slug: 'software-inventory'},
{name: 'Inventario Hardware', slug: 'hardware-inventory'},
{name: 'Ejecutar script', slug: 'run-script'},
];
datePipe: DatePipe = new DatePipe('es-ES');
columns = [
{
@ -58,6 +76,11 @@ export class ClientMainViewComponent implements OnInit {
header: 'Uso',
cell: (partition: any) => `${partition.memoryUsage} %`
},
{
columnDef: 'operativeSystem',
header: 'SO',
cell: (partition: any) => `${partition.operativeSystem?.name}`
},
];
displayedColumns = [...this.columns.map(column => column.columnDef)];
isDiskUsageEmpty: boolean = true;
@ -75,6 +98,7 @@ export class ClientMainViewComponent implements OnInit {
ngOnInit() {
this.clientData = history.state.clientData;
console.log(this.clientData)
this.loadPartitions()
this.updateGeneralData();
this.updateNetworkData();
@ -100,11 +124,11 @@ export class ClientMainViewComponent implements OnInit {
this.networkData = [
{ property: 'Remote Pc', value: this.clientData.remotePc || '' },
{ property: 'Subred', value: this.clientData?.subnet || '' },
{ property: 'OGlive', value: '' },
{ property: 'OGlive', value: this.clientData?.ogLive?.name || '' },
{ property: 'Autoexec', value: '' },
{ property: 'Repositorio', value: '' },
{ property: 'Repositorio', value: this.clientData?.repository?.name || '' },
{ property: 'Validación', value: this.clientData?.organizationalUnit?.networkSettings?.validation || '' },
{ property: 'Pxe', value: this.clientData?.template.name || '' },
{ property: 'Pxe', value: this.clientData?.template?.name || '' },
{ property: 'Creado por', value: this.clientData?.createdBy || '' }
];
}
@ -124,19 +148,39 @@ export class ClientMainViewComponent implements OnInit {
if (partition.partitionNumber === 0) {
diskData!.total = Number((partition.size / 1024).toFixed(2));
} else {
diskData!.used += Number(((partition.size * (partition.memoryUsage / 100)) / 1024).toFixed(2));
diskData!.used += Number((partition.size / 1024).toFixed(2));
diskData!.partitions.push(partition);
}
});
this.diskUsageData = Array.from(diskUsageMap.entries()).map(([diskNumber, { total, used, partitions }]) => {
const percentage = total > 0 ? Math.round((used / total) * 100) : 0;
return { diskNumber, total, used, percentage, partitions };
this.chartDisk = Array.from(diskUsageMap.entries()).map(([diskNumber, { total, used, partitions }]) => {
const partitionData = partitions.map(partition => ({
name: `Partición ${partition.partitionNumber}`,
value: Number((partition.size / 1024).toFixed(2))
}));
const freeSpace = total - used;
if (freeSpace > 0) {
partitionData.push({
name: 'Espacio libre',
value: Number(freeSpace.toFixed(2))
});
}
return {
diskNumber,
chartData: partitionData,
total,
used,
percentage: total > 0 ? Math.round((used / total) * 100) : 0
};
});
this.diskUsageData = this.chartDisk;
this.isDiskUsageEmpty = this.diskUsageData.length === 0;
}
getStrokeOffset(partitions: any[], index: number): number {
const totalSize = partitions.reduce((acc, part) => acc + (part.size / 1024), 0);
@ -170,16 +214,35 @@ export class ClientMainViewComponent implements OnInit {
});
}
onCommandSelect(command: any): void {
if (command.name === 'Particionar y Formatear') {
onCommandSelect(action: any): void {
if (action === 'partition') {
this.openPartitionAssistant();
}
if (action === 'create-image') {
this.openCreateImageAssistant();
}
if (action === 'deploy-image') {
this.openDeployImageAssistant();
}
}
openPartitionAssistant(): void {
console.log(this.clientData)
this.router.navigate([`/client/${this.clientData.uuid}/partition-assistant`]).then(r => {
console.log('navigated', r);
});
}
openCreateImageAssistant(): void {
this.router.navigate([`/client/${this.clientData.uuid}/create-image`]).then(r => {
console.log('navigated', r);
});
}
openDeployImageAssistant(): void {
this.router.navigate([`/client/${this.clientData.uuid}/deploy-image`]).then(r => {
console.log('navigated', r);
});
}
}

View File

@ -0,0 +1,76 @@
.title {
font-size: 24px;
}
.calendar-button-row {
display: flex;
justify-content: flex-start;
margin-top: 16px;
}
.divider {
margin: 20px 0;
}
.lists-container {
padding: 16px;
}
.card.unidad-card {
height: 100%;
box-sizing: border-box;
}
table {
width: 100%;
margin-top: 50px;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 5px;
box-sizing: border-box;
}
.select-container {
margin-top: 20px;
align-items: center;
width: 100%;
padding: 0 5px;
box-sizing: border-box;
}
.full-width {
width: 100%;
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;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
}

View File

@ -0,0 +1,46 @@
<div class="header-container">
<h2 class="title" i18n="@@subnetsTitle">Crear Imagen desde {{ clientName }}</h2>
<div class="subnets-button-row">
<button mat-flat-button color="primary" (click)="save()">Guardar y ejecutar</button>
</div>
</div>
<mat-divider></mat-divider>
<div class="select-container">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Nombre canónico</mat-label>
<input matInput [(ngModel)]="name" placeholder="Nombre canónico" required>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione imagen</mat-label>
<mat-select [(ngModel)]="selectedImage">
<mat-option>--</mat-option>
<mat-option *ngFor="let image of images" [value]="image['@id']">{{ image.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar partición</th>
<td mat-cell *matCellDef="let row">
<mat-radio-group
[(ngModel)]="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>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CreateImageComponent } from './create-image.component';
describe('CreateImageComponent', () => {
let component: CreateImageComponent;
let fixture: ComponentFixture<CreateImageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CreateImageComponent]
})
.compileComponents();
fixture = TestBed.createComponent(CreateImageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,168 @@
import {Component, EventEmitter, Output} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import {ActivatedRoute} from "@angular/router";
import {MatButton} from "@angular/material/button";
import {MatDivider} from "@angular/material/divider";
import {NgForOf, NgIf} from "@angular/common";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {
MatCell, MatCellDef,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef,
MatTable,
MatTableDataSource
} from "@angular/material/table";
import {MatChip} from "@angular/material/chips";
import {MatCheckbox} from "@angular/material/checkbox";
import {SelectionModel} from "@angular/cdk/collections";
import {MatRadioButton, MatRadioGroup} from "@angular/material/radio";
import {MatFormField, MatLabel} from "@angular/material/form-field";
import {MatOption} from "@angular/material/autocomplete";
import {MatSelect} from "@angular/material/select";
import {MatInput} from "@angular/material/input";
@Component({
selector: 'app-create-image',
templateUrl: './create-image.component.html',
standalone: true,
imports: [
MatButton,
MatDivider,
NgForOf,
NgIf,
ReactiveFormsModule,
MatTable,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
MatCell,
MatCellDef,
MatChip,
MatHeaderRow,
MatRow,
MatHeaderRowDef,
MatRowDef,
MatCheckbox,
MatRadioGroup,
MatRadioButton,
MatFormField,
MatLabel,
MatOption,
MatSelect,
MatInput,
FormsModule
],
styleUrl: './create-image.component.css'
})
export class CreateImageComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
@Output() dataChange = new EventEmitter<any>();
errorMessage = '';
clientId: string | null = null;
partitions: any[] = [];
images: any[] = [];
clientName: string = '';
selectedImage: string | null = null;
selectedPartition: any = null;
name: string = '';
dataSource = new MatTableDataSource<any>();
columns = [
{
columnDef: 'diskNumber',
header: 'Disco',
cell: (partition: any) => `${partition.diskNumber}`
},
{
columnDef: 'partitionNumber',
header: 'Particion',
cell: (partition: any) => `${partition.partitionNumber}`
},
{
columnDef: 'size',
header: 'Tamaño',
cell: (partition: any) => `${partition.size} MB`
},
{
columnDef: 'filesystem',
header: 'Sistema de ficheros',
cell: (partition: any) => `${partition.filesystem}`
},
{
columnDef: 'operativeSystem',
header: 'SO',
cell: (partition: any) => `${partition.operativeSystem?.name}`
}
];
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
selection = new SelectionModel(true, []);
constructor(
private http: HttpClient,
private toastService: ToastrService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.clientId = this.route.snapshot.paramMap.get('id');
this.loadPartitions();
this.loadImages();
}
loadPartitions() {
const url = `${this.baseUrl}/clients/${this.clientId}`;
this.http.get(url).subscribe(
(response: any) => {
if (response.partitions) {
this.clientName = response.name;
this.dataSource.data = response.partitions.filter((partition: any) => {
return partition.partitionNumber !== 0;
});
}
},
(error) => {
console.error('Error al cargar los datos del cliente:', error);
}
);
}
loadImages() {
const url = `${this.baseUrl}/images?created=true&page=1&itemsPerPage=1000`;
this.http.get(url).subscribe(
(response: any) => {
this.images = response['hydra:member'];
},
(error) => {
console.error('Error al cargar las imágenes:', error);
}
);
}
save(): void {
const payload = {
client: `/clients/${this.clientId}`,
name: this.name,
image: this.selectedImage,
partition: this.selectedPartition['@id'],
input: 'assistant'
};
this.http.post(`${this.baseUrl}/images`, payload)
.subscribe({
next: (response) => {
this.toastService.success('Imagen creada exitosamente');
},
error: (error) => {
console.error('Error:', error);
this.toastService.error(error.error['hydra:description']);
}
}
);
}
}

View File

@ -0,0 +1,84 @@
.divider {
margin: 20px 0;
}
table {
width: 100%;
margin-top: 50px;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 5px;
box-sizing: border-box;
}
.option-container {
margin: 20px 0;
width: 100%;
}
.deploy-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 5px;
gap: 10px;
}
.select-container {
margin-top: 20px;
align-items: center;
width: 100%;
padding: 0 5px;
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;
}
.full-width {
width: 100%;
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;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
}

View File

@ -0,0 +1,109 @@
<div class="header-container">
<h2 class="title" i18n="@@subnetsTitle">Deploy imagen en {{ clientName }}</h2>
<div class="subnets-button-row">
<button mat-flat-button color="primary" (click)="save()">Guardar</button>
</div>
</div>
<mat-divider></mat-divider>
<div class="select-container">
<div class="option-container">
<mat-radio-group [(ngModel)]="selectedOption" aria-label="Selecciona una opcion">
<mat-radio-button value="update-cache">Actualizar cache</mat-radio-button>
<mat-radio-button value="deploy-image">Deploy imagen</mat-radio-button>
</mat-radio-group>
</div>
<div class="deploy-container">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione imagen</mat-label>
<mat-select [(ngModel)]="selectedImage">
<mat-option *ngFor="let image of images" [value]="image['@id']">{{ image.name }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione método de deploy</mat-label>
<mat-select [(ngModel)]="selectedMethod">
<mat-option *ngFor="let method of deployMethods" [value]="method">{{ method }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar partición</th>
<td mat-cell *matCellDef="let row">
<mat-radio-group [(ngModel)]="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>
<mat-divider></mat-divider>
<h3 *ngIf="isMethod('multicast')" class="input-group">Opciones multicast</h3>
<h3 *ngIf="isMethod('torrent')" class="input-group">Opciones torrent</h3>
<div *ngIf="isMethod('multicast')" class="input-group">
<mat-form-field appearance="fill" class="input-field">
<mat-label>Puerto</mat-label>
<input matInput [(ngModel)]="mcastPort">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Dirección</mat-label>
<input matInput [(ngModel)]="mcastIp">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label i18n="@@mcastModeLabel">Modo Multicast</mat-label>
<mat-select [(ngModel)]="mcastMode">
<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">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Máximo Clientes</mat-label>
<input matInput [(ngModel)]="mcastMaxClients">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Tiempo Máximo de Espera</mat-label>
<input matInput [(ngModel)]="mcastMaxTime">
</mat-form-field>
</div>
<div *ngIf="isMethod('torrent')" class="input-group">
<mat-form-field appearance="fill" class="input-field">
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
<mat-select [(ngModel)]="p2pMode">
<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">
<mat-label>Semilla</mat-label>
<input matInput [(ngModel)]="p2pTime">
</mat-form-field>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DeployImageComponent } from './deploy-image.component';
describe('DeployImageComponent', () => {
let component: DeployImageComponent;
let fixture: ComponentFixture<DeployImageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DeployImageComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DeployImageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,161 @@
import {Component, EventEmitter, Output} from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {SelectionModel} from "@angular/cdk/collections";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import {ActivatedRoute} from "@angular/router";
@Component({
selector: 'app-deploy-image',
templateUrl: './deploy-image.component.html',
styleUrl: './deploy-image.component.css'
})
export class DeployImageComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
@Output() dataChange = new EventEmitter<any>();
errorMessage = '';
clientId: string | null = null;
partitions: any[] = [];
images: any[] = [];
clientName: string = '';
selectedImage: string | null = null;
selectedOption: string | null = null;
selectedMethod: string | null = null;
selectedPartition: any = null;
mcastIp: string = '';
mcastPort: string = '';
mcastMode: string = '';
mcastSpeed: string = '';
mcastMaxClients: string = '';
mcastMaxTime: string = '';
p2pMode: string = '';
p2pTime: string = '';
name: string = '';
protected p2pModeOptions = [
{ name: 'Leecher', value: 'p2p-mode-leecher' },
{ name: 'Peer', value: 'p2p-mode-peer' },
{ name: 'Seeder', value: 'p2p-mode-seeder' },
];
protected multicastModeOptions = [
{"name": 'Half duplex', "value": "half-duplex"},
{"name": 'Full duplex', "value": "full-duplex"},
];
allMethods = [
'multicast',
'multicast-direct',
'unicast',
'unicast-direct',
'torrent'
];
updateCacheMethods = [
'multicast',
'unicast',
'torrent'
];
dataSource = new MatTableDataSource<any>();
columns = [
{
columnDef: 'diskNumber',
header: 'Disco',
cell: (partition: any) => `${partition.diskNumber}`
},
{
columnDef: 'partitionNumber',
header: 'Particion',
cell: (partition: any) => `${partition.partitionNumber}`
},
{
columnDef: 'size',
header: 'Tamaño',
cell: (partition: any) => `${partition.size} MB`
},
{
columnDef: 'filesystem',
header: 'Sistema de ficheros',
cell: (partition: any) => `${partition.filesystem}`
},
{
columnDef: 'operativeSystem',
header: 'SO',
cell: (partition: any) => `${partition.operativeSystem?.name}`
}
];
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
selection = new SelectionModel(true, []);
constructor(
private http: HttpClient,
private toastService: ToastrService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.clientId = this.route.snapshot.paramMap.get('id');
this.loadPartitions();
this.loadImages();
}
get deployMethods() {
return this.selectedOption === 'update-cache' ? this.updateCacheMethods : this.allMethods;
}
isMethod(method: string): boolean {
return this.selectedMethod === method;
}
loadPartitions() {
const url = `${this.baseUrl}/clients/${this.clientId}`;
this.http.get(url).subscribe(
(response: any) => {
if (response.partitions) {
this.clientName = response.name;
this.dataSource.data = response?.partitions;
}
},
(error) => {
console.error('Error al cargar los datos del cliente:', error);
}
);
}
loadImages() {
const url = `${this.baseUrl}/images?page=1&itemsPerPage=1000`;
this.http.get(url).subscribe(
(response: any) => {
this.images = response['hydra:member'];
},
(error) => {
console.error('Error al cargar las imágenes:', error);
}
);
}
save(): void {
const payload = {
client: `/clients/${this.clientId}`,
name: this.name,
image: this.selectedImage,
partition: this.selectedPartition['@id']
};
this.http.post(`${this.baseUrl}/images`, payload)
.subscribe({
next: (response) => {
this.toastService.success('Imagen creada exitosamente');
},
error: (error) => {
console.error('Error:', error);
this.toastService.error(error.error['hydra:description']);
}
}
);
}
}

View File

@ -30,41 +30,42 @@
<th>Partición</th>
<th>Tipo partición</th>
<th>Tamaño (MB)</th>
<th>Uso (%)</th>
<th>Tamaño (%)</th>
<th>Formatear</th>
<th>Eliminar</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let partition of disk.partitions; let j = index">
<td>{{ partition.partitionNumber }}</td>
<td>
<select [(ngModel)]="partition.type">
<option value="NTFS">NTFS</option>
<option value="LINUX">LINUX</option>
<option value="CACHE">CACHE</option>
</select>
</td>
<td>
<input
type="number"
[(ngModel)]="partition.size"
(input)="updatePartitionSize(disk.diskNumber, j, partition.size)"
/>
</td>
<td>
<input
type="number"
[(ngModel)]="partition.memoryUsage"
/>
</td>
<td>
<input type="checkbox" [(ngModel)]="partition.format" />
</td>
<td>
<button (click)="removePartition(disk.diskNumber, partition)" class="remove-btn">X</button>
</td>
</tr>
<tr *ngFor="let partition of disk.partitions; let j = index">
<td>{{ partition.partitionNumber }}</td>
<td>
<select [(ngModel)]="partition.type">
<option value="NTFS">NTFS</option>
<option value="LINUX">LINUX</option>
<option value="CACHE">CACHE</option>
</select>
</td>
<td>
<input
type="number"
[(ngModel)]="partition.size"
(input)="updatePartitionSize(disk.diskNumber, j, partition.size)"
/>
</td>
<td>
<input
type="number"
[(ngModel)]="partition.percentage"
(input)="updatePartitionPercentage(disk.diskNumber, j, partition.percentage)"
/>
</td>
<td>
<input type="checkbox" [(ngModel)]="partition.format" />
</td>
<td>
<button (click)="removePartition(disk.diskNumber, partition)" class="remove-btn">X</button>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -41,7 +41,6 @@ export class PartitionAssistantComponent implements OnInit {
ngOnInit() {
this.clientId = this.route.snapshot.paramMap.get('id');
this.loadPartitions();
}
@ -112,7 +111,6 @@ export class PartitionAssistantComponent implements OnInit {
if (disk) {
const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize);
console.log(remainingGB)
if (remainingGB > 0) {
const maxPartitionNumber =
disk.partitions.length > 0 ? Math.max(...disk.partitions.map((p) => p.partitionNumber)) : 0;
@ -142,20 +140,41 @@ export class PartitionAssistantComponent implements OnInit {
const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize) + partition.size;
if (size > remainingGB) {
this.errorMessage = `El tamaño de la partición no puede superar el espacio libre (${remainingGB.toFixed(
2
)} GB).`;
this.errorMessage = `El tamaño de la partición no puede superar el espacio libre (${remainingGB.toFixed(2)} GB).`;
} else {
this.errorMessage = '';
partition.size = size;
partition.sizeBytes = size;
partition.percentage = (size / disk.totalDiskSize) * 100;
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
}
}
}
updatePartitionPercentage(diskNumber: number, index: number, percentage: number) {
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
if (disk) {
const partition = disk.partitions[index];
const newSizeMB = (percentage / 100) * disk.totalDiskSize;
const totalPercentage = disk.partitions.reduce((sum, part) => sum + (part === partition ? percentage : part.percentage), 0);
if (totalPercentage > 100) {
this.errorMessage = 'El tamaño total en porcentaje de las particiones no puede exceder el 100%';
partition.percentage = 100 - (totalPercentage - percentage);
} else {
this.errorMessage = '';
partition.percentage = percentage;
partition.size = newSizeMB;
}
}
}
getRemainingGB(partitions: Partition[], totalDiskSize: number): number {
console.log(totalDiskSize)
const totalUsedGB = partitions.reduce((acc, partition) => acc + partition.size, 0);
return Math.max(0, totalDiskSize - totalUsedGB);
}
@ -228,6 +247,7 @@ export class PartitionAssistantComponent implements OnInit {
this.http.post(this.apiUrl, payload).subscribe(
(response) => {
this.toastService.success('Partición creada exitosamente');
window.location.reload();
},
(error) => {
console.error('Error al crear la partición:', error);
@ -239,6 +259,7 @@ export class PartitionAssistantComponent implements OnInit {
this.http.patch(patchUrl, payload).subscribe(
(response) => {
this.toastService.success('Partición actualizada exitosamente');
window.location.reload();
},
(error) => {
console.error('Error al actualizar la partición:', error);
@ -249,7 +270,6 @@ export class PartitionAssistantComponent implements OnInit {
});
}
removePartition(diskNumber: number, partition: Partition) {
const disk = this.disks.find((d) => d.diskNumber === diskNumber);
@ -264,6 +284,7 @@ export class PartitionAssistantComponent implements OnInit {
this.http.delete(deleteUrl).subscribe(
(response) => {
this.toastService.success('Partición eliminada exitosamente');
window.location.reload();
},
(error) => {}
);

View File

@ -1,71 +0,0 @@
.partition-assistant {
font-family: 'Roboto', sans-serif;
background-color: #f9f9f9;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin: 20px auto;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
padding: 10px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.partition-table {
width: 100%;
border-collapse: collapse;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 20px;
}
.partition-table th {
background-color: #f5f5f5;
color: #333;
padding: 12px;
font-weight: 600;
}
.partition-table td {
padding: 10px;
text-align: center;
border-bottom: 1px solid #eee;
}
.partition-table select {
padding: 5px;
border-radius: 4px;
border: 1px solid #ccc;
width: 100%;
}
.actions {
display: flex;
justify-content: flex-end;
padding-top: 10px;
}
button.mat-flat-button {
background-color: #28a745;
color: white;
padding: 10px 20px;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s ease;
}
button.mat-flat-button:hover {
background-color: #218838;
}

View File

@ -1,39 +0,0 @@
<h2>Asistente de imagenes en disco</h2>
<div *ngFor="let disk of disks" class="partition-assistant">
<div class="header">
<label>Disco {{ disk.diskNumber }}</label>
</div>
<table class="partition-table">
<thead>
<tr>
<th>Partición</th>
<th>Imagen ISO</th>
<th>OgLive</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let partition of disk.partitions">
<td>{{ partition.partitionNumber }}</td>
<td>
<select [(ngModel)]="partition.associatedImageId" (change)="onImageSelected(partition, $event)" name="associatedImage-{{partition.partitionNumber}}">
<option value="">Seleccionar imagen</option>
<option *ngFor="let image of availableImages" [value]="image['@id']">
{{ image.name }}
</option>
</select>
</td>
<td>
<select (change)="onOgLiveSelected(partition, $event)">
<option value="">Seleccionar OgLive</option>
<option *ngFor="let ogLive of availableOgLives" [value]="ogLive">{{ ogLive }}</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<div class="actions">
<button mat-flat-button color="primary" (click)="saveAssociations()">Guardar Asociaciones</button>
</div>

View File

@ -1,95 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface Image {
'@id': string;
'@type': string;
name: string;
description: string;
comments: string;
uuid: string;
id: number;
}
interface Partition {
diskNumber: number;
partitionNumber: number;
associatedImageId?: string;
associatedOgLive?: string;
}
@Component({
selector: 'app-restore-image',
templateUrl: './restore-image.component.html',
styleUrls: ['./restore-image.component.css']
})
export class RestoreImageComponent implements OnInit {
@Input() data: any;
disks: { diskNumber: number; partitions: Partition[] }[] = [];
availableImages: Image[] = [];
availableOgLives: string[] = [];
constructor(private http: HttpClient) {}
ngOnInit(): void {
this.initializeDisks();
this.fetchAvailableImages();
this.availableOgLives = ['LiveCD1', 'LiveCD2', 'LiveCD3'];
}
initializeDisks() {
const partitionsFromData = this.data.partitions;
const disksMap = new Map<number, Partition[]>();
partitionsFromData.forEach((partition: any) => {
if (!disksMap.has(partition.diskNumber)) {
disksMap.set(partition.diskNumber, []);
}
disksMap.get(partition.diskNumber)!.push({
diskNumber: partition.diskNumber,
partitionNumber: partition.partitionNumber
});
});
disksMap.forEach((partitions, diskNumber) => {
this.disks.push({ diskNumber, partitions });
});
}
fetchAvailableImages() {
const url = 'http://127.0.0.1:8001/images?page=1&itemsPerPage=30';
this.http.get(url).subscribe(
(response: any) => {
this.availableImages = response['hydra:member'];
},
(error) => {
console.error('Error al obtener las imágenes:', error);
}
);
}
onImageSelected(partition: Partition, event: Event) {
const selectElement = event.target as HTMLSelectElement;
partition.associatedImageId = selectElement.value;
}
onOgLiveSelected(partition: Partition, event: Event) {
const selectElement = event.target as HTMLSelectElement;
partition.associatedOgLive = selectElement.value;
}
saveAssociations() {
this.disks.forEach(disk => {
disk.partitions.forEach(partition => {
if (partition.associatedImageId || partition.associatedOgLive) {
console.log(
`Guardando para disco ${partition.diskNumber}, partición ${partition.partitionNumber}, ` +
`Imagen ID: ${partition.associatedImageId}, OgLive: ${partition.associatedOgLive}`
);
}
});
});
}
}

View File

@ -62,3 +62,31 @@ button{
color: #666;
line-height: 1.5;
}
.chip-busy {
background-color: red !important;
color: black;
}
.chip-og-live {
background-color: yellow !important;
color: black;
}
.chip-windows,
.chip-windows-session,
.chip-macos {
background-color: blue !important;
color: white;
}
.chip-linux,
.chip-linux-session {
background-color: purple !important;
color: white;
}
.chip-off {
background-color: grey !important;
color: white;
}

View File

@ -41,49 +41,59 @@
<div *ngIf="loading">
<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 client" >
<ng-container *ngIf="column.columnDef === 'name'">
<div class="client-info">
<div class="client-name">{{ client.name }}</div>
<div class="client-ip">{{ client.ip }}</div>
<div class="client-mac">{{ client.mac }}</div>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef === 'status'">
<mat-chip>
{{ client.status }}
</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'status' && column.columnDef !== 'name'" >
{{ column.cell(client) }}
</ng-container>
</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 client">
<ng-container *ngIf="column.columnDef === 'name'">
<div class="client-info">
<div class="client-name">{{ client.name }}</div>
<div class="client-ip">{{ client.ip }}</div>
<div class="client-mac">{{ client.mac }}</div>
</div>
</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 *ngIf="!syncStatus" mat-icon-button color="primary" (click)="getStatus(client)"><mat-icon>sync</mat-icon></button>
<button *ngIf="syncStatus" mat-icon-button color="primary"><mat-spinner diameter="24"></mat-spinner></button>
<button mat-icon-button color="info" (click)="handleClientClick($event, client)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" (click)="onEditClick($event, client.uuid)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="onDeleteClick($event, client)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</td>
</ng-container>
<ng-container *ngIf="column.columnDef === 'status'">
<mat-chip [ngClass]="{
'chip-og-live': client.status === 'og-live',
'chip-busy': client.status === 'busy',
'chip-windows': client.status === 'windows' || client.status === 'windows-session',
'chip-linux': client.status === 'linux' || client.status === 'linux-session',
'chip-macos': client.status === 'macos',
'chip-off': client.status === 'off'
}">
{{ client.status }}
</mat-chip>
</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]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>
<ng-container *ngIf="column.columnDef !== 'status' && column.columnDef !== 'name'">
{{ column.cell(client) }}
</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 client" style="text-align: center;">
<button *ngIf="!syncStatus" mat-icon-button color="primary" (click)="getStatus(client)"><mat-icon>sync</mat-icon></button>
<button *ngIf="syncStatus" mat-icon-button color="primary"><mat-spinner diameter="24"></mat-spinner></button>
<button mat-icon-button color="info" (click)="handleClientClick($event, client)"><mat-icon i18n="@@deleteElementTooltip">visibility</mat-icon></button>
<button mat-icon-button color="primary" (click)="onEditClick($event, client.uuid)" i18n="@@editImage"> <mat-icon>edit</mat-icon></button>
<button mat-icon-button color="warn" (click)="onDeleteClick($event, client)">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button>
</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]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>
</div>

View File

@ -77,7 +77,15 @@
{{ unit.description }}
</mat-option>
</mat-select>
<mat-error i18n="@@hardware-profile-error">Formato de URL inválido.</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label i18n="@@hardware-profile-label">Repositorio</mat-label>
<mat-select formControlName="repository">
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
{{ repository.name }}
</mat-option>
</mat-select>
</mat-form-field>
</form>

View File

@ -16,6 +16,7 @@ export class EditClientComponent {
clientForm!: FormGroup;
parentUnits: any[] = [];
hardwareProfiles: any[] = [];
repositories: any[] = [];
ogLives: any[] = [];
templates: any[] = [];
isEditMode: boolean;
@ -48,6 +49,7 @@ export class EditClientComponent {
this.loadHardwareProfiles();
this.loadOgLives();
this.loadPxeTemplates()
this.loadRepositories();
this.clientForm = this.fb.group({
organizationalUnit: [null, Validators.required],
name: ['', Validators.required],
@ -59,6 +61,7 @@ export class EditClientComponent {
template: null,
hardwareProfile: null,
ogLive: null,
repository: null,
});
}
@ -99,6 +102,19 @@ export class EditClientComponent {
);
}
loadRepositories(): void {
const url = `${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe(
response => {
this.repositories = response['hydra:member'];
},
error => {
console.error('Error fetching ogLives:', error);
}
);
}
loadPxeTemplates(): void {
const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`;
@ -127,6 +143,7 @@ export class EditClientComponent {
serialNumber: data.serialNumber,
hardwareProfile: data.hardwareProfile ? data.hardwareProfile['@id'] : null,
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null,
repository: data.repository ? data.repository['@id'] : null,
ogLive: data.ogLive ? data.ogLive['@id'] : null,
template: data.template ? data.template['@id'] : null,
});
@ -150,7 +167,6 @@ export class EditClientComponent {
this.http.patch<any>(putUrl, formData, { headers }).subscribe(
response => {
console.log('PUT successful:', response);
this.dialogRef.close();
this.openSnackBar(false, 'Cliente actualizado exitosamente');
@ -167,7 +183,6 @@ export class EditClientComponent {
this.http.post<any>(postUrl, formData, { headers }).subscribe(
response => {
console.log('POST successful:', response);
this.dialogRef.close();
this.openSnackBar(false, 'Cliente creado exitosamente');
},

View File

@ -15,6 +15,14 @@
margin-bottom: 16px;
}
.partition-info-container {
background-color: #f0f8ff; /* Un color de fondo suave */
padding: 10px;
border: 1px solid #ccc;
margin-top: 10px;
border-radius: 5px;
}
/* Botones alineados al final, con margen superior */
.dialog-actions {
display: flex;

View File

@ -1,4 +1,4 @@
<h2 mat-dialog-title>Añadir nueva imagen</h2>
<h2 mat-dialog-title>{{ imageId ? 'Editar' : 'Crear' }} imagen</h2>
<mat-dialog-content class="dialog-content">
<form [formGroup]="imageForm" (ngSubmit)="saveImage()" class="image-form">
@ -28,7 +28,7 @@
<mat-form-field appearance="fill" class="form-field">
<mat-label>Perfil de software</mat-label>
<mat-select formControlName="softwareProfile" required>
<mat-select formControlName="softwareProfile">
<mat-option *ngFor="let profile of softwareProfiles" [value]="profile['@id']">
{{ profile.description }}
</mat-option>
@ -41,7 +41,19 @@
>
Remote Pc
</mat-checkbox>
<mat-divider *ngIf="imageId && partitionInfo"></mat-divider>
<div *ngIf="imageId && partitionInfo" class="partition-info-container">
<h3>Información de Partición de origen</h3>
<p>Sistema de archivos: {{ partitionInfo['filesystem'] }}</p>
<p>Disco: {{ partitionInfo['numDisk'] }}</p>
<p>Particion: {{ partitionInfo['numPartition'] }}</p>
<p>Nombre del SO: {{ partitionInfo['osName'] }}</p>
<p>Código de partición: {{ partitionInfo['partitionCode'] }}</p>
</div>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end" class="dialog-actions">

View File

@ -15,6 +15,7 @@ export class CreateImageComponent implements OnInit {
imageId: string | null = null;
softwareProfiles: any[] = [];
repositories: any[] = [];
partitionInfo: { [key: string]: string } = {};
constructor(
private fb: FormBuilder,
@ -29,7 +30,7 @@ export class CreateImageComponent implements OnInit {
description: [''],
comments: [''],
remotePc: [false],
softwareProfile: ['', Validators.required],
softwareProfile: [''],
imageRepository: ['', Validators.required],
});
}
@ -50,10 +51,11 @@ export class CreateImageComponent implements OnInit {
description: [response.description],
comments: [response.comments],
remotePc: [response.remotePc],
softwareProfile: [response.softwareProfile['@id'], Validators.required],
imageRepository: [response.repository['@id'], Validators.required],
softwareProfile: [response.softwareProfile ? response.softwareProfile['@id'] : null, Validators.required],
imageRepository: [response.imageRepository ? response.imageRepository['@id'] : null, Validators.required],
});
this.imageId = response['@id'];
this.partitionInfo = response.partitionInfo;
},
error: (err) => {
console.error('Error fetching remote calendar:', err);
@ -93,13 +95,13 @@ export class CreateImageComponent implements OnInit {
return;
}
const payload = {
const payload: any = {
name: this.imageForm.value.name,
description: this.imageForm.value.description,
comments: this.imageForm.value.comments,
remotePc: this.imageForm.value.remotePc,
softwareProfile: this.imageForm.value.softwareProfile,
imageRepository: this.imageForm.value.imageRepository,
...(this.imageForm.value.softwareProfile ? { softwareProfile: this.imageForm.value.softwareProfile } : {}),
};
if (this.imageId) {

View File

@ -1,44 +1,15 @@
.title {
font-size: 24px;
font-size: 24px;
}
.images-button-row {
display: flex;
justify-content: flex-start;
margin-top: 16px;
display: flex;
justify-content: flex-start;
margin-top: 16px;
}
.divider {
margin: 20px 0;
}
.lists-container {
padding: 16px;
}
.imagesLists-container {
flex: 1;
}
.card.unidad-card {
height: 100%;
box-sizing: border-box;
}
.image-container {
display: flex;
align-items: center;
margin-bottom: 16px;
border-bottom: 1px solid rgba(122, 122, 122, 0.555);
}
.image-container h4 {
margin: 0;
flex: 1;
}
.image-name{
cursor: pointer;
margin: 20px 0;
}
table {
@ -91,12 +62,28 @@ table {
margin-left: 8px;
}
.example-button-row {
.button-row {
display: table-cell;
max-width: 600px;
}
.example-button-row .mat-mdc-button-base {
.button-row .mat-mdc-button-base {
margin: 8px 8px 8px 0;
}
.status-success {
background-color: #4caf50; /* Verde */
color: white;
}
.status-failed {
background-color: #f44336; /* Rojo */
color: white;
}
.status-pending {
background-color: #ff9800; /* Naranja */
color: white;
}

View File

@ -1,4 +1,5 @@
<div class="header-container">
<div class="header-container">
<h2 class="title">Administrar imágenes</h2>
<div class="images-button-row">
<button mat-flat-button color="primary" (click)="addImage()">Añadir imagen</button>
@ -18,25 +19,49 @@
<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" >
<td mat-cell *matCellDef="let image">
<!-- Chip for 'status' column -->
<ng-container *ngIf="column.columnDef === 'status'">
<mat-chip [ngClass]="{
'status-success': image[column.columnDef] === 'success',
'status-failed': image[column.columnDef] === 'failed',
'status-pending': image[column.columnDef] === 'pending'
}"
>
{{ image[column.columnDef] }}
</mat-chip>
</ng-container>
<!-- Icon for 'remotePc' column -->
<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'">
<!-- Default cell content for other columns -->
<ng-container *ngIf="column.columnDef !== 'status' && column.columnDef !== 'remotePc'">
{{ 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 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)">
<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)="deleteImage($event, image)">
<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 (click)="toggleAction(image, 'get-aux')">Obtener ficheros auxiliares</button>
<button mat-menu-item [disabled]="!image.imageFullsum" (click)="toggleAction(image, 'get-aux')">Eliminar imagen temporalmente</button>
</mat-menu>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>

View File

@ -7,6 +7,9 @@ import { DatePipe } from '@angular/common';
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";
import {ServerInfoDialogComponent} from "../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component";
import {Observable} from "rxjs";
import {InfoImageComponent} from "../ogboot/pxe-images/info-image/info-image/info-image.component";
@Component({
selector: 'app-images',
@ -21,6 +24,7 @@ export class ImagesComponent implements OnInit {
page: number = 0;
loading: boolean = false;
filters: { [key: string]: string } = {};
alertMessage: string | null = null;
datePipe: DatePipe = new DatePipe('es-ES');
columns = [
{
@ -33,11 +37,6 @@ export class ImagesComponent implements OnInit {
header: 'Nombre de imagen',
cell: (image: any) => `${image.name}`
},
{
columnDef: 'softwareProfile',
header: 'Perfil de software',
cell: (image: any) => `${image.softwareProfile?.description}`
},
{
columnDef: 'imageRepository',
header: 'Repositorio',
@ -48,6 +47,16 @@ export class ImagesComponent implements OnInit {
header: 'Remote Pc',
cell: (image: any) => `${image.remotePc}`
},
{
columnDef: 'status',
header: 'Estado',
cell: (image: any) => `${image.status}`
},
{
columnDef: 'imageFullsum',
header: 'Fullsum',
cell: (image: any) => `${image.imageFullsum}`
},
{
columnDef: 'createdAt',
header: 'Fecha de creación',
@ -70,7 +79,7 @@ export class ImagesComponent implements OnInit {
addImage(): void {
const dialogRef = this.dialog.open(CreateImageComponent, {
width: '600px'
width: '800px'
});
dialogRef.afterClosed().subscribe(() => {
@ -96,7 +105,7 @@ export class ImagesComponent implements OnInit {
editImage(event: MouseEvent, image: any): void {
event.stopPropagation();
this.dialog.open(CreateImageComponent, {
width: '600px',
width: '800px',
data: image['@id']
}).afterClosed().subscribe(() => this.search());
}
@ -127,4 +136,46 @@ export class ImagesComponent implements OnInit {
this.length = event.length;
this.search();
}
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']);
}
);
}
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.search()
},
error: (error) => {
this.toastService.error(error.error['hydra:description']);
}
});
break;
default:
console.error('Acción no soportada:', action);
break;
}
}
}

View File

@ -0,0 +1,307 @@
.client-header {
display: flex;
align-items: center;
margin-bottom: 10px;
background-color: #fff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.client-icon {
flex-shrink: 0;
margin-right: 20px;
display: flex;
align-items: center;
justify-content: center;
min-width: 120px;
min-height: 120px;
}
.row-container {
justify-content: space-between;
width: 100%;
}
.table-container {
padding-right: 10px;
}
.charts-wrapper {
width: 100%;
margin-top: 20px;
}
.charts-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between; /* Distribuye el espacio entre los gráficos */
gap: 20px; /* Añade espacio entre los gráficos */
}
.disk-usage {
text-align: center;
flex: 1;
min-width: 200px; /* Ajusta este valor según el tamaño mínimo deseado para cada gráfico */
}
.circular-chart {
max-width: 150px;
max-height: 150px;
margin: 0 auto; /* Centra el gráfico dentro del contenedor */
}
.chart {
display: flex;
justify-content: center;
}
.icon-pc {
font-size: 25px;
color: #3b82f6;
}
.client-title h1 {
font-size: 2rem;
margin-bottom: 10px;
}
.client-title p {
margin: 2px 0;
font-size: 1rem;
color: #666;
}
.client-info {
margin: 20px 0;
}
.form-container {
padding: 20px;
border-radius: 8px;
}
.repository-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.save-button{
align-self: flex-start;
width: 100px !important;
margin-top: 20px;
}
.info-section {
margin-bottom: 30px;
background-color: #fff;
padding: 20px;
border-radius: 12px;
}
.two-column-table {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 15px;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
.table-row {
display: flex;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid #e0e0e0;
}
.column.property {
font-weight: bold;
text-align: left;
width: 45%;
}
.column.value {
text-align: right;
width: 45%;
}
.mat-tab-group {
min-height: 400px;
}
.mat-tab-body-wrapper {
min-height: inherit;
}
.info-section h2 {
font-size: 1.4rem;
margin-bottom: 10px;
color: #0056b3;
}
.second-section {
display: grid;
gap: 20px;
}
.buttons-row {
display: flex;
flex-direction: column;
background-color: #fff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
justify-content: flex-start;
}
.buttons-row button {
margin-bottom: 10px;
width: 100%;
}
.disk-usage-info{
display: flex;
justify-content: start;
margin-top: 10px;
}
p {
margin-left: 15px;
}.dashboard {
padding: 20px;
}
.disk-usage-container {
display: flex;
align-items: flex-start;
margin-bottom: 20px;
}
.header-button-container {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.disk-usage {
flex: 2;
margin-right: 20px;
}
.services-status {
flex: 1;
}
.services-status ul {
list-style-type: none;
padding: 0;
}
.services-status li {
margin: 5px 0;
display: flex;
align-items: center;
}
.status-led {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 10px;
}
.status-led.active {
background-color: green;
}
.status-led.inactive {
background-color: red;
}
table {
width: 100%;
margin-top: 50px;
}
.button-container {
display: flex;
flex-direction: column;
gap: 10px; /* Espacio entre botones */
margin-top: 50px;
}
.btn:first-child {
margin-left: 0;
}
.btn:last-child {
margin-right: 0;
}
.title {
font-size: 24px;
}
.images-button-row {
display: flex;
justify-content: flex-start;
margin-top: 16px;
}
.divider {
margin: 20px 0;
}
table {
width: 100%;
margin-top: 50px;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0 5px;
box-sizing: border-box;
}
.search-string {
flex: 2;
padding: 5px;
}
.search-boolean {
flex: 1;
padding: 5px;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 100px;
padding: 10px;
margin-top: 16px;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
}

View File

@ -0,0 +1,141 @@
<mat-tab-group 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>
<ngx-charts-pie-chart
[view]="view"
[scheme]="colorScheme"
[results]="diskUsageChartData"
[gradient]="gradient"
[doughnut]="isDoughnut"
[labels]="showLabels"
[legend]="showLegend">
</ngx-charts-pie-chart>
<div class="disk-usage-info">
<p>Total: {{ diskUsage.total }}</p>
<p>Ocupado: {{ diskUsage.used }}</p>
<p>Disponible: {{ diskUsage.available }}</p>
<p>Ocupado ( % ): {{ diskUsage.percentage }}</p>
</div>
</div>
<div class="services-status">
<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>
{{ 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>
</li>
</ul>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="Datos generales">
<div class="dashboard">
<div class="header-button-container">
<button mat-flat-button color="primary" (click)="syncRepository()">Sincronizar base de datos</button>
<button mat-flat-button color="accent" (click)="openImageInfoDialog()">Ver Información</button>
</div>
<h2>Editar datos repositorio</h2>
<div *ngIf="loading" class="loading-container">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="!loading" class="client-info form-container">
<form [formGroup]="repositoryForm" (ngSubmit)="save()" class="repository-form">
<mat-form-field appearance="fill" class="form-field">
<mat-label>Nombre del repositorio</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field appearance="fill" class="form-field">
<mat-label>Ip</mat-label>
<input matInput formControlName="ip" name="description">
</mat-form-field>
<mat-form-field appearance="fill" class="form-field">
<mat-label>Comentarios</mat-label>
<input matInput formControlName="comments" name="comments">
</mat-form-field>
<button mat-flat-button color="primary" class="save-button"(click)="save()">Guardar</button>
</form>
</div>
</div>
</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>
</mat-tab>
</mat-tab-group>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MainRepositoryViewComponent } from './main-repository-view.component';
describe('MainRepositoryViewComponent', () => {
let component: MainRepositoryViewComponent;
let fixture: ComponentFixture<MainRepositoryViewComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MainRepositoryViewComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MainRepositoryViewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,323 @@
import {Component, Inject} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import {DataService} from "../../images/data.service";
import {ActivatedRoute} from "@angular/router";
import {MatTableDataSource} from "@angular/material/table";
import {DatePipe} from "@angular/common";
import {ServerInfoDialogComponent} from "../../ogdhcp/og-dhcp-subnets/server-info-dialog/server-info-dialog.component";
import {CreateImageComponent} from "../../images/create-image/create-image.component";
import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component";
import {Observable} from "rxjs";
import {MatDialog} from "@angular/material/dialog";
@Component({
selector: 'app-main-repository-view',
templateUrl: './main-repository-view.component.html',
styleUrl: './main-repository-view.component.css'
})
export class MainRepositoryViewComponent {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
repositoryForm: FormGroup<any>;
repositoryId: string | null = null;
dataSource = new MatTableDataSource<any>();
loading: boolean = true;
diskUsage: any = {};
servicesStatus: any = {};
diskUsageChartData: 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;
repositoryData: any = {};
colorScheme: any = {
domain: ['#FF6384', '#3f51b5']
};
filters: { [key: string]: string } = {};
datePipe: DatePipe = new DatePipe('es-ES');
columns = [
{
columnDef: 'id',
header: 'Id',
cell: (image: any) => `${image.id}`
},
{
columnDef: 'name',
header: 'Nombre de imagen',
cell: (image: any) => `${image.name}`
},
{
columnDef: 'imageRepository',
header: 'Repositorio',
cell: (image: any) => `${image.imageRepository?.name}`
},
{
columnDef: 'remotePc',
header: 'Remote Pc',
cell: (image: any) => `${image.remotePc}`
},
{
columnDef: 'created',
header: 'Creado en repositorio',
cell: (image: any) => `${image.created}`
},
{
columnDef: 'imageFullsum',
header: 'Fullsum',
cell: (image: any) => `${image.imageFullsum}`
},
{
columnDef: 'createdAt',
header: 'Fecha de creación',
cell: (image: any) => `${this.datePipe.transform(image.createdAt, 'dd/MM/yyyy hh:mm:ss')}`
}
];
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
private apiUrl = `${this.baseUrl}/images`;
constructor(
private fb: FormBuilder,
private http: HttpClient,
private toastService: ToastrService,
private dataService: DataService,
private route: ActivatedRoute,
public dialog: MatDialog,
) {
this.repositoryForm = this.fb.group({
name: ['', Validators.required],
ip: [''],
comments: [''],
});
}
ngOnInit() {
this.repositoryId = this.route.snapshot.paramMap.get('id');
this.loading = true
this.load()
this.loadStatus()
}
load(): void {
const url = `${this.baseUrl}/image-repositories/${this.repositoryId}`;
this.http.get(url).subscribe(
(response: any) => {
this.repositoryData = response;
this.repositoryForm = this.fb.group({
name: [response.name, Validators.required],
ip: [response.ip],
comments: [response.comments],
});
this.loading = false;
// Llamar searchImages() solo cuando la data de repository esté cargada
this.searchImages();
},
(error) => {
console.error('Error al cargar los datos del cliente:', error);
this.loading = false;
}
);
}
save(): void {
const payload = {
name: this.repositoryForm.value.name,
ip: this.repositoryForm.value.ip,
comments: this.repositoryForm.value.comments,
};
if (this.repositoryId) {
this.http.put(`${this.baseUrl}/image-repositories/${this.repositoryId}`, payload).subscribe(
(response) => {
this.toastService.success('Imagen editada correctamente');
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
console.error('Error al editar la imagen', error);
}
);
} else {
this.http.post(`${this.baseUrl}/image-repositories`, payload).subscribe(
(response) => {
this.toastService.success('Imagen añadida correctamente');
},
(error) => {
this.toastService.error(error['error']['hydra:description']);
console.error('Error al añadir la imagen', error);
}
);
}
}
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;
this.diskUsage = {
total: diskData.total,
used: diskData.used,
available: diskData.available,
percentage: diskData.used_percentage
};
this.diskUsageChartData = [
{
name: 'Usado',
value: parseFloat(diskData.used)
},
{
name: 'Disponible',
value: parseFloat(diskData.available)
}
];
this.servicesStatus = servicesData;
}, error => {
console.error('Error fetching status', error);
});
}
getServices(): { name: string, status: string }[] {
return Object.keys(this.servicesStatus).map(key => ({
name: key,
status: this.servicesStatus[key]
}));
}
searchImages(): void {
this.loading = true;
this.http.get<any>(`${this.apiUrl}?repository.id=${this.repositoryData.id}&page=${this.page +1 }&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
data => {
this.dataSource.data = data['hydra:member'];
this.length = data['hydra:totalItems'];
this.loading = false;
},
error => {
console.error('Error fetching images', error);
this.loading = false;
}
);
}
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`);
}
syncRepository() {
this.http.post(`${this.apiUrl}/sync`, {})
.subscribe(response => {
this.toastService.success('Sincronización completada');
this.load()
}, error => {
console.error('Error al sincronizar', error);
this.toastService.error('Error al sincronizar');
});
}
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 => {
this.alertMessage = response.output;
this.dialog.open(ServerInfoDialogComponent, {
width: '800px',
data: {
message: this.alertMessage
}
});
},
error => {
console.error('Error al cargar la información del alert', error);
}
);
}
}

View File

@ -10,6 +10,10 @@ mat-toolbar {
background-color: #e0e0e0;
}
.trace-button .mat-icon {
color: #f0f0f0;
}
.navbar-button-row {
display: flex;
justify-content: end;

View File

@ -4,6 +4,7 @@
<mat-icon class="navbar-icon" >menu</mat-icon>
</button>
<div class="navbar-button-row">
<button class="trace-button" routerLink="/commands-logs" mat-button i18n="@@admin"><mat-icon>notifications</mat-icon></button>
<button class="admin-button" *ngIf="isSuperAdmin" mat-button [matMenuTriggerFor]="menu" i18n="@@admin">Administración</button>
<button class="user-button" mat-button *ngIf="!isSuperAdmin" (click)="editUser()" i18n="@@editUser">Editar usuario</button>
<mat-menu #menu="matMenu">

View File

@ -41,12 +41,6 @@
<span i18n="@@gallery">Tareas</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/commands-logs">
<span class="entry">
<mat-icon class="icon">notifications</mat-icon>
<span i18n="@@gallery">Trazas</span>
</span>
</mat-list-item>
</mat-nav-list>
<!-- End commands sub -->