Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop

pull/22/head
Manuel Aranda Rosales 2025-04-28 08:20:16 +02:00
commit d47eaf49a7
19 changed files with 543 additions and 470 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';
@ -143,6 +142,8 @@ import {
import { EditImageComponent } from './components/repositories/edit-image/edit-image.component';
import { ShowGitImagesComponent } from './components/repositories/show-git-images/show-git-images.component';
import { RenameImageComponent } from './components/repositories/rename-image/rename-image.component';
import { ClientDetailsComponent } from './components/groups/shared/client-details/client-details.component';
import { PartitionTypeOrganizatorComponent } from './components/groups/shared/partition-type-organizator/partition-type-organizator.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
@ -204,7 +205,6 @@ registerLocaleData(localeEs, 'es-ES');
TaskLogsComponent,
ServerInfoDialogComponent,
StatusComponent,
ClientMainViewComponent,
ImagesComponent,
CreateImageComponent,
PartitionAssistantComponent,
@ -243,7 +243,9 @@ registerLocaleData(localeEs, 'es-ES');
SaveScriptComponent,
EditImageComponent,
ShowGitImagesComponent,
RenameImageComponent
RenameImageComponent,
ClientDetailsComponent,
PartitionTypeOrganizatorComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,

View File

@ -10,11 +10,13 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./execute-command.component.css']
})
export class ExecuteCommandComponent implements OnInit {
@Input() clientState: string = 'off';
@Input() clientData: any[] = [];
@Input() buttonType: 'icon' | 'text' | 'menu-item' = 'icon';
@Input() buttonText: string = 'Ejecutar Comandos';
@Input() icon: string = 'terminal';
@Input() disabled: boolean = false;
@Input() runScriptContext: string = '';
baseUrl: string;
loading: boolean = true;
@ -45,6 +47,48 @@ export class ExecuteCommandComponent implements OnInit {
ngOnInit(): void {
this.clientData = this.clientData || [];
this.updateCommandStates();
}
ngOnChanges(): void {
this.updateCommandStates();
}
private updateCommandStates(): void {
let states: string[] = [];
if (this.clientData.length > 0) {
states = this.clientData.map(client => client.status);
} else if (this.clientState) {
states = [this.clientState];
}
const allOffOrDisconnected = states.every(state => state === 'off' || state === 'disconnected');
const allSameState = states.every(state => state === states[0]);
const multipleClients = this.clientData.length > 1;
this.arrayCommands = this.arrayCommands.map(command => {
if (allOffOrDisconnected) {
command.disabled = command.slug !== 'power-on';
} else if (allSameState) {
if (states[0] === 'off' || states[0] === 'disconnected') {
command.disabled = command.slug !== 'power-on';
} else {
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'partition', 'run-script'].includes(command.slug);
}
} else {
if (command.slug === 'create-image') {
command.disabled = multipleClients;
} else if (
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'run-script'].includes(command.slug)
) {
command.disabled = false;
} else {
command.disabled = true;
}
}
return command;
});
}
onCommandSelect(action: any): void {
@ -137,7 +181,7 @@ export class ExecuteCommandComponent implements OnInit {
const clientDataToSend = this.clientData.map(client => ({
name: client.name,
mac: client.mac,
uuid: '/clients/'+client.uuid,
uuid: '/clients/' + client.uuid,
status: client.status,
partitions: client.partitions,
firmwareType: client.firmwareType,
@ -161,7 +205,7 @@ export class ExecuteCommandComponent implements OnInit {
const clientDataToSend = this.clientData.map(client => ({
name: client.name,
mac: client.mac,
uuid: '/clients/'+client.uuid,
uuid: '/clients/' + client.uuid,
status: client.status,
partitions: client.partitions,
ip: client.ip
@ -178,18 +222,19 @@ export class ExecuteCommandComponent implements OnInit {
const clientDataToSend = this.clientData.map(client => ({
name: client.name,
mac: client.mac,
uuid: '/clients/'+client.uuid,
uuid: '/clients/' + client.uuid,
status: client.status,
partitions: client.partitions,
ip: client.ip
}));
this.router.navigate(['/clients/run-script'], {
queryParams: { clientData: JSON.stringify(clientDataToSend) }
queryParams: {
clientData: JSON.stringify(clientDataToSend) ,
runScriptContext: this.runScriptContext
}
}).then(() => {
console.log('Navigated to run script with data:', clientDataToSend);
console.log('Navigated to run script with data:', clientDataToSend, 'runScriptContext:', this.runScriptContext);
});
}
}

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

@ -3,7 +3,7 @@
<div class="header-container">
<div class="header-container-title">
<h2>
{{ 'runScript' | translate }}
{{ 'runScript' | translate }} {{this.runScriptContext}}
</h2>
</div>
<div class="button-row">

View File

@ -32,6 +32,7 @@ export class RunScriptAssistantComponent {
newScript: string = '';
selection = new SelectionModel(true, []);
parameterNames: string[] = Object.keys(this.parameters);
runScriptContext: string = '';
constructor(
private http: HttpClient,
@ -46,6 +47,9 @@ export class RunScriptAssistantComponent {
if (params['clientData']) {
this.clientData = JSON.parse(params['clientData']);
}
if (params['runScriptContext']) {
this.runScriptContext = params['runScriptContext'];
}
});
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
this.clientData.forEach((client: { selected: boolean; status: string}) => {

View File

@ -198,11 +198,16 @@
<mat-icon>delete</mat-icon>
<span>{{ 'delete' | translate }}</span>
</button>
<button mat-menu-item (click)="openPartitionTypeModal($event, selectedNode)">
<mat-icon>storage</mat-icon>
<span>{{ 'partitions' | translate }}</span>
</button>
<app-execute-command [clientData]="selectedNode?.clients || []" [buttonType]="'menu-item'"
[buttonText]="'Ejecutar comandos'" [icon]="'terminal'" [disabled]="!((selectedNode?.clients ?? []).length > 0)">
[buttonText]="'Ejecutar comandos'" [icon]="'terminal'"
[disabled]="!((selectedNode?.clients ?? []).length > 0)" [runScriptContext]="selectedNode?.name || ''"
[runScriptContext]="getRunScriptContext(selectedNode?.clients || [])">
</app-execute-command>
</mat-menu>
</div>
<mat-divider [vertical]="true"></mat-divider>
@ -218,7 +223,8 @@
</span>
<div class="view-type-container">
<app-execute-command [clientData]="selection.selected" [buttonType]="'text'"
[buttonText]="'Ejecutar comandos'" [disabled]="selection.selected.length === 0"></app-execute-command>
[buttonText]="'Ejecutar comandos'" [disabled]="selection.selected.length === 0"
[runScriptContext]="getRunScriptContext(selection.selected)"></app-execute-command>
<mat-button-toggle-group name="viewType" aria-label="View Type" [hideSingleSelectionIndicator]="true"
(change)="toggleView($event.value)">
<mat-button-toggle value="list" [disabled]="currentView === 'list'">
@ -260,8 +266,10 @@
<span class="client-ip">{{ client.mac }}</span>
<div class="action-icons">
<app-execute-command [clientData]="[client]" [buttonType]="'icon'" [icon]="'terminal'"
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"></app-execute-command>
<app-execute-command [clientState]="client.status" [clientData]="[client]"
[buttonType]="'icon'" [icon]="'terminal'"
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
[runScriptContext]="getRunScriptContext([client])"></app-execute-command>
<button
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
@ -395,8 +403,10 @@
mat-icon-button [matMenuTriggerFor]="clientMenu" color="primary">
<mat-icon>more_vert</mat-icon>
</button>
<app-execute-command [clientData]="[client]" [buttonType]="'icon'" [icon]="'terminal'"
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))">
<app-execute-command [clientState]="client.status" [clientData]="[client]" [buttonType]="'icon'"
[icon]="'terminal'"
[disabled]="selection.selected.length > 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
[runScriptContext]="getRunScriptContext([client])">
</app-execute-command>
<mat-menu #clientMenu="matMenu">
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
@ -442,4 +452,4 @@
</div>
</ng-template>
</div>
</div>

View File

@ -1,4 +1,4 @@
import {Component, OnInit, OnDestroy, ViewChild, QueryList, ViewChildren, ChangeDetectorRef} from '@angular/core';
import { Component, OnInit, OnDestroy, ViewChild, QueryList, ViewChildren, ChangeDetectorRef } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
@ -26,6 +26,8 @@ import { Subject } from 'rxjs';
import { ConfigService } from '@services/config.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MatMenuTrigger } from '@angular/material/menu';
import { ClientDetailsComponent } from './shared/client-details/client-details.component';
import { PartitionTypeOrganizatorComponent } from './shared/partition-type-organizator/partition-type-organizator.component';
enum NodeType {
OrganizationalUnit = 'organizational-unit',
@ -85,6 +87,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
{ value: 'windows-session', name: 'Windows Session' },
{ value: 'busy', name: 'Ocupado' },
{ value: 'mac', name: 'Mac' },
{ value: 'disconnected', name: 'Desconectado' }
];
displayedColumns: string[] = ['select', 'status', 'ip', 'firmwareType', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
@ -181,7 +184,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
const index = this.arrayClients.findIndex(client => client['@id'] === clientUuid);
if (index !== -1) {
const updatedClient = {...this.arrayClients[index], status};
const updatedClient = { ...this.arrayClients[index], status };
this.arrayClients = [
...this.arrayClients.slice(0, index),
updatedClient,
@ -608,7 +611,10 @@ export class GroupsComponent implements OnInit, OnDestroy {
onShowClientDetail(event: MouseEvent, client: Client): void {
event.stopPropagation();
this.router.navigate(['clients', client.uuid], { state: { clientData: client } });
this.dialog.open(ClientDetailsComponent, {
width: '1200px',
data: { clientData: client },
})
}
@ -823,4 +829,36 @@ export class GroupsComponent implements OnInit, OnDestroy {
clientSearchStatusInput.value = null;
this.fetchClientsForNode(this.selectedNode);
}
getRunScriptContext(clientData: any[]): string {
const selectedClientNames = clientData.map(client => client.name);
if (clientData.length === 1) {
return selectedClientNames[0];
} else if (
clientData.length === this.selectedClients.data.length &&
selectedClientNames.every(name => this.selectedClients.data.some(c => c.name === name))
) {
return this.selectedNode?.name || '';
} else if (clientData.length > 1) {
return selectedClientNames.join(', ');
} else if (this.selectedNode?.name && clientData.length === 0) {
return this.selectedNode.name;
}
return '';
}
openPartitionTypeModal(event: MouseEvent, node: TreeNode | null = null): void {
event.stopPropagation();
const simplifiedClientsData = node?.clients?.map((client: any) => ({
name: client.name,
partitions: client.partitions
}));
this.dialog.open(PartitionTypeOrganizatorComponent, {
width: '1200px',
data: simplifiedClientsData
});
}
}

View File

@ -0,0 +1,110 @@
.modal-content {
max-height: 80vh;
overflow-y: auto;
background-color: #fff;
display: flex;
flex-direction: column;
padding: 1em 4em 2em 4em;
}
.title {
margin-bottom: 1.5em;
}
.client-info-card {
background-color: #f1f1f1;
padding: 24px;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
margin-bottom: 60px;
}
.info-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.info-column {
display: flex;
flex-direction: column;
gap: 16px;
}
.info-pair {
display: flex;
flex-direction: column;
}
.label {
font-size: 0.9rem;
font-weight: 550;
color: #3f51b5;
}
.value {
font-size: 1rem;
color: #222;
word-break: break-word;
}
.disk-container {
display: flex;
flex-direction: column;
gap: 32px;
}
.disk-layout {
display: flex;
flex-direction: row;
gap: 32px;
flex-wrap: wrap;
}
.table-container {
flex: 1 1 500px;
overflow-x: auto;
}
.charts-container {
display: flex;
flex-wrap: wrap;
gap: 24px;
flex: 1 1 300px;
justify-content: flex-start;
}
.mat-elevation-z8 {
width: 100%;
border-radius: 8px;
}
table {
border: 2px solid #f3f3f3;
border-radius: 0px !important;
box-shadow: none;
}
.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 260px;
min-width: 240px;
padding: 16px;
background: #f9f9f9;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.chart {
width: 100%;
height: 160px;
margin-bottom: 12px;
}

View File

@ -0,0 +1,66 @@
<mat-dialog-content class="modal-content">
<app-loading [isLoading]="loading"></app-loading>
<div class="client-container" *ngIf="!loading">
<div class="header-container">
<h2 class="title">{{ 'clientDetailsTitle' | translate }} {{ clientData.name }}</h2>
</div>
<div class="client-info-card">
<div class="info-columns">
<div class="info-column">
<div class="info-pair" *ngFor="let data of generalData">
<div class="label">{{ data?.property }}</div>
<div class="value">{{ data?.value || '--' }}</div>
</div>
</div>
<div class="info-column">
<div class="info-pair" *ngFor="let data of networkData">
<div class="label">{{ data?.property }}</div>
<div class="value">{{ data?.value || '--' }}</div>
</div>
</div>
</div>
</div>
<div class="section-header">
<h2 class="title" i18n="@@adminImagesTitle">Discos/Particiones</h2>
</div>
<div class="disk-container">
<div class="disk-layout">
<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" [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">
</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>
</div>
</mat-dialog-content>

View File

@ -0,0 +1,52 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ClientDetailsComponent } from './client-details.component';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ToastrModule } from 'ngx-toastr';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { ConfigService } from '@services/config.service';
import { LoadingComponent } from 'src/app/shared/loading/loading.component';
import { TranslateModule } from '@ngx-translate/core';
import { MatDividerModule } from '@angular/material/divider';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatTableModule } from '@angular/material/table';
describe('ClientDetailsComponent', () => {
let component: ClientDetailsComponent;
let fixture: ComponentFixture<ClientDetailsComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [ClientDetailsComponent, LoadingComponent],
imports: [
MatDialogModule,
HttpClientTestingModule,
ToastrModule.forRoot(),
MatDividerModule,
TranslateModule.forRoot(),
CommonModule,
MatTableModule,
MatProgressSpinnerModule
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } },
{ provide: ConfigService, useValue: mockConfigService }
]
})
.compileComponents();
fixture = TestBed.createComponent(ClientDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,4 +1,5 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DatePipe } from "@angular/common";
import { MatTableDataSource } from "@angular/material/table";
@ -14,11 +15,11 @@ interface ClientInfo {
}
@Component({
selector: 'app-client-main-view',
templateUrl: './client-main-view.component.html',
styleUrl: './client-main-view.component.css'
selector: 'app-client-details',
templateUrl: './client-details.component.html',
styleUrl: './client-details.component.css'
})
export class ClientMainViewComponent implements OnInit {
export class ClientDetailsComponent {
baseUrl: string;
@ViewChild('assistantContainer') assistantContainer!: ElementRef;
clientUuid: string;
@ -34,7 +35,7 @@ export class ClientMainViewComponent implements OnInit {
partitions: any[] = [];
commands: any[] = [];
chartDisk: any[] = [];
view: [number, number] = [300, 200];
view: [number, number] = [260, 160];
showLegend: boolean = true;
arrayCommands: any[] = [
@ -93,7 +94,8 @@ export class ClientMainViewComponent implements OnInit {
private dialog: MatDialog,
private configService: ConfigService,
private router: Router,
private toastService: ToastrService
private toastService: ToastrService,
@Inject(MAT_DIALOG_DATA) public data: { clientData: any }
) {
this.baseUrl = this.configService.apiUrl;
const url = window.location.href;
@ -102,14 +104,18 @@ export class ClientMainViewComponent implements OnInit {
}
ngOnInit() {
this.clientData = history.state.clientData['@id'];
this.loadClient(this.clientData)
this.loadCommands()
this.calculateDiskUsage();
this.loading = false;
if (this.data && this.data.clientData) {
this.clientData = this.data.clientData;
this.updateGeneralData();
this.updateNetworkData();
this.calculateDiskUsage();
this.loadPartitions();
this.loading = false;
} else {
console.error('No se recibieron datos del cliente.');
}
}
loadClient = (uuid: string) => {
this.http.get<any>(`${this.baseUrl}${uuid}`).subscribe({
next: data => {
@ -125,10 +131,6 @@ export class ClientMainViewComponent implements OnInit {
});
}
navigateToGroups() {
this.router.navigate(['/groups']);
}
updateGeneralData() {
this.generalData = [
{ property: 'Nombre', value: this.clientData?.name || '' },
@ -205,7 +207,12 @@ export class ClientMainViewComponent implements OnInit {
}
loadPartitions(): void {
this.http.get<any>(`${this.baseUrl}/partitions?client.id=${this.clientData?.id}&order[diskNumber, partitionNumber]=ASC`).subscribe({
if (!this.clientData?.id) {
console.error('El ID del cliente no está disponible.');
return;
}
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;

View File

@ -0,0 +1,46 @@
.modal-content {
max-height: 80vh;
overflow-y: auto;
background-color: #fff;
display: flex;
flex-direction: column;
padding: 1em 4em 2em 4em;
box-sizing: border-box;
}
.client-section {
margin-bottom: 2em;
}
.client-title {
font-size: 1.5em;
font-weight: 500;
color: #3f51b5;
margin-bottom: 1em;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 0.25em;
}
.partition-table {
width: 100%;
border-spacing: 0;
border-collapse: collapse;
}
.partition-table th,
.partition-table td {
padding: 0.75em 1em;
text-align: left;
font-size: 0.95em;
border-bottom: 1px solid #ddd;
}
.partition-table th {
background-color: #f5f5f5;
font-weight: 500;
color: #444;
}
.partition-table tr:hover td {
background-color: #f9f9f9;
}

View File

@ -0,0 +1,42 @@
<mat-dialog-content class="modal-content">
<div *ngFor="let client of simplifiedData" class="client-section">
<h3 class="client-title">{{ client.name }}</h3>
<table mat-table [dataSource]="client.partitions" class="mat-elevation-z1 partition-table">
<ng-container matColumnDef="diskNumber">
<th mat-header-cell *matHeaderCellDef>Disco</th>
<td mat-cell *matCellDef="let element">{{ element.diskNumber }}</td>
</ng-container>
<ng-container matColumnDef="partitionNumber">
<th mat-header-cell *matHeaderCellDef>Partición</th>
<td mat-cell *matCellDef="let element">{{ element.partitionNumber }}</td>
</ng-container>
<ng-container matColumnDef="partitionCode">
<th mat-header-cell *matHeaderCellDef>Tipo</th>
<td mat-cell *matCellDef="let element">{{ element.partitionCode }}</td>
</ng-container>
<ng-container matColumnDef="size">
<th mat-header-cell *matHeaderCellDef>Tamaño</th>
<td mat-cell *matCellDef="let element">{{ element.size }}</td>
</ng-container>
<ng-container matColumnDef="filesystem">
<th mat-header-cell *matHeaderCellDef>Fyle System</th>
<td mat-cell *matCellDef="let element">{{ element.filesystem }}</td>
</ng-container>
<ng-container matColumnDef="memoryUsage">
<th mat-header-cell *matHeaderCellDef>Memoria</th>
<td mat-cell *matCellDef="let element">{{ element.memoryUsage }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-dialog-content>

View File

@ -0,0 +1,42 @@
import { TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { PartitionTypeOrganizatorComponent } from './partition-type-organizator.component';
import { MatTableModule } from '@angular/material/table';
describe('PartitionTypeOrganizatorComponent', () => {
let component: PartitionTypeOrganizatorComponent;
let fixture: any;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PartitionTypeOrganizatorComponent],
imports: [
MatDialogModule,
MatTableModule
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{
provide: MAT_DIALOG_DATA,
useValue: [
{
name: 'Client 1',
partitions: [
{ diskNumber: 1, partitionNumber: 1, partitionCode: 'EXT4', size: 1024, filesystem: 'ext4', memoryUsage: 50 },
{ diskNumber: 1, partitionNumber: 2, partitionCode: 'NTFS', size: 2048, filesystem: 'ntfs', memoryUsage: 75 }
]
}
]
}
]
}).compileComponents();
fixture = TestBed.createComponent(PartitionTypeOrganizatorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
@Component({
selector: 'app-partition-type-organizator',
templateUrl: './partition-type-organizator.component.html',
styleUrl: './partition-type-organizator.component.css'
})
export class PartitionTypeOrganizatorComponent implements OnInit {
constructor(@Inject(MAT_DIALOG_DATA) public data: any) { }
public simplifiedData: any[] = [];
displayedColumns: string[] = ['diskNumber', 'partitionNumber', 'partitionCode', 'size', 'filesystem', 'memoryUsage'];
ngOnInit(): void {
console.log('Data recibida en modal:', this.data);
this.simplifiedData = this.data.map((client: any) => ({
name: client.name,
partitions: client.partitions.map((p: any) => ({
diskNumber: p.diskNumber,
partitionNumber: p.partitionNumber,
partitionCode: p.partitionCode,
size: p.size,
filesystem: p.filesystem,
memoryUsage: p.memoryUsage
}))
}));
}
}

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",
@ -482,5 +482,6 @@
"processes": "Processes",
"usedPercentageLabel": "Used",
"errorLoadingData": "Error fetching data. Service not available",
"repositoryTitleStep": "On this screen you can manage image repositories."
"repositoryTitleStep": "On this screen you can manage image repositories.",
"partitions": "Particiones"
}

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",
@ -484,5 +484,6 @@
"processes": "Procesos",
"usedPercentageLabel": "Usado",
"errorLoadingData": "Error al cargar los datos. Servicio inactivo",
"repositoryTitleStep": "En esta pantalla se pueden gestionar los repositorios de imágenes."
"repositoryTitleStep": "En esta pantalla se pueden gestionar los repositorios de imágenes.",
"partitions": "Particiones"
}