refs #1931 Refactor ClientMainViewComponent and related files: remove component and styles, update routing, and adjust dialog dimensions for improved client detail display
testing/ogGui-multibranch/pipeline/head This commit looks good Details

develop
Lucas Lara García 2025-04-23 14:22:12 +02:00
parent 70e21c6ca2
commit f016c66d55
10 changed files with 22 additions and 753 deletions

View File

@ -18,8 +18,6 @@ import { CommandsComponent } from './components/commands/main-commands/commands.
import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component';
import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component';
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
import { ImagesComponent } from './components/images/images.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";
@ -67,7 +65,6 @@ const routes: Routes = [
{ path: 'clients/deploy-image', component: DeployImageComponent },
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent },
{ path: 'clients/run-script', component: RunScriptAssistantComponent },
{ path: 'clients/:id', component: ClientMainViewComponent },
{ path: 'clients/:id/create-image', component: CreateClientImageComponent },
{ path: 'repositories', component: RepositoriesComponent },
{ path: 'repository/:id', component: MainRepositoryViewComponent },

View File

@ -89,7 +89,6 @@ import { CreateTaskComponent } from './components/commands/commands-task/create-
import { DetailTaskComponent } from './components/commands/commands-task/detail-task/detail-task.component';
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
import { MatSliderModule } from '@angular/material/slider';
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
import { ImagesComponent } from './components/images/images.component';
import { CreateImageComponent } from './components/images/create-image/create-image.component';
import { CreateClientImageComponent } from './components/groups/components/client-main-view/create-image/create-image.component';
@ -205,7 +204,6 @@ registerLocaleData(localeEs, 'es-ES');
TaskLogsComponent,
ServerInfoDialogComponent,
StatusComponent,
ClientMainViewComponent,
ImagesComponent,
CreateImageComponent,
PartitionAssistantComponent,

View File

