refs #1091. CreateImage logic
parent
8152d310f8
commit
4dfc45b125
|
@ -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",
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
||||
<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"
|
||||
|
|
|
@ -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)];
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -4,39 +4,36 @@
|
|||
<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>
|
||||
|
||||
<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 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>
|
||||
</mat-tab>
|
||||
<mat-tab label="Discos/Particiones">
|
||||
<div class="header-container">
|
||||
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
|
||||
</div>
|
||||
|
@ -64,25 +61,14 @@
|
|||
<div class="charts-wrapper">
|
||||
<ng-container *ngIf="diskUsageData && diskUsageData.length > 0">
|
||||
<div class="charts-row">
|
||||
<div *ngFor="let disk of diskUsageData" class="disk-usage">
|
||||
<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>
|
||||
<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>
|
||||
|
@ -90,4 +76,5 @@
|
|||
</ng-container>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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']);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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']);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@
|
|||
<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>
|
||||
|
@ -55,7 +55,8 @@
|
|||
<td>
|
||||
<input
|
||||
type="number"
|
||||
[(ngModel)]="partition.memoryUsage"
|
||||
[(ngModel)]="partition.percentage"
|
||||
(input)="updatePartitionPercentage(disk.diskNumber, j, partition.percentage)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
@ -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) => {}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -51,17 +51,27 @@
|
|||
<div class="client-mac">{{ client.mac }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'status'">
|
||||
<mat-chip>
|
||||
<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>
|
||||
|
||||
<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;">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -12,35 +12,6 @@
|
|||
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;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
<div class="header-container">
|
||||
<h2 class="title">Administrar imágenes</h2>
|
||||
<div class="images-button-row">
|
||||
|
@ -19,24 +20,48 @@
|
|||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,6 +10,10 @@ mat-toolbar {
|
|||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.trace-button .mat-icon {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
.navbar-button-row {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 -->
|
||||
|
||||
|
|
Loading…
Reference in New Issue