@ -1,351 +0,0 @@
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
.client-container {
flex-grow: 1;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0rem 1rem 0rem 0.5rem;
}
.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;
gap: 20px;
}
.disk-usage {
text-align: center;
flex: 1;
min-width: 200px;
}
.circular-chart {
max-width: 150px;
max-height: 150px;
margin: 0 auto;
}
.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;
border-radius: 12px;
background-color: #f5f7fa;
padding: 20px;
border: 2px solid #d1d9e6;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
}
.info-section {
background-color: #fff;
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;
}
.info-section p {
font-size: 1rem;
margin: 5px 0;
}
.second-section {
display: grid;
gap: 20px;
}
.client-button-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
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%;
}
.circular-chart {
display: block;
margin: 0 auto;
max-width: 100%;
max-height: 150px;
}
.circle-bg {
fill: none;
stroke: #f0f0f0;
stroke-width: 3.8;
}
.circle {
fill: none;
stroke-width: 3.8;
stroke: #00bfa5;
stroke-linecap: round;
animation: progress 1s ease-out forwards;
}
.percentage {
fill: #333;
font-size: 0.7rem;
text-anchor: middle;
}
.disk-usage h3 {
margin: 0 0 10px 0;
font-size: 1.2rem;
color: #333;
}
@keyframes progress {
0% {
stroke-dasharray: 0, 100;
}
}
.assistants-container {
background-color: #fff;
margin-top: 10px;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.circular-chart {
display: block;
margin: 0 auto;
max-width: 100%;
max-height: 150px;
}
.circle-bg {
fill: none;
stroke: #f0f0f0;
stroke-width: 3.8;
}
.circle {
fill: none;
stroke-width: 3.8;
stroke-linecap: round;
animation: progress 1s ease-out forwards;
}
.partition-0 {
stroke: #00bfa5;
}
.partition-1 {
stroke: #ff6f61;
}
.partition-2 {
stroke: #ffb400;
}
.partition-3 {
stroke: #3498db;
}
.percentage {
fill: #333;
font-size: 0.7rem;
text-anchor: middle;
}
.disk-container {
display: flex;
flex-direction: row;
gap: 20px;
background-color: #f5f7fa;
border: 2px solid #d1d9e6;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
flex-wrap: wrap;
justify-content: center;
align-items: stretch;
margin-bottom: 20px;
}
.table-container {
flex: 3;
display: flex;
justify-content: center;
align-items: center;
}
table.mat-elevation-z8 {
width: 100%;
max-width: 800px;
background-color: white;
border-radius: 8px;
overflow: hidden;
}
.mat-header-cell {
background-color: #d1d9e6 !important;
color: #333;
font-weight: bold;
text-align: center;
}
.mat-cell {
text-align: center;
}
.mat-chip {
font-weight: bold;
color: white;
}
.charts-container {
flex: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.disk-usage {
background-color: white;
padding: 15px;
border-radius: 8px;
justify-self: center;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
text-align: center;
}
.chart {
display: flex;
justify-content: center;
}
.back-button {
display: flex;
align-items: center;
gap: 5px;
margin-left: 0.5em;
background-color: #3f51b5;
color: white;
padding: 8px 18px 8px 8px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: transform 0.3s ease;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
.back-button:hover:not(:disabled) {
background-color: #485ac0d7;
}
.back-button:disabled {
background-color: #ced0df;
cursor: not-allowed;
}

View File

@ -1,71 +0,0 @@
<app-loading [isLoading]="loading"></app-loading>
<div class="client-container">
<div class="header-container">
<h2 class="title">{{ 'clientDetailsTitle' | translate }} {{ clientData.name }}</h2>
<div class="client-button-row">
<button class="back-button" (click)="navigateToGroups()">
<mat-icon>arrow_back</mat-icon>
{{ 'back' | translate }}
</button>
</div>
</div>
<mat-divider></mat-divider>
<div *ngIf="!loading" class="client-info">
<div class="info-section">
<div class="two-column-table">
<div class="table-row" *ngFor="let clientData of generalData">
<div class="column property">{{ clientData?.property }}</div>
<div class="column value">{{ clientData?.value }}</div>
</div>
</div>
<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>
</div>
</div>
<div class="header-container">
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
</div>
<div class="disk-container">
<div class="table-container">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef !== 'size'">
{{ column.cell(image) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'size'">
<mat-chip color="primary">
{{ (image.size / 1024).toFixed(2) }} GB
</mat-chip>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<div class="charts-container">
<ng-container *ngIf="diskUsageData && diskUsageData.length > 0">
<div *ngFor="let disk of chartDisk" class="disk-usage">
<ngx-charts-pie-chart class="chart" [view]="view" [results]="disk.chartData" [doughnut]="true">
</ngx-charts-pie-chart>
<h3>Disco {{ disk.diskNumber }}</h3>
<p>Usado: {{ (disk.used).toFixed(2) }} GB ({{ disk.percentage }}%)</p>
<p>Total: {{ disk.total }} GB</p>
</div>
</ng-container>
</div>
</div>
</div>

View File

@ -1,313 +0,0 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DatePipe } from "@angular/common";
import { MatTableDataSource } from "@angular/material/table";
import { MatDialog } from "@angular/material/dialog";
import { Router } from "@angular/router";
import { ToastrService } from "ngx-toastr";
import { ManageClientComponent } from "../../shared/clients/manage-client/manage-client.component";
import { ConfigService } from '@services/config.service';
interface ClientInfo {
property: string;
value: any;
}
@Component({
selector: 'app-client-main-view',
templateUrl: './client-main-view.component.html',
styleUrl: './client-main-view.component.css'
})
export class ClientMainViewComponent implements OnInit {
baseUrl: string;
@ViewChild('assistantContainer') assistantContainer!: ElementRef;
clientUuid: string;
clientData: any = {};
isPartitionAssistantVisible: boolean = false;
isBootImageVisible: boolean = false;
isDiskUsageVisible: boolean = true;
dataSource = new MatTableDataSource<any>();
generalData: ClientInfo[] = [];
networkData: ClientInfo[] = [];
classroomData: ClientInfo[] = [];
diskUsageData: any[] = [];
partitions: any[] = [];
commands: any[] = [];
chartDisk: any[] = [];
view: [number, number] = [300, 200];
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 imagen', slug: 'create-image' },
{ name: 'Clonar/desplegar imagen', 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 comando', slug: 'run-script' },
];
datePipe: DatePipe = new DatePipe('es-ES');
columns = [
{
columnDef: 'diskNumber',
header: 'Disco',
cell: (partition: any) => `${partition.diskNumber}`,
},
{
columnDef: 'partitionNumber',
header: 'Particion',
cell: (partition: any) => `${partition.partitionNumber}`
},
{
columnDef: 'description',
header: 'Sistema de ficheros',
cell: (partition: any) => `${partition.filesystem}`
},
{
columnDef: 'size',
header: 'Tamaño',
cell: (partition: any) => `${(partition.size / 1024).toFixed(2)} GB`
},
{
columnDef: 'memoryUsage',
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;
loading: boolean = true;
constructor(
private http: HttpClient,
private dialog: MatDialog,
private configService: ConfigService,
private router: Router,
private toastService: ToastrService
) {
this.baseUrl = this.configService.apiUrl;
const url = window.location.href;
const segments = url.split('/');
this.clientUuid = segments[segments.length - 1];
}
ngOnInit() {
this.clientData = history.state.clientData['@id'];
this.loadClient(this.clientData)
this.loadCommands()
this.calculateDiskUsage();
this.loading = false;
}
loadClient = (uuid: string) => {
this.http.get<any>(`${this.baseUrl}${uuid}`).subscribe({
next: data => {
this.clientData = data;
this.updateGeneralData();
this.updateNetworkData();
this.loadPartitions()
this.loading = false;
},
error: error => {
console.error('Error al obtener el cliente:', error);
}
});
}
navigateToGroups() {
this.router.navigate(['/groups']);
}
updateGeneralData() {
this.generalData = [
{ property: 'Nombre', value: this.clientData?.name || '' },
{ property: 'IP', value: this.clientData?.ip || '' },
{ property: 'MAC', value: this.clientData?.mac || '' },
{ property: 'Nº de serie', value: this.clientData?.serialNumber || '' },
{ property: 'Netiface', value: this.clientData?.netiface || this.clientData?.organizationalUnit?.networkSettings?.netiface || '' },
{ property: 'Perfil hardware', value: this.clientData?.hardwareProfile?.description || '' },
];
}
updateNetworkData() {
this.networkData = [
{ property: 'Padre', value: this.clientData?.organizationalUnit?.name || '' },
{ property: 'Pxe', value: this.clientData?.template?.name || '' },
{ property: 'Remote Pc', value: this.clientData.remotePc || '' },
{ property: 'Subred', value: this.clientData?.subnet || '' },
{ property: 'OGlive', value: this.clientData?.ogLive?.name || '' },
{ property: 'Repositorio', value: this.clientData?.repository?.name || '' },
];
}
calculateDiskUsage() {
const diskUsageMap = new Map<number, { total: number, used: number, partitions: any[] }>();
this.partitions.forEach((partition: any) => {
const diskNumber = partition.diskNumber;
if (!diskUsageMap.has(diskNumber)) {
diskUsageMap.set(diskNumber, { total: 0, used: 0, partitions: [] });
}
const diskData = diskUsageMap.get(diskNumber);
if (partition.partitionNumber === 0) {
diskData!.total = Number((partition.size / 1024).toFixed(2));
} else {
diskData!.used += Number((partition.size / 1024).toFixed(2));
diskData!.partitions.push(partition);
}
});
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;
}
onEditClick(event: MouseEvent, uuid: string): void {
event.stopPropagation();
const dialogRef = this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' });
dialogRef.afterClosed().subscribe();
}
loadPartitions(): void {
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[diskNumber, partitionNumber]=ASC`).subscribe({
next: data => {
const filteredPartitions = data['hydra:member'].filter((partition: any) => partition.partitionNumber !== 0);
this.dataSource = filteredPartitions;
this.partitions = filteredPartitions;
this.calculateDiskUsage();
},
error: error => {
console.error('Error al obtener las particiones:', error);
}
});
}
loadCommands(): void {
this.http.get<any>(`${this.baseUrl}/commands?`).subscribe({
next: data => {
this.commands = data['hydra:member'];
},
error: error => {
console.error('Error al obtener las particiones:', error);
}
});
}
onCommandSelect(action: any): void {
if (action === 'partition') {
this.openPartitionAssistant();
}
if (action === 'create-image') {
this.openCreateImageAssistant();
}
if (action === 'deploy-image') {
this.openDeployImageAssistant();
}
if (action === 'reboot') {
this.rebootClient();
}
if (action === 'power-off') {
this.powerOffClient();
}
if (action === 'power-on') {
this.powerOnClient();
}
}
rebootClient(): void {
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/reboot`, {}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
},
error => {
this.toastService.error('Error de conexión con el cliente');
}
);
}
powerOnClient(): void {
const payload = {
client: this.clientData['@id']
}
this.http.post(`${this.baseUrl}${this.clientData.repository['@id']}/wol`, payload).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
},
error => {
this.toastService.error('Error de conexión con el cliente');
}
);
}
powerOffClient(): void {
this.http.post(`${this.baseUrl}/clients/server/${this.clientData.uuid}/power-off`, {}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
},
error => {
this.toastService.error('Error de conexión con el cliente');
}
);
}
openPartitionAssistant(): void {
this.router.navigate([`/clients/${this.clientData.uuid}/partition-assistant`]).then(r => {
console.log('navigated', r);
});
}
openCreateImageAssistant(): void {
this.router.navigate([`/clients/${this.clientData.uuid}/create-image`]).then(r => {
console.log('navigated', r);
});
}
openDeployImageAssistant(): void {
this.router.navigate([`/clients/deploy-image`]).then(r => {
console.log('navigated', r);
});
}
}

View File

@ -611,7 +611,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
onShowClientDetail(event: MouseEvent, client: Client): void {
event.stopPropagation();
this.dialog.open(ClientDetailsComponent, {
width: '600px',
width: '900px',
data: { clientData: client },
})
}

View File

@ -4,19 +4,19 @@
background-color: #fff;
display: flex;
flex-direction: column;
padding: 1em 4em 2em 4em;
}
.section-header {
margin-bottom: 8px;
.title {
margin-bottom: 1.5em;
}
.client-info-card {
background-color: #e4e4e4;
background-color: #f1f1f1;
padding: 24px;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
margin-bottom: 2em;
margin-top: 1.5em;
margin-bottom: 60px;
}
.info-columns {
@ -58,8 +58,7 @@
overflow-x: auto;
border-radius: 8px;
background: #fff;
padding: 16px;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.mat-elevation-z8 {
@ -73,16 +72,25 @@
gap: 24px;
}
.charts-container.single-disk {
justify-content: center;
}
.charts-container.single-disk .disk-usage {
max-width: 400px;
flex: 0 1 auto;
}
.disk-usage {
flex: 1 1 300px;
padding: 16px;
background: #f9f9f9;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.chart {
width: 100%;
height: 200px;
margin-bottom: 12px;
}
}

View File

@ -48,7 +48,7 @@
</table>
</div>
<div class="charts-container">
<div class="charts-container" [ngClass]="{'single-disk': chartDisk.length === 1}">
<ng-container *ngIf="diskUsageData && diskUsageData.length > 0">
<div *ngFor="let disk of chartDisk" class="disk-usage">
<ngx-charts-pie-chart class="chart" [view]="view" [results]="disk.chartData" [doughnut]="true">
@ -59,6 +59,7 @@
</div>
</ng-container>
</div>
</div>
</div>
</mat-dialog-content>

View File

@ -272,7 +272,7 @@
"classroomOption": "Classroom",
"clientsGroupOption": "PC Groups",
"roomMapOption": "Classroom map",
"clientDetailsTitle": "Client details",
"clientDetailsTitle": "Details of",
"commandsButton": "Commands",
"networkPropertiesTab": "Network properties",
"disksPartitionsTitle": "Disks/Partitions",

View File

@ -276,7 +276,7 @@
"classroomOption": "Aula",
"clientsGroupOption": "Grupos de PCs",
"roomMapOption": "Plano de aula",
"clientDetailsTitle": "Datos de cliente",
"clientDetailsTitle": "Datos de",
"commandsButton": "Comandos",
"excludeParentChanges": "Excluir cambios de las unidades organizativas superiores",
"networkPropertiesTab": "Propiedades de red",