commit
f840272e0d
|
@ -1,4 +1,13 @@
|
|||
# Changelog
|
||||
## [0.14.0] - 2025-06-02
|
||||
### Added
|
||||
- Se ha añadido funcionalidad de usuarios/roles para separar las vistas segun los permisos.
|
||||
- Nuevo boton de "mover" en la pantalla de grupos, para mover equipos entre aulas y grupos.
|
||||
|
||||
### Improved
|
||||
- Se ha completado la opcion de inicion de sesion y eliminar imagen cache.
|
||||
|
||||
---
|
||||
## [0.13.1] - 2025-05-23
|
||||
### Changed
|
||||
- Desactivado temporalmente la funcionalidad de ogGit.
|
||||
|
|
|
@ -3,28 +3,26 @@ import { RouterModule, Routes } from '@angular/router';
|
|||
import { MainLayoutComponent } from './layout/main-layout/main-layout.component';
|
||||
import { AuthLayoutComponent } from './layout/auth-layout/auth-layout.component';
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { DashboardComponent } from './components/dashboard/dashboard.component';
|
||||
import { PageNotFoundComponent } from './shared/page-not-found/page-not-found.component';
|
||||
import { AdminComponent } from './components/admin/admin.component';
|
||||
import { UsersComponent } from './components/admin/users/users/users.component';
|
||||
import { RolesComponent } from './components/admin/roles/roles/roles.component';
|
||||
import { GroupsComponent } from './components/groups/groups.component';
|
||||
import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.component';
|
||||
import { PxeComponent } from './components/ogboot/pxe/pxe.component';
|
||||
import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component';
|
||||
import {OgbootStatusComponent} from "./components/ogboot/ogboot-status/ogboot-status.component";
|
||||
import { OgbootStatusComponent } from "./components/ogboot/ogboot-status/ogboot-status.component";
|
||||
import { CalendarComponent } from "./components/calendar/calendar.component";
|
||||
import { CommandsComponent } from './components/commands/main-commands/commands.component';
|
||||
import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component';
|
||||
import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component';
|
||||
import { TaskLogsComponent } from './components/task-logs/task-logs.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";
|
||||
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";
|
||||
import {
|
||||
PartitionAssistantComponent
|
||||
} from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component";
|
||||
import {RepositoriesComponent} from "./components/repositories/repositories.component";
|
||||
import { RepositoriesComponent } from "./components/repositories/repositories.component";
|
||||
import {
|
||||
CreateClientImageComponent
|
||||
} from "./components/groups/components/client-main-view/create-image/create-image.component";
|
||||
|
@ -34,44 +32,44 @@ import {
|
|||
import {
|
||||
MainRepositoryViewComponent
|
||||
} from "./components/repositories/main-repository-view/main-repository-view.component";
|
||||
import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
|
||||
import {MenusComponent} from "./components/menus/menus.component";
|
||||
import {OgDhcpSubnetsComponent} from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||
import {StatusComponent} from "./components/ogdhcp/status/status.component";
|
||||
import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component";
|
||||
import { MenusComponent } from "./components/menus/menus.component";
|
||||
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||
import { StatusComponent } from "./components/ogdhcp/status/status.component";
|
||||
import {
|
||||
RunScriptAssistantComponent
|
||||
} from "./components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component";
|
||||
import { roleGuard } from './guards/role.guard';
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
||||
{ path: '', component: MainLayoutComponent,
|
||||
{
|
||||
path: '', component: MainLayoutComponent,
|
||||
children: [
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: 'admin', component: AdminComponent },
|
||||
{ path: 'users', component: UsersComponent },
|
||||
{ path: 'env-vars', component: EnvVarsComponent },
|
||||
{ path: 'user-groups', component: RolesComponent },
|
||||
{ path: 'users', component: UsersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'env-vars', component: EnvVarsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'roles', component: RolesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin'] } },
|
||||
{ path: 'groups', component: GroupsComponent },
|
||||
{ path: 'pxe-images', component: PXEimagesComponent },
|
||||
{ path: 'pxe', component: PxeComponent },
|
||||
{ path: 'pxe-boot-file', component: PxeBootFilesComponent },
|
||||
{ path: 'ogboot-status', component: OgbootStatusComponent },
|
||||
{ path: 'subnets', component: OgDhcpSubnetsComponent },
|
||||
{ path: 'ogdhcp-status', component: StatusComponent },
|
||||
{ path: 'commands', component: CommandsComponent },
|
||||
{ path: 'commands-groups', component: CommandsGroupsComponent },
|
||||
{ path: 'commands-task', component: CommandsTaskComponent },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent },
|
||||
{ path: 'calendars', component: CalendarComponent },
|
||||
{ path: 'clients/deploy-image', component: DeployImageComponent },
|
||||
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent },
|
||||
{ path: 'clients/run-script', component: RunScriptAssistantComponent },
|
||||
{ path: 'clients/:id/create-image', component: CreateClientImageComponent },
|
||||
{ path: 'repositories', component: RepositoriesComponent },
|
||||
{ path: 'repository/:id', component: MainRepositoryViewComponent },
|
||||
{ path: 'software', component: SoftwareComponent },
|
||||
{ path: 'software-profiles', component: SoftwareProfileComponent },
|
||||
{ path: 'operative-systems', component: OperativeSystemComponent },
|
||||
{ path: 'menus', component: MenusComponent },
|
||||
{ path: 'pxe-images', component: PXEimagesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'pxe', component: PxeComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'pxe-boot-file', component: PxeBootFilesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'ogboot-status', component: OgbootStatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'subnets', component: OgDhcpSubnetsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'ogdhcp-status', component: StatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'calendars', component: CalendarComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/run-script', component: RunScriptAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'clients/:id/create-image', component: CreateClientImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'repositories', component: RepositoriesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'repository/:id', component: MainRepositoryViewComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'software', component: SoftwareComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'software-profiles', component: SoftwareProfileComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'operative-systems', component: OperativeSystemComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'menus', component: MenusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -17,7 +17,6 @@ import { MatIconModule } from '@angular/material/icon';
|
|||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AdminComponent } from './components/admin/admin.component';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
|
@ -151,6 +150,9 @@ import { CreateTaskScriptComponent } from './components/commands/commands-task/c
|
|||
import { ViewParametersModalComponent } from './components/commands/commands-task/show-task-script/view-parameters-modal/view-parameters-modal.component';
|
||||
import { OutputDialogComponent } from './components/task-logs/output-dialog/output-dialog.component';
|
||||
import { ClientTaskLogsComponent } from './components/task-logs/client-task-logs/client-task-logs.component';
|
||||
import { BootSoPartitionComponent } from './components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component';
|
||||
import { RemoveCacheImageComponent } from './components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component';
|
||||
import { ChangeParentComponent } from './components/groups/shared/change-parent/change-parent.component';
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
|
@ -170,7 +172,6 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
HeaderComponent,
|
||||
SidebarComponent,
|
||||
LoginComponent,
|
||||
AdminComponent,
|
||||
MainLayoutComponent,
|
||||
UsersComponent,
|
||||
RolesComponent,
|
||||
|
@ -259,7 +260,10 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
CreateTaskScriptComponent,
|
||||
ViewParametersModalComponent,
|
||||
OutputDialogComponent,
|
||||
ClientTaskLogsComponent
|
||||
ClientTaskLogsComponent,
|
||||
BootSoPartitionComponent,
|
||||
RemoveCacheImageComponent,
|
||||
ChangeParentComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
/* Estilos del contenedor para centrar los botones */
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Estilos del contenedor de cada botón y texto */
|
||||
.button-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
/* Estilos del texto debajo de los botones */
|
||||
span{
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Media query para hacer los botones responsive */
|
||||
@media (max-width: 900px) {
|
||||
button {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
button {
|
||||
height: 90px;
|
||||
width: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
button {
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
span{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<div class="container">
|
||||
<button class="action-button" routerLink="/users">
|
||||
<mat-icon>group</mat-icon>
|
||||
<span>{{ 'labelUsers' | translate }}</span>
|
||||
</button>
|
||||
<button class="action-button" routerLink="/user-groups">
|
||||
<mat-icon>admin_panel_settings</mat-icon>
|
||||
<span>{{ 'labelRoles' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
|
@ -1,48 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AdminComponent } from './admin.component';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
describe('AdminComponent', () => {
|
||||
let component: AdminComponent;
|
||||
let fixture: ComponentFixture<AdminComponent>;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AdminComponent],
|
||||
imports: [
|
||||
RouterTestingModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
TranslateModule.forRoot()
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('debería crear el componente', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('debería renderizar dos botones', () => {
|
||||
const buttons = fixture.nativeElement.querySelectorAll('button');
|
||||
expect(buttons.length).toBe(2);
|
||||
});
|
||||
|
||||
it('debería tener un botón con routerLink a "/users"', () => {
|
||||
const button = fixture.nativeElement.querySelector('button[routerLink="/users"]');
|
||||
expect(button).toBeTruthy();
|
||||
expect(button.querySelector('mat-icon').textContent.trim()).toBe('group');
|
||||
});
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
templateUrl: './admin.component.html',
|
||||
styleUrl: './admin.component.css'
|
||||
})
|
||||
|
||||
export class AdminComponent {
|
||||
|
||||
}
|
||||
|
|
@ -33,15 +33,6 @@
|
|||
<ng-container *ngIf="column.columnDef !== 'commands'">
|
||||
{{ column.cell(commandGroup) }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'commands'" joyrideStep="viewCommandsStep" text="{{ 'viewCommandsStepText' | translate }}">
|
||||
<button class="action-button" [matMenuTriggerFor]="menu">{{ 'viewCommands' | translate }}</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item *ngFor="let command of commandGroup.commands">
|
||||
{{ command.name }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
|
|
@ -6,6 +6,11 @@
|
|||
padding: 20px;
|
||||
}
|
||||
|
||||
.select-task {
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,40 @@
|
|||
<h2 mat-dialog-title class="dialog-title">{{ editing ? ('editTask' | translate) : ('createTask' | translate) }}</h2>
|
||||
<h2 mat-dialog-title class="dialog-title">
|
||||
{{ editing ? ('editTask' | translate) : ('createTask' | translate) }}
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<form *ngIf="taskForm && !loading" [formGroup]="taskForm" class="task-form">
|
||||
<!-- Toggle entre crear o añadir -->
|
||||
<mat-radio-group *ngIf="data?.source === 'assistant'" [(ngModel)]="taskMode" class="task-mode-selection" name="taskMode">
|
||||
<mat-radio-button value="create">Crear tarea</mat-radio-button>
|
||||
<mat-radio-button value="add">Introducir en tarea existente</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
|
||||
<!-- Selección de tarea existente -->
|
||||
<div *ngIf="taskMode === 'add'" class="select-task">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione una tarea</mat-label>
|
||||
<mat-select [(ngModel)]="selectedExistingTask" name="existingTask">
|
||||
<mat-option *ngFor="let task of existingTasks" [value]="task">{{ task.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Orden de ejecución</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[(ngModel)]="executionOrder"
|
||||
name="executionOrder"
|
||||
min="1"
|
||||
placeholder="Introduce el orden"
|
||||
>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de nueva tarea -->
|
||||
<form *ngIf="taskMode === 'create' && taskForm && !loading" [formGroup]="taskForm" class="task-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" placeholder="{{ 'nameLabel' | translate }}">
|
||||
|
@ -17,17 +48,17 @@
|
|||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Ámbito</mat-label>
|
||||
<mat-select formControlName="scope" class="full-width" (selectionChange)="onScopeChange($event.value)">
|
||||
<mat-option [value]="'organizational-unit'">Unidad Organizativa</mat-option>
|
||||
<mat-option [value]="'classrooms-group'">Grupo de aulas</mat-option>
|
||||
<mat-option [value]="'classroom'">Aulas</mat-option>
|
||||
<mat-option [value]="'clients-group'">Grupos de clientes</mat-option>
|
||||
<mat-select formControlName="scope" (selectionChange)="onScopeChange($event.value)">
|
||||
<mat-option value="organizational-unit">Unidad Organizativa</mat-option>
|
||||
<mat-option value="classrooms-group">Grupo de aulas</mat-option>
|
||||
<mat-option value="classroom">Aulas</mat-option>
|
||||
<mat-option value="clients-group">Grupos de clientes</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" >
|
||||
<mat-select formControlName="organizationalUnit">
|
||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
|
@ -35,11 +66,18 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox *ngIf="!editing" formControlName="scheduleAfterCreate">¿Quieres programar la tarea al finalizar su creación?</mat-checkbox>
|
||||
<mat-checkbox *ngIf="!editing" formControlName="scheduleAfterCreate">
|
||||
¿Quieres programar la tarea al finalizar su creación?
|
||||
</mat-checkbox>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" [disabled]="!taskForm.valid" (click)="saveTask()">{{ 'buttonSave' | translate }}</button>
|
||||
<button
|
||||
class="submit-button"
|
||||
(click)="taskMode === 'create' ? saveTask() : addToExistingTask()"
|
||||
>
|
||||
{{ 'buttonSave' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
|
@ -25,6 +25,10 @@ export class CreateTaskComponent implements OnInit {
|
|||
clients: any[] = [];
|
||||
allOrganizationalUnits: any[] = [];
|
||||
loading: boolean = false;
|
||||
taskMode: 'create' | 'add' = 'create';
|
||||
existingTasks: any[] = [];
|
||||
selectedExistingTask: string | null = null;
|
||||
executionOrder: number | null = null;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
|
@ -53,6 +57,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
this.loadIndividualCommands(),
|
||||
this.loadOrganizationalUnits(),
|
||||
this.startUnitsFilter(),
|
||||
this.loadTasks()
|
||||
];
|
||||
|
||||
Promise.all(observables).then(() => {
|
||||
|
@ -90,6 +95,21 @@ export class CreateTaskComponent implements OnInit {
|
|||
})
|
||||
}
|
||||
|
||||
loadTasks(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.http.get<any>(`${this.apiUrl}?page=1&itemsPerPage=100`).subscribe(
|
||||
(data) => {
|
||||
this.existingTasks = data['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar las tareas existentes');
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onScopeChange(scope: string): void {
|
||||
this.filterUnits(scope).subscribe(filteredUnits => {
|
||||
this.availableOrganizationalUnits = filteredUnits;
|
||||
|
@ -161,6 +181,27 @@ export class CreateTaskComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
addToExistingTask() {
|
||||
if (!this.selectedExistingTask) {
|
||||
this.toastr.error('Debes seleccionar una tarea existente.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.executionOrder == null || this.executionOrder < 1) {
|
||||
this.toastr.error('Debes introducir un orden de ejecución válido (mayor que 0).');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
taskId: this.selectedExistingTask,
|
||||
executionOrder: this.executionOrder
|
||||
};
|
||||
|
||||
this.toastr.success('Tarea actualizada con éxito');
|
||||
this.dialogRef.close(data);
|
||||
}
|
||||
|
||||
|
||||
saveTask(): void {
|
||||
if (this.taskForm.invalid) {
|
||||
this.toastr.error('Por favor, rellene todos los campos obligatorios');
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selected-client {
|
||||
background-color: #a0c2e5 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
<h2 mat-dialog-title> Seleccionar particion para arrancar SO</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<div *ngIf="!loading" class="select-container">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Clientes </mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Listado de clientes para arrancar un SO
|
||||
<mat-icon>desktop_windows</mat-icon>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="toggleSelectAll()">
|
||||
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clients-grid">
|
||||
<div *ngFor="let client of data.clients" class="client-item">
|
||||
<div class="client-card"
|
||||
(click)="client.status === 'og-live' && toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}" >
|
||||
|
||||
<img
|
||||
[src]="'assets/images/computer_' + client.status + '.svg'"
|
||||
alt="Client Icon"
|
||||
class="client-image" />
|
||||
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name | slice:0:20 }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
|
||||
<mat-radio-button [value]="client"
|
||||
color="primary"
|
||||
[disabled]="!client.selected"
|
||||
(click)="$event.stopPropagation()">
|
||||
Modelo
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div *ngIf="!loading" class="partition-table-container">
|
||||
<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"
|
||||
[disabled]="!row.operativeSystem"
|
||||
>
|
||||
<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">
|
||||
<ng-container *ngIf="column.columnDef !== 'size'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.size }} MB</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
|
||||
</div>
|
|
@ -0,0 +1,82 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BootSoPartitionComponent } from './boot-so-partition.component';
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from "@angular/material/dialog";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatCheckboxModule } from "@angular/material/checkbox";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
import { MatMenuModule } from "@angular/material/menu";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { MatTableModule } from "@angular/material/table";
|
||||
import { MatSelectModule } from "@angular/material/select";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
import { ToastrModule, ToastrService } from "ngx-toastr";
|
||||
import { TranslateModule } from "@ngx-translate/core";
|
||||
import { DataService } from "../../data.service";
|
||||
import { provideHttpClient } from "@angular/common/http";
|
||||
import { provideHttpClientTesting } from "@angular/common/http/testing";
|
||||
import { ConfigService } from "@services/config.service";
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
|
||||
describe('BootSoPartitionComponent', () => {
|
||||
let component: BootSoPartitionComponent;
|
||||
let fixture: ComponentFixture<BootSoPartitionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [BootSoPartitionComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
MatExpansionModule,
|
||||
MatMenuModule,
|
||||
BrowserAnimationsModule,
|
||||
MatTableModule,
|
||||
MatDividerModule,
|
||||
MatSelectModule,
|
||||
MatIconModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {
|
||||
clients: []
|
||||
}
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BootSoPartitionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,154 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { ConfigService } from "@services/config.service";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-boot-so-partition',
|
||||
templateUrl: './boot-so-partition.component.html',
|
||||
styleUrl: './boot-so-partition.component.css'
|
||||
})
|
||||
export class BootSoPartitionComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
selectedPartition: any = null;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
clientId: string | null = null;
|
||||
selectedClients: any[] = [];
|
||||
selectedModelClient: any = null;
|
||||
filteredPartitions: any[] = [];
|
||||
allSelected: boolean = false;
|
||||
clientData: any[] = [];
|
||||
loading: boolean = false;
|
||||
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: 'partitionCode',
|
||||
header: 'Tipo de partición',
|
||||
cell: (partition: any) => partition.partitionCode
|
||||
},
|
||||
{
|
||||
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)];
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: { clients: any },
|
||||
private dialogRef: MatDialogRef<BootSoPartitionComponent>,
|
||||
private configService: ConfigService,
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.clientId = this.data.clients?.length ? this.data.clients[0]['@id'] : null;
|
||||
|
||||
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === 'og-live') {
|
||||
client.selected = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.selectedClients = this.data.clients.filter(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
);
|
||||
|
||||
this.selectedModelClient = this.data.clients.find(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
) || null;
|
||||
|
||||
if (this.selectedModelClient) {
|
||||
this.loadPartitions(this.selectedModelClient);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
loadPartitions(client: any) {
|
||||
const url = `${this.baseUrl}${client.uuid}`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar los datos del cliente:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toggleClientSelection(client: any) {
|
||||
client.selected = !client.selected;
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
updateSelectedClients() {
|
||||
this.selectedClients = this.data.clients.filter(
|
||||
(client: { selected: boolean; state: string }) => client.selected && client.state === "og-live"
|
||||
);
|
||||
|
||||
if (!this.selectedClients.includes(this.selectedModelClient)) {
|
||||
this.selectedModelClient = null;
|
||||
this.filteredPartitions = [];
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
this.allSelected = !this.allSelected;
|
||||
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === "og-live") {
|
||||
client.selected = this.allSelected;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this.loading = true;
|
||||
this.http.post(`${this.baseUrl}/clients/server/boot-client`, {
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
partition: this.selectedPartition['@id']
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,10 @@ import { HttpClient } from '@angular/common/http';
|
|||
import { Router } from "@angular/router";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { BootSoPartitionComponent } from "./boot-so-partition/boot-so-partition.component";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { RemoveCacheImageComponent } from "./remove-cache-image/remove-cache-image.component";
|
||||
import { AuthService } from '@services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-execute-command',
|
||||
|
@ -27,7 +31,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
{ translationKey: 'executeCommands.login', slug: 'login', disabled: true },
|
||||
{ translationKey: 'executeCommands.createImage', slug: 'create-image', disabled: false },
|
||||
{ translationKey: 'executeCommands.deployImage', slug: 'deploy-image', disabled: false },
|
||||
{ translationKey: 'executeCommands.deleteImageCache', slug: 'delete-image-cache', disabled: true },
|
||||
{ translationKey: 'executeCommands.deleteImageCache', slug: 'remove-cache-image', disabled: false },
|
||||
{ translationKey: 'executeCommands.partition', slug: 'partition', disabled: false },
|
||||
{ translationKey: 'executeCommands.softwareInventory', slug: 'software-inventory', disabled: true },
|
||||
{ translationKey: 'executeCommands.hardwareInventory', slug: 'hardware-inventory', disabled: true },
|
||||
|
@ -40,13 +44,18 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService
|
||||
private toastService: ToastrService,
|
||||
public auth: AuthService,
|
||||
private dialog: MatDialog,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.clientData = this.clientData || [];
|
||||
const allowed = this.getAllowedCommandsByRole();
|
||||
this.arrayCommands = this.arrayCommands.filter(c => allowed.includes(c.slug));
|
||||
|
||||
this.updateCommandStates();
|
||||
}
|
||||
|
||||
|
@ -54,6 +63,34 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
this.updateCommandStates();
|
||||
}
|
||||
|
||||
private getAllowedCommandsByRole(): string[] {
|
||||
const role = this.auth.userCategory;
|
||||
|
||||
const permissions: Record<string, string[]> = {
|
||||
'super-admin': ['*'],
|
||||
'ou-admin': ['*'],
|
||||
'ou-operator': [
|
||||
'power-on',
|
||||
'power-off',
|
||||
'reboot',
|
||||
'login',
|
||||
'deploy-image',
|
||||
'software-inventory',
|
||||
'hardware-inventory',
|
||||
'remove-cache-image',
|
||||
'partition'
|
||||
],
|
||||
'ou-minimal': [
|
||||
'power-on',
|
||||
'power-off'
|
||||
]
|
||||
};
|
||||
|
||||
const allowed = permissions[role] || [];
|
||||
return allowed.includes('*') ? this.arrayCommands.map(c => c.slug) : allowed;
|
||||
}
|
||||
|
||||
|
||||
private updateCommandStates(): void {
|
||||
let states: string[] = [];
|
||||
|
||||
|
@ -74,13 +111,13 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
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);
|
||||
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'remove-cache-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)
|
||||
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'remove-cache-image', 'run-script'].includes(command.slug)
|
||||
) {
|
||||
command.disabled = false;
|
||||
} else {
|
||||
|
@ -123,6 +160,10 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
if (action === 'power-on') {
|
||||
this.powerOnClient();
|
||||
}
|
||||
|
||||
if (action === 'remove-cache-image') {
|
||||
this.removeImageCache();
|
||||
}
|
||||
}
|
||||
|
||||
rebootClient(): void {
|
||||
|
@ -139,16 +180,51 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
}
|
||||
|
||||
loginClient(): void {
|
||||
this.http.post(`${this.baseUrl}/clients/server/login-client`, {
|
||||
clients: this.clientData.map((client: any) => client['@id'])
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
},
|
||||
error => {
|
||||
this.toastService.error('Error de conexión con el cliente');
|
||||
const clientDataToSend = this.clientData.map(client => ({
|
||||
name: client.name,
|
||||
mac: client.mac,
|
||||
uuid: '/clients/' + client.uuid,
|
||||
status: client.status,
|
||||
partitions: client.partitions,
|
||||
firmwareType: client.firmwareType,
|
||||
ip: client.ip
|
||||
}));
|
||||
|
||||
const dialogRef = this.dialog.open(BootSoPartitionComponent, {
|
||||
width: '70vw',
|
||||
height: 'auto',
|
||||
data: { clients: clientDataToSend }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success('Petición de arranque de SO enviada correctamente');
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
removeImageCache(): void {
|
||||
const clientDataToSend = this.clientData.map(client => ({
|
||||
name: client.name,
|
||||
mac: client.mac,
|
||||
uuid: '/clients/' + client.uuid,
|
||||
status: client.status,
|
||||
partitions: client.partitions,
|
||||
firmwareType: client.firmwareType,
|
||||
ip: client.ip
|
||||
}));
|
||||
|
||||
const dialogRef = this.dialog.open(RemoveCacheImageComponent, {
|
||||
width: '70vw',
|
||||
height: 'auto',
|
||||
data: { clients: clientDataToSend }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.toastService.success('Petición de borrado de caché de imagen enviada correctamente');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
powerOnClient(): void {
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.select-container {
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.clients-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.client-card {
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.client-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mat-expansion-panel-header-description {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selected-client {
|
||||
background-color: #a0c2e5 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.client-ip {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
<h2 mat-dialog-title> Seleccionar imagen para eliminar de la cache</h2>
|
||||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
|
||||
<div *ngIf="!loading" class="select-container">
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Clientes </mat-panel-title>
|
||||
<mat-panel-description>
|
||||
Listado de clientes para arrancar un SO
|
||||
<mat-icon>desktop_windows</mat-icon>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="toggleSelectAll()">
|
||||
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clients-grid">
|
||||
<div *ngFor="let client of data.clients" class="client-item">
|
||||
<div class="client-card"
|
||||
(click)="client.status === 'og-live' && toggleClientSelection(client)"
|
||||
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}" >
|
||||
|
||||
<img
|
||||
[src]="'assets/images/computer_' + client.status + '.svg'"
|
||||
alt="Client Icon"
|
||||
class="client-image" />
|
||||
|
||||
<div class="client-details">
|
||||
<span class="client-name">{{ client.name | slice:0:20 }}</span>
|
||||
<span class="client-ip">{{ client.ip }}</span>
|
||||
<span class="client-ip">{{ client.mac }}</span>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
|
||||
<mat-radio-button [value]="client"
|
||||
color="primary"
|
||||
[disabled]="!client.selected"
|
||||
(click)="$event.stopPropagation()">
|
||||
Modelo
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
|
||||
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
|
||||
|
||||
<div *ngIf="!loading" class="partition-table-container">
|
||||
<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 imagen</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-radio-group
|
||||
[(ngModel)]="selectedPartition"
|
||||
[disabled]="!row.operativeSystem"
|
||||
>
|
||||
<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">
|
||||
<ng-container *ngIf="column.columnDef !== 'size' && column.columnDef !== 'operativeSystem'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'size'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.size }} MB</span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'operativeSystem'">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span> {{ image.operativeSystem?.name }} </span>
|
||||
<span style="font-size: 0.75rem; color: gray;">{{ image.image?.name}} </span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
|
||||
</div>
|
|
@ -0,0 +1,92 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RemoveCacheImageComponent } from './remove-cache-image.component';
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from "@angular/material/dialog";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatCheckboxModule } from "@angular/material/checkbox";
|
||||
import { MatButtonModule } from "@angular/material/button";
|
||||
import { MatMenuModule } from "@angular/material/menu";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { MatTableModule } from "@angular/material/table";
|
||||
import { MatSelectModule } from "@angular/material/select";
|
||||
import { MatIconModule } from "@angular/material/icon";
|
||||
import { ToastrModule, ToastrService } from "ngx-toastr";
|
||||
import { TranslateModule } from "@ngx-translate/core";
|
||||
import { DataService } from "../../data.service";
|
||||
import { provideHttpClient } from "@angular/common/http";
|
||||
import { provideHttpClientTesting } from "@angular/common/http/testing";
|
||||
import { ConfigService } from "@services/config.service";
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
|
||||
describe('RemoveCacheImageComponent', () => {
|
||||
let component: RemoveCacheImageComponent;
|
||||
let fixture: ComponentFixture<RemoveCacheImageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RemoveCacheImageComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatExpansionModule,
|
||||
BrowserAnimationsModule,
|
||||
MatTableModule,
|
||||
MatDividerModule,
|
||||
MatSelectModule,
|
||||
MatRadioModule,
|
||||
MatIconModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {
|
||||
clients: [
|
||||
{
|
||||
'@id': '/clients/1',
|
||||
uuid: 'client-uuid-1',
|
||||
selected: false,
|
||||
status: 'og-live',
|
||||
state: 'og-live'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RemoveCacheImageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,154 @@
|
|||
import {Component, Inject} from '@angular/core';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-remove-cache-image',
|
||||
templateUrl: './remove-cache-image.component.html',
|
||||
styleUrl: './remove-cache-image.component.css'
|
||||
})
|
||||
export class RemoveCacheImageComponent {
|
||||
baseUrl: string;
|
||||
selectedPartition: any = null;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
clientId: string | null = null;
|
||||
selectedClients: any[] = [];
|
||||
selectedModelClient: any = null;
|
||||
filteredPartitions: any[] = [];
|
||||
allSelected: boolean = false;
|
||||
clientData: any[] = [];
|
||||
loading: boolean = false;
|
||||
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: 'partitionCode',
|
||||
header: 'Tipo de partición',
|
||||
cell: (partition: any) => partition.partitionCode
|
||||
},
|
||||
{
|
||||
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)];
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: { clients: any },
|
||||
private dialogRef: MatDialogRef<RemoveCacheImageComponent>,
|
||||
private configService: ConfigService,
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.clientId = this.data.clients?.length ? this.data.clients[0]['@id'] : null;
|
||||
|
||||
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === 'og-live') {
|
||||
client.selected = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.selectedClients = this.data.clients.filter(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
);
|
||||
|
||||
this.selectedModelClient = this.data.clients.find(
|
||||
(client: { status: string }) => client.status === 'og-live'
|
||||
) || null;
|
||||
|
||||
if (this.selectedModelClient) {
|
||||
this.loadPartitions(this.selectedModelClient);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
}
|
||||
|
||||
loadPartitions(client: any) {
|
||||
const url = `${this.baseUrl}${client.uuid}`;
|
||||
this.http.get(url).subscribe(
|
||||
(response: any) => {
|
||||
if (response.partitions) {
|
||||
this.dataSource.data = response.partitions.filter((partition: any) => {
|
||||
return partition.partitionNumber !== 0 && partition.image;
|
||||
});
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error al cargar los datos del cliente:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toggleClientSelection(client: any) {
|
||||
client.selected = !client.selected;
|
||||
this.updateSelectedClients();
|
||||
}
|
||||
|
||||
updateSelectedClients() {
|
||||
this.selectedClients = this.data.clients.filter(
|
||||
(client: { selected: boolean; state: string }) => client.selected && client.state === "og-live"
|
||||
);
|
||||
|
||||
if (!this.selectedClients.includes(this.selectedModelClient)) {
|
||||
this.selectedModelClient = null;
|
||||
this.filteredPartitions = [];
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
this.allSelected = !this.allSelected;
|
||||
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
|
||||
if (client.status === "og-live") {
|
||||
client.selected = this.allSelected;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
this.loading = true;
|
||||
this.http.post(`${this.baseUrl}/clients/server/remove-cache-image`, {
|
||||
clients: this.selectedClients.map((client: any) => client.uuid),
|
||||
partition: this.selectedPartition['@id']
|
||||
}).subscribe(
|
||||
response => {
|
||||
this.toastService.success('Cliente actualizado correctamente');
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<p>dashboard works!</p>
|
|
@ -1,23 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
|
||||
describe('DashboardComponent', () => {
|
||||
let component: DashboardComponent;
|
||||
let fixture: ComponentFixture<DashboardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DashboardComponent]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,10 +0,0 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrl: './dashboard.component.css'
|
||||
})
|
||||
export class DashboardComponent {
|
||||
|
||||
}
|
|
@ -10,11 +10,15 @@
|
|||
</h4>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button class="action-button" [disabled]="!allSelected || !selectedModelClient || !selectedImage || !selectedMethod || !selectedPartition" (click)="save()">Ejecutar</button>
|
||||
<button class="action-button"
|
||||
[disabled]="!isFormValid()"
|
||||
(click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button mat-stroked-button color="accent" [disabled]="!allSelected || !selectedModelClient || !selectedImage || !selectedMethod || !selectedPartition" (click)="openScheduleModal()">
|
||||
<button mat-stroked-button color="accent"
|
||||
[disabled]="!isFormValid()"
|
||||
(click)="openScheduleModal()">
|
||||
<mat-icon>schedule</mat-icon> Opciones de programación
|
||||
</button>
|
||||
</div>
|
||||
|
@ -144,18 +148,21 @@
|
|||
<div *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')" class="input-group">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Puerto</mat-label>
|
||||
<input matInput [(ngModel)]="mcastPort" name="mcastPort" type="number">
|
||||
<input matInput [(ngModel)]="mcastPort" name="mcastPort" type="number"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Dirección</mat-label>
|
||||
<input matInput [(ngModel)]="mcastIp" name="mcastIp">
|
||||
<input matInput [(ngModel)]="mcastIp" name="mcastIp"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label i18n="@@mcastModeLabel">Modo Multicast</mat-label>
|
||||
<mat-select [(ngModel)]="mcastMode" name="mcastMode">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
|
||||
<mat-select [(ngModel)]="mcastMode" name="mcastMode"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
|
@ -163,25 +170,29 @@
|
|||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Velocidad</mat-label>
|
||||
<input matInput [(ngModel)]="mcastSpeed" name="mcastSpeed" type="number">
|
||||
<input matInput [(ngModel)]="mcastSpeed" name="mcastSpeed" type="number"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Máximo Clientes</mat-label>
|
||||
<input matInput [(ngModel)]="mcastMaxClients" name="mcastMaxClients" type="number">
|
||||
<input matInput [(ngModel)]="mcastMaxClients" name="mcastMaxClients" type="number"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Tiempo Máximo de Espera</mat-label>
|
||||
<input matInput [(ngModel)]="mcastMaxTime" name="mcastMaxTime" type="number">
|
||||
<input matInput [(ngModel)]="mcastMaxTime" name="mcastMaxTime" type="number"
|
||||
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isMethod('p2p')" class="input-group">
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
|
||||
<mat-select [(ngModel)]="p2pMode" name="p2pMode">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
<mat-select [(ngModel)]="p2pMode" name="p2pMode"
|
||||
[required]="isMethod('p2p')">
|
||||
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
|
@ -189,7 +200,8 @@
|
|||
|
||||
<mat-form-field appearance="fill" class="input-field">
|
||||
<mat-label>Semilla</mat-label>
|
||||
<input matInput [(ngModel)]="p2pTime" name="p2pTime" type="number">
|
||||
<input matInput [(ngModel)]="p2pTime" name="p2pTime" type="number"
|
||||
[required]="isMethod('p2p')">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -332,13 +332,34 @@ export class DeployImageComponent implements OnInit{
|
|||
});
|
||||
}
|
||||
|
||||
isFormValid(): boolean {
|
||||
if (!this.allSelected || !this.selectedModelClient || !this.selectedImage || !this.selectedMethod || !this.selectedPartition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isMethod('udpcast') || this.isMethod('uftp') || this.isMethod('udpcast-direct')) {
|
||||
if (!this.mcastPort || !this.mcastIp || !this.mcastMode || !this.mcastSpeed || !this.mcastMaxClients || !this.mcastMaxTime) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isMethod('p2p')) {
|
||||
if (!this.p2pMode || !this.p2pTime) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
openScheduleModal(): void {
|
||||
const dialogRef = this.dialog.open(CreateTaskComponent, {
|
||||
width: '800px',
|
||||
data: {
|
||||
scope: this.runScriptContext.type,
|
||||
organizationalUnit: this.runScriptContext['@id']
|
||||
organizationalUnit: this.runScriptContext['@id'],
|
||||
source: 'assistant'
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -359,9 +380,9 @@ export class DeployImageComponent implements OnInit{
|
|||
};
|
||||
|
||||
this.http.post(`${this.baseUrl}/command-task-scripts`, {
|
||||
commandTask: result['@id'],
|
||||
commandTask: result['taskId'] ? result['taskId']['@id'] : result['@id'],
|
||||
parameters: payload,
|
||||
order: 1,
|
||||
order: result['executionOrder'] || 1,
|
||||
type: 'deploy-image',
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
|
|
|
@ -267,5 +267,18 @@ button.remove-btn:hover {
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
.instructions-box {
|
||||
margin-top: 15px;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ccc;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.instructions-textarea textarea {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -13,6 +13,12 @@
|
|||
<button class="action-button" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="save()">Ejecutar</button>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="action-button" (click)="generateInstructions()">
|
||||
Generar instrucciones
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button mat-stroked-button color="accent" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="openScheduleModal()">
|
||||
<mat-icon>schedule</mat-icon> Opciones de programación
|
||||
|
@ -86,6 +92,13 @@
|
|||
</div>
|
||||
|
||||
<div class="partition-assistant" *ngIf="selectedDisk">
|
||||
|
||||
<div *ngIf="generatedInstructions" class="instructions-box">
|
||||
<mat-form-field class="instructions-textarea" appearance="fill" style="width: 100%;">
|
||||
<textarea matInput rows="10" readonly [value]="generatedInstructions"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="row-button">
|
||||
<button class="action-button" [disabled]="partitionCode === 'MSDOS'" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
|
||||
<mat-chip *ngIf="selectedModelClient.firmwareType">
|
||||
|
|
|
@ -52,6 +52,7 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
selectedClients: any[] = [];
|
||||
selectedModelClient: any = null;
|
||||
partitionCode: string = '';
|
||||
generatedInstructions: string = '';
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
|
@ -426,7 +427,8 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
width: '800px',
|
||||
data: {
|
||||
scope: this.runScriptContext.type,
|
||||
organizationalUnit: this.runScriptContext['@id']
|
||||
organizationalUnit: this.runScriptContext['@id'],
|
||||
source: 'assistant'
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -473,4 +475,39 @@ export class PartitionAssistantComponent implements OnInit{
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateInstructions(): void {
|
||||
if (!this.selectedDisk || !this.selectedDisk.partitions) {
|
||||
this.generatedInstructions = 'No hay particiones configuradas para generar instrucciones.';
|
||||
return;
|
||||
}
|
||||
|
||||
const diskNumber = this.selectedDisk.diskNumber;
|
||||
const partitionTable = this.partitionCode || 'MSDOS';
|
||||
|
||||
let instructions = `ogCreatePartitionTable ${diskNumber} ${partitionTable}\n`;
|
||||
instructions += `ogEcho log session "[0] $MSG_HELP_ogCreatePartitions"\n`;
|
||||
instructions += `ogEcho session "[10] $MSG_HELP_ogUnmountAll ${diskNumber}"\n`;
|
||||
instructions += `ogUnmountAll ${diskNumber} 2>/dev/null\n`;
|
||||
instructions += `ogUnmountCache\n`;
|
||||
instructions += `ogEcho session "[30] $MSG_HELP_ogUpdatePartitionTable ${diskNumber}"\n`;
|
||||
instructions += `ogDeletePartitionTable ${diskNumber}\n`;
|
||||
instructions += `ogUpdatePartitionTable ${diskNumber}\n`;
|
||||
|
||||
this.selectedDisk.partitions.forEach((partition: { removed: any; partitionNumber: any; partitionCode: any; filesystem: any; size: any; format: any; }, index: any) => {
|
||||
if (partition.removed) return;
|
||||
|
||||
const partNumber = partition.partitionNumber;
|
||||
const partType = partition.partitionCode;
|
||||
const fs = partition.filesystem;
|
||||
const size = partition.size;
|
||||
const shouldFormat = partition.format ? 'yes' : 'no';
|
||||
|
||||
instructions += `ogCreatePartition ${diskNumber} ${partNumber} ${partType} ${fs} ${size}MB ${shouldFormat}\n`;
|
||||
});
|
||||
|
||||
instructions += `ogExecAndLog command session ogListPartitions ${diskNumber}\n`;
|
||||
|
||||
this.generatedInstructions = instructions;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -203,7 +203,8 @@ export class RunScriptAssistantComponent implements OnInit{
|
|||
width: '800px',
|
||||
data: {
|
||||
scope: this.runScriptContext.type,
|
||||
organizationalUnit: this.runScriptContext['@id']
|
||||
organizationalUnit: this.runScriptContext['@id'],
|
||||
source: 'assistant'
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -11,11 +11,12 @@
|
|||
</div>
|
||||
<div class="groups-button-row">
|
||||
<div joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}" style="display: flex; gap: 15px;">
|
||||
<button class="action-button" (click)="addOU($event)"
|
||||
<button class="action-button" (click)="addOU($event)" *ngIf="auth.userCategory !== 'ou-minimal'"
|
||||
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'newOrganizationalUnitButton' | translate }}
|
||||
</button>
|
||||
<button class="action-button" [matMenuTriggerFor]="menuClients">{{ 'newClientButton' | translate }}</button>
|
||||
<button class="action-button" [matMenuTriggerFor]="menuClients" *ngIf="auth.userCategory !== 'ou-minimal'">{{
|
||||
'newClientButton' | translate }}</button>
|
||||
<mat-menu #menuClients="matMenu">
|
||||
<button mat-menu-item (click)="addClient($event)">{{ 'newSingleClientButton' | translate }}</button>
|
||||
<button mat-menu-item (click)="addMultipleClients($event)">{{ 'newMultipleClientButton' | translate
|
||||
|
@ -180,23 +181,23 @@
|
|||
<mat-icon>map</mat-icon>
|
||||
<span>{{ 'roomMap' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addClient($event, selectedNode)">
|
||||
<button mat-menu-item (click)="addClient($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'newSingleClientButton' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addMultipleClients($event, selectedNode)">
|
||||
<button mat-menu-item (click)="addMultipleClients($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>playlist_add</mat-icon>
|
||||
<span>{{ 'newMultipleClientButton' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="addOU($event, selectedNode)">
|
||||
<button mat-menu-item (click)="addOU($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>account_tree</mat-icon>
|
||||
<span>{{ 'addOrganizationalUnit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onEditNode($event, selectedNode)">
|
||||
<button mat-menu-item (click)="onEditNode($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)">
|
||||
<button mat-menu-item (click)="onDeleteClick($event, selectedNode)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
|
@ -224,11 +225,16 @@
|
|||
<strong>{{ selectedNode?.name }}</strong>
|
||||
</span>
|
||||
<div class="view-type-container">
|
||||
<button class="action-button" [disabled]="selection.selected.length === 0" (click)="changeParent($event)"
|
||||
matTooltip="{{ 'moveClientsTooltip' | translate }}" matTooltipShowDelay="1000">
|
||||
{{ 'changeOU' | translate }}
|
||||
</button>
|
||||
<div joyrideStep="executeCommandStep" text="{{ 'executeCommandStepText' | translate }}">
|
||||
<app-execute-command [clientData]="selection.selected" [buttonType]="'text'"
|
||||
[buttonText]="'ejecutarComandos' | translate" [disabled]="selection.selected.length === 0"
|
||||
[runScriptContext]="getRunScriptContext(selection.selected)"></app-execute-command>
|
||||
</div>
|
||||
|
||||
<mat-button-toggle-group name="viewType" aria-label="View Type" [hideSingleSelectionIndicator]="true"
|
||||
(change)="toggleView($event.value)" joyrideStep="tabsStep" text="{{ 'tabsStepText' | translate }}">
|
||||
<mat-button-toggle value="list" [disabled]="currentView === 'list'">
|
||||
|
@ -287,7 +293,7 @@
|
|||
</span>
|
||||
|
||||
<mat-menu #clientMenu="matMenu">
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
|
@ -304,7 +310,7 @@
|
|||
<mat-icon>list_alt</mat-icon>
|
||||
<span>{{ 'procedimientosCliente' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
|
@ -422,7 +428,7 @@
|
|||
[runScriptContext]="getRunScriptContext([client])">
|
||||
</app-execute-command>
|
||||
<mat-menu #clientMenu="matMenu">
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
|
||||
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>{{ 'edit' | translate }}</span>
|
||||
</button>
|
||||
|
@ -438,7 +444,7 @@
|
|||
<mat-icon>list_alt</mat-icon>
|
||||
<span>{{ 'procedimientosCliente' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)">
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
</button>
|
||||
|
@ -469,4 +475,4 @@
|
|||
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -29,6 +29,8 @@ 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';
|
||||
import { ClientTaskLogsComponent } from '../task-logs/client-task-logs/client-task-logs.component';
|
||||
import {ChangeParentComponent} from "./shared/change-parent/change-parent.component";
|
||||
import { AuthService } from '@services/auth.service';
|
||||
|
||||
enum NodeType {
|
||||
OrganizationalUnit = 'organizational-unit',
|
||||
|
@ -114,6 +116,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
private joyrideService: JoyrideService,
|
||||
private breakpointObserver: BreakpointObserver,
|
||||
private toastr: ToastrService,
|
||||
public auth: AuthService,
|
||||
private configService: ConfigService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
|
@ -132,7 +135,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
|
||||
this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
|
||||
this.currentView = localStorage.getItem('groupsView') || 'list';
|
||||
this.currentView = this.auth.groupsView || 'list';
|
||||
}
|
||||
|
||||
|
||||
|
@ -864,12 +867,27 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
changeParent(event: MouseEvent, ): void {
|
||||
event.stopPropagation();
|
||||
|
||||
const dialogRef = this.dialog.open(ChangeParentComponent, {
|
||||
data: { clients: this.selection.selected },
|
||||
width: '700px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.refreshData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openClientTaskLogs(event: MouseEvent, client: Client): void {
|
||||
event.stopPropagation();
|
||||
|
||||
this.dialog.open(ClientTaskLogsComponent, {
|
||||
width: '1200px',
|
||||
data: {client}
|
||||
data: { client }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
mat-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.selected-list ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.selected-item {
|
||||
display: flex;
|
||||
justify-content: space-between; /* Alinea texto a la izquierda y botón a la derecha */
|
||||
align-items: center; /* Centra verticalmente */
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.selected-item button {
|
||||
margin-left: 10px;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<h2 mat-dialog-title>Mover clientes a:</h2>
|
||||
|
||||
<mat-dialog-content>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Seleccione aula</mat-label>
|
||||
<mat-select [(ngModel)]="newOU">
|
||||
<mat-option *ngFor="let unit of units" [value]="unit">{{ unit.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
|
||||
<div class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">Cancelar</button>
|
||||
<button class="submit-button" (click)="save()" [disabled]="!newOU">Continuar</button>
|
||||
</div>
|
|
@ -0,0 +1,46 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ChangeParentComponent } from './change-parent.component';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('ChangeParentComponent', () => {
|
||||
let component: ChangeParentComponent;
|
||||
let fixture: ComponentFixture<ChangeParentComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = { apiUrl: 'http://mock-api-url' };
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ChangeParentComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
FormsModule,
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: {} },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { clients: [] } },
|
||||
{ provide: ToastrService, useValue: { success: () => { }, error: () => { } } },
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChangeParentComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
import {Component, Inject, OnInit} from '@angular/core';
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {Router} from "@angular/router";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-parent',
|
||||
templateUrl: './change-parent.component.html',
|
||||
styleUrl: './change-parent.component.css'
|
||||
})
|
||||
export class ChangeParentComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
loading: boolean = true;
|
||||
units: any[] = [];
|
||||
newOU: any;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<ChangeParentComponent>,
|
||||
private toastService: ToastrService,
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: { clients: any}
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true;
|
||||
this.loadUnits();
|
||||
}
|
||||
|
||||
loadUnits() {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=500`).subscribe(
|
||||
response => {
|
||||
this.units = response['hydra:member'];
|
||||
this.loading = false;
|
||||
},
|
||||
error => console.error('Error fetching organizational units:', error)
|
||||
);
|
||||
}
|
||||
|
||||
save() {
|
||||
this.http.post<any>(`${this.baseUrl}/clients/change-organizational-unit`, {
|
||||
clients: this.data.clients.map((client: any) => client['@id']),
|
||||
organizationalUnit: this.newOU['@id']
|
||||
}).subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Parent changed successfully');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: error => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
|
@ -3,10 +3,10 @@ import { Component, signal } from '@angular/core';
|
|||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { GlobalStatusComponent } from '../global-status/global-status.component'
|
||||
import { AuthService } from '@services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
|
@ -20,8 +20,6 @@ export class LoginComponent {
|
|||
};
|
||||
errorMessage: string = '';
|
||||
isLoading: boolean = false;
|
||||
decodedToken: any;
|
||||
|
||||
baseUrl: string;
|
||||
|
||||
constructor(
|
||||
|
@ -29,6 +27,7 @@ export class LoginComponent {
|
|||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
private auth: AuthService,
|
||||
private translateService: TranslateService,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
|
@ -62,13 +61,8 @@ export class LoginComponent {
|
|||
next: (res: any) => {
|
||||
if (res.token) {
|
||||
localStorage.setItem('loginToken', res.token);
|
||||
localStorage.setItem('refreshToken', res.refreshToken);
|
||||
localStorage.setItem('username', this.loginObj.username);
|
||||
|
||||
this.decodedToken = jwtDecode(res.token);
|
||||
localStorage.setItem('groupsView', this.decodedToken.groupsView);
|
||||
|
||||
this.openSnackBar(false, 'Bienvenido ' + this.loginObj.username);
|
||||
this.auth.refresh();
|
||||
this.openSnackBar(false, 'Bienvenido ' + this.auth.username);
|
||||
this.router.navigateByUrl('/groups');
|
||||
this.dialog.open(GlobalStatusComponent, {
|
||||
width: '45vw',
|
||||
|
|
|
@ -30,8 +30,8 @@ export class ManageRepositoryComponent implements OnInit {
|
|||
this.imageForm = this.fb.group({
|
||||
name: [null, Validators.required],
|
||||
ip: [null],
|
||||
sshPort: [null],
|
||||
user: [null],
|
||||
sshPort: ['22'],
|
||||
user: ['opengnsys'],
|
||||
comments: [null],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -44,15 +44,10 @@ table {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
margin: 1.5rem 0rem 0.5rem 0rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
|
@ -63,6 +58,11 @@ table {
|
|||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-date {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
|
|
@ -47,13 +47,29 @@
|
|||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-date">
|
||||
<mat-label>Desde</mat-label>
|
||||
<input matInput [matDatepicker]="fromPicker" [(ngModel)]="filters['startDate']"
|
||||
(dateChange)="onDateFilterChange()" [max]="today">
|
||||
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #fromPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-date">
|
||||
<mat-label>Hasta</mat-label>
|
||||
<input matInput [matDatepicker]="toPicker" [(ngModel)]="filters['endDate']" (dateChange)="onDateFilterChange()"
|
||||
[max]="today">
|
||||
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #toPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="traces" class="mat-elevation-z8" joyrideStep="tracesTableStep"
|
||||
text="{{ 'tracesTableStepText' | translate }}">
|
||||
<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">
|
||||
|
@ -69,7 +85,8 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
<ng-template #statusChip>
|
||||
<div class="status-progress-flex">
|
||||
<div class="status-progress-flex" joyrideStep="tracesProgressStep"
|
||||
text="{{ 'tracesProgressStepText' | translate }}">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-failed': trace.status === 'failed',
|
||||
'chip-success': trace.status === 'success',
|
||||
|
@ -128,9 +145,10 @@
|
|||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<ng-container matColumnDef="information">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'informationLabel' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let trace" style="text-align: center;">
|
||||
<td mat-cell *matCellDef="let trace" style="text-align: center;" joyrideStep="tracesInfoStep"
|
||||
text="{{ 'tracesInfoStepText' | translate }}">
|
||||
<button mat-icon-button color="primary" [disabled]="!trace.input" (click)="openInputModal(trace.input)">
|
||||
<mat-icon>
|
||||
<span class="material-symbols-outlined">
|
||||
|
|
|
@ -13,6 +13,7 @@ import { LoadingComponent } from 'src/app/shared/loading/loading.component';
|
|||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
describe('ClientTaskLogsComponent', () => {
|
||||
let component: ClientTaskLogsComponent;
|
||||
|
@ -32,6 +33,7 @@ describe('ClientTaskLogsComponent', () => {
|
|||
TranslateModule.forRoot(),
|
||||
MatFormFieldModule,
|
||||
MatPaginatorModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
|
|
|
@ -35,6 +35,7 @@ export class ClientTaskLogsComponent implements OnInit {
|
|||
mode: ProgressBarMode = 'buffer';
|
||||
progress = 0;
|
||||
bufferValue = 0;
|
||||
today = new Date();
|
||||
|
||||
filteredCommands2 = Object.keys(COMMAND_TYPES).map(key => ({
|
||||
name: key,
|
||||
|
@ -69,9 +70,9 @@ export class ClientTaskLogsComponent implements OnInit {
|
|||
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
},
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'information'];
|
||||
|
||||
filters: { [key: string]: string } = {};
|
||||
filters: { [key: string]: any } = {};
|
||||
filteredCommands!: Observable<any[]>;
|
||||
commandControl = new FormControl();
|
||||
|
||||
|
@ -134,9 +135,7 @@ export class ClientTaskLogsComponent implements OnInit {
|
|||
}
|
||||
|
||||
onOptionCommandSelected(selectedCommand: any): void {
|
||||
this.filters['command'] = selectedCommand.id || selectedCommand.uuid;
|
||||
console.log('Comando seleccionado:', selectedCommand);
|
||||
console.log('Valor que se usará para el filtro:', selectedCommand.name);
|
||||
this.filters['command'] = selectedCommand.name;
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
|
@ -145,6 +144,21 @@ export class ClientTaskLogsComponent implements OnInit {
|
|||
this.loadTraces();
|
||||
}
|
||||
|
||||
onDateFilterChange(): void {
|
||||
const start = this.filters['startDate'];
|
||||
const end = this.filters['endDate'];
|
||||
|
||||
if (!start || !end) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (start && end && start > end) {
|
||||
this.toastService.warning('La fecha de inicio no puede ser mayor que la fecha de fin');
|
||||
return;
|
||||
}
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
openInputModal(inputData: any): void {
|
||||
this.dialog.open(InputDialogComponent, {
|
||||
width: '70vw',
|
||||
|
@ -186,18 +200,22 @@ export class ClientTaskLogsComponent implements OnInit {
|
|||
|
||||
this.loading = true;
|
||||
|
||||
let params = new HttpParams()
|
||||
.set('client.id', clientId)
|
||||
.set('page', (this.page + 1).toString())
|
||||
.set('itemsPerPage', this.itemsPerPage.toString());
|
||||
const params: any = {
|
||||
'client.id': clientId,
|
||||
page: this.page + 1,
|
||||
itemsPerPage: this.itemsPerPage,
|
||||
...this.filters
|
||||
};
|
||||
|
||||
if (this.filters['command']) {
|
||||
params = params.set('command.uuid', this.filters['command']);
|
||||
if (params['startDate']) {
|
||||
params['executedAt[after]'] = this.datePipe.transform(params['startDate'], 'yyyy-MM-dd');
|
||||
delete params['startDate'];
|
||||
}
|
||||
|
||||
if (this.filters['status']) {
|
||||
params = params.set('status', this.filters['status']);
|
||||
if (params['endDate']) {
|
||||
params['executedAt[before]'] = this.datePipe.transform(params['endDate'], 'yyyy-MM-dd');
|
||||
delete params['endDate'];
|
||||
}
|
||||
console.log('🌐 GET', `${this.baseUrl}/traces`, params);
|
||||
|
||||
const url = `${this.baseUrl}/traces`;
|
||||
|
||||
|
@ -233,6 +251,7 @@ export class ClientTaskLogsComponent implements OnInit {
|
|||
clientSearchCommandInput.value = null;
|
||||
clientSearchStatusInput.value = null;
|
||||
this.filters = {};
|
||||
this.page = 0;
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
|
@ -284,7 +303,8 @@ export class ClientTaskLogsComponent implements OnInit {
|
|||
'tracesTitleStep',
|
||||
'resetFiltersStep',
|
||||
'filtersStep',
|
||||
'tracesTableStep',
|
||||
'tracesProgressStep',
|
||||
'tracesInfoStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
|
|
|
@ -73,8 +73,7 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="traces" class="mat-elevation-z8" joyrideStep="tracesTableStep"
|
||||
text="{{ 'tracesTableStepText' | translate }}">
|
||||
<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">
|
||||
|
@ -90,7 +89,8 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
<ng-template #statusChip>
|
||||
<div class="status-progress-flex">
|
||||
<div class="status-progress-flex" joyrideStep="tracesProgressStep"
|
||||
text="{{ 'tracesProgressStepText' | translate }}">
|
||||
<mat-chip [ngClass]="{
|
||||
'chip-failed': trace.status === 'failed',
|
||||
'chip-success': trace.status === 'success',
|
||||
|
@ -149,9 +149,10 @@
|
|||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<ng-container matColumnDef="information">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'informationLabel' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let trace" style="text-align: center;">
|
||||
<td mat-cell *matCellDef="let trace" style="text-align: center;" joyrideStep="tracesInfoStep"
|
||||
text="{{ 'tracesInfoStepText' | translate }}">
|
||||
<button mat-icon-button color="primary" [disabled]="!trace.input || trace.input.length === 0"
|
||||
(click)="openInputModal(trace.input)">
|
||||
<mat-icon>
|
||||
|
|
|
@ -75,7 +75,7 @@ export class TaskLogsComponent implements OnInit {
|
|||
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
|
||||
},
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'information'];
|
||||
|
||||
filters: { [key: string]: string } = {};
|
||||
filteredClients!: Observable<any[]>;
|
||||
|
@ -326,7 +326,8 @@ export class TaskLogsComponent implements OnInit {
|
|||
'tracesTitleStep',
|
||||
'resetFiltersStep',
|
||||
'filtersStep',
|
||||
'tracesTableStep',
|
||||
'tracesProgressStep',
|
||||
'tracesInfoStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanActivateFn } from '@angular/router';
|
||||
|
||||
import { roleGuard } from './role.guard';
|
||||
|
||||
describe('roleGuard', () => {
|
||||
const executeGuard: CanActivateFn = (...guardParameters) =>
|
||||
TestBed.runInInjectionContext(() => roleGuard(...guardParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeGuard).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { AuthService } from '@services/auth.service';
|
||||
|
||||
export const roleGuard: CanActivateFn = (route, state) => {
|
||||
const auth = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
const currentRole = auth.getCurrentRole();
|
||||
|
||||
const allowedRoles = route.data?.['allowedRoles'] as string[];
|
||||
|
||||
if (allowedRoles?.includes(currentRole)) {
|
||||
return true;
|
||||
} else {
|
||||
router.navigate(['/groups']);
|
||||
return false;
|
||||
}
|
||||
};
|
|
@ -18,17 +18,17 @@
|
|||
{{ 'GlobalStatus' | translate }}
|
||||
</button>
|
||||
|
||||
<button class="ordinary-button" *ngIf="isSuperAdmin" [matMenuTriggerFor]="menu"
|
||||
<button class="ordinary-button" *ngIf="auth.userCategory === 'super-admin'" [matMenuTriggerFor]="menu"
|
||||
matTooltip="Gestión de usuarios y roles de la aplicación" matTooltipShowDelay="1000">
|
||||
{{ 'Administration' | translate }}
|
||||
</button>
|
||||
|
||||
<button class="ordinary-button" *ngIf="!isSuperAdmin" (click)="editUser()"
|
||||
<button class="ordinary-button" (click)="editUser()"
|
||||
matTooltip="Editar tu información de usuario" matTooltipShowDelay="1000">
|
||||
{{ 'changePassword' | translate }}
|
||||
</button>
|
||||
|
||||
<button class="logout-button" routerLink="/auth/login" matTooltip="Cerrar sesión y salir de la aplicación"
|
||||
<button class="logout-button" (click)="logOut()" matTooltip="Cerrar sesión y salir de la aplicación"
|
||||
matTooltipShowDelay="1000">
|
||||
{{ 'logout' | translate }}
|
||||
</button>
|
||||
|
@ -47,10 +47,10 @@
|
|||
<button mat-menu-item (click)="showGlobalStatus()">
|
||||
{{ 'GlobalStatus' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item *ngIf="isSuperAdmin" [matMenuTriggerFor]="menu">
|
||||
<button mat-menu-item *ngIf="auth.userCategory === 'super-admin'" [matMenuTriggerFor]="menu">
|
||||
{{ 'Administration' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item *ngIf="!isSuperAdmin" (click)="editUser()">
|
||||
<button mat-menu-item *ngIf="auth.userCategory === 'super-admin'" (click)="editUser()">
|
||||
{{ 'changePassword' | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
@ -65,7 +65,7 @@
|
|||
matTooltipShowDelay="1000">
|
||||
{{ 'labelUsers' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/user-groups" matTooltip="Gestionar roles de usuario" matTooltipShowDelay="1000">
|
||||
<button mat-menu-item routerLink="/roles" matTooltip="Gestionar roles de usuario" matTooltipShowDelay="1000">
|
||||
{{ 'labelRoles' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item routerLink="/env-vars" matTooltip="Gestionar variables de entorno" matTooltipShowDelay="1000">
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { ChangePasswordModalComponent } from '../../components/admin/users/users/change-password-modal/change-password-modal.component';
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { GlobalStatusComponent } from 'src/app/components/global-status/global-status.component';
|
||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { AuthService } from '@services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
|
@ -16,39 +16,26 @@ export class HeaderComponent implements OnInit {
|
|||
isSmallScreen: boolean = false;
|
||||
|
||||
@Output() toggleSidebar = new EventEmitter<void>();
|
||||
private decodedToken: any;
|
||||
private username: any;
|
||||
|
||||
onToggleSidebar() {
|
||||
this.toggleSidebar.emit();
|
||||
}
|
||||
|
||||
constructor(public dialog: MatDialog, private breakpointObserver: BreakpointObserver) { }
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
public auth: AuthService,
|
||||
private breakpointObserver: BreakpointObserver
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
const token = localStorage.getItem('loginToken');
|
||||
if (token) {
|
||||
try {
|
||||
this.decodedToken = jwtDecode(token);
|
||||
this.isSuperAdmin = this.decodedToken.roles.includes('ROLE_SUPER_ADMIN');
|
||||
localStorage.setItem('isSuperAdmin', String(this.isSuperAdmin));
|
||||
this.username = this.decodedToken.username;
|
||||
} catch (error) {
|
||||
console.error('Error decoding JWT:', error);
|
||||
}
|
||||
}
|
||||
this.breakpointObserver.observe(['(max-width: 576px)']).subscribe((result) => {
|
||||
this.isSmallScreen = result.matches;
|
||||
})
|
||||
}
|
||||
|
||||
ngDoCheck(): void {
|
||||
this.isSuperAdmin = localStorage.getItem('isSuperAdmin') === 'true';
|
||||
}
|
||||
|
||||
editUser() {
|
||||
const dialogRef = this.dialog.open(ChangePasswordModalComponent, {
|
||||
data: { user: this.decodedToken.username, uuid: this.decodedToken.uuid },
|
||||
data: { user: this.auth.username, uuid: this.auth.uuid },
|
||||
width: '400px',
|
||||
});
|
||||
}
|
||||
|
@ -56,7 +43,11 @@ export class HeaderComponent implements OnInit {
|
|||
showGlobalStatus() {
|
||||
this.dialog.open(GlobalStatusComponent, {
|
||||
width: '45vw',
|
||||
height: '80vh',
|
||||
height: '80vh',
|
||||
})
|
||||
}
|
||||
|
||||
logOut() {
|
||||
this.auth.logout();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<mat-nav-list class="sidebar-content">
|
||||
<div disabled class="user-info">
|
||||
<div class="user-info-wrapper" matTooltipShowDelay="1000">
|
||||
<img ngSrc="assets/images/logo.png" alt="Logo" class="user-logo" height="500" width="500"/>
|
||||
<img ngSrc="assets/images/logo.png" alt="Logo" class="user-logo" height="500" width="500" />
|
||||
<span class="user-logged">
|
||||
{{ 'welcomeUser' | translate:{username: username} }}
|
||||
{{ 'welcomeUser' | translate:{username: username} }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,7 +18,7 @@
|
|||
</mat-list-item>
|
||||
|
||||
<mat-list-item (click)="toggleCommandSub()" matTooltip="{{ 'TOOLTIP_ACTIONS' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">playlist_play</mat-icon>
|
||||
<span>{{ 'actions' | translate }}</span>
|
||||
|
@ -28,7 +28,7 @@
|
|||
<!-- Submenu items for commands -->
|
||||
<mat-nav-list *ngIf="showCommandSub" style="padding-left: 20px;">
|
||||
<mat-list-item routerLink="/commands" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_COMMANDS' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">chevron_right</mat-icon>
|
||||
<span>{{ 'commands' | translate }}</span>
|
||||
|
@ -44,7 +44,7 @@
|
|||
</mat-list-item>
|
||||
-->
|
||||
<mat-list-item routerLink="/commands-task" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_TASKS' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">chevron_right</mat-icon>
|
||||
<span>{{ 'tasks' | translate }}</span>
|
||||
|
@ -53,15 +53,15 @@
|
|||
</mat-nav-list>
|
||||
|
||||
<mat-list-item routerLink="/subnets" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_SUBNETS' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">lan</mat-icon>
|
||||
<span> {{ 'subnets' | translate }}</span>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item (click)="toggleOgBootSub()" matTooltip="{{ 'TOOLTIP_BOOT' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
<mat-list-item (click)="toggleOgBootSub()" matTooltip="{{ 'TOOLTIP_BOOT' | translate }}" matTooltipShowDelay="1000"
|
||||
*ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">desktop_windows</mat-icon>
|
||||
<span>{{ 'boot' | translate }}</span>
|
||||
|
@ -94,7 +94,7 @@
|
|||
</mat-nav-list>
|
||||
|
||||
<mat-list-item routerLink="/calendars" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_CALENDARS' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">calendar_month</mat-icon>
|
||||
<span>{{ 'calendars' | translate }}</span>
|
||||
|
@ -102,7 +102,7 @@
|
|||
</mat-list-item>
|
||||
|
||||
<mat-list-item (click)="toggleSoftwareSub()" matTooltip="{{ 'TOOLTIP_SOFTWARE' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">terminal</mat-icon>
|
||||
<span>{{ 'software' | translate }}</span>
|
||||
|
@ -135,7 +135,8 @@
|
|||
</mat-nav-list>
|
||||
|
||||
<mat-list-item routerLink="/repositories" (click)="onItemClick()"
|
||||
matTooltip="{{ 'TOOLTIP_REPOSITORIES' | translate }}" matTooltipShowDelay="1000">
|
||||
matTooltip="{{ 'TOOLTIP_REPOSITORIES' | translate }}" matTooltipShowDelay="1000"
|
||||
*ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">warehouse</mat-icon>
|
||||
<span>{{ 'repositories' | translate }}</span>
|
||||
|
@ -143,10 +144,10 @@
|
|||
</mat-list-item>
|
||||
|
||||
<mat-list-item routerLink="/menus" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_MENUS' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">list</mat-icon>
|
||||
<span>{{ 'menus' | translate }}</span>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
</mat-nav-list>
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { AuthService } from '@services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
|
@ -12,9 +12,7 @@ export class SidebarComponent {
|
|||
@Input() sidebarMode: 'side' | 'over' = 'side';
|
||||
@Output() closeSidebar: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
isSuperAdmin: boolean = false;
|
||||
username: string = "";
|
||||
decodedToken: any = "";
|
||||
username: string | null = "";
|
||||
showOgBootSub: boolean = false;
|
||||
showOgDhcpSub: boolean = false;
|
||||
showCommandSub: boolean = false;
|
||||
|
@ -39,19 +37,9 @@ export class SidebarComponent {
|
|||
}
|
||||
}
|
||||
|
||||
constructor(public dialog: MatDialog) {}
|
||||
constructor(public dialog: MatDialog, public auth: AuthService,) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const token = localStorage.getItem('loginToken');
|
||||
if (token) {
|
||||
try {
|
||||
this.decodedToken = jwtDecode(token);
|
||||
this.isSuperAdmin = this.decodedToken.roles.includes('ROLE_SUPER_ADMIN');
|
||||
localStorage.setItem('isSuperAdmin', String(this.isSuperAdmin));
|
||||
this.username = this.decodedToken.username;
|
||||
} catch (error) {
|
||||
console.error('Error decoding JWT:', error);
|
||||
}
|
||||
}
|
||||
this.username = this.auth.username
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
export type UserCategory =
|
||||
| 'super-admin'
|
||||
| 'ou-admin'
|
||||
| 'ou-operator'
|
||||
| 'ou-minimal'
|
||||
| 'user';
|
||||
|
||||
interface JwtPayload {
|
||||
roles: string[];
|
||||
username: string;
|
||||
id: number;
|
||||
uuid: string;
|
||||
groupsView: string;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthService {
|
||||
private tokenPayload: JwtPayload | null = null;
|
||||
|
||||
constructor(private router: Router) {
|
||||
this.loadToken();
|
||||
}
|
||||
|
||||
/** Intenta leer y decodificar el token de localStorage */
|
||||
private loadToken() {
|
||||
const token = localStorage.getItem('loginToken');
|
||||
if (!token) return;
|
||||
try {
|
||||
this.tokenPayload = jwtDecode<JwtPayload>(token);
|
||||
} catch (e) {
|
||||
console.error('Error decoding JWT', e);
|
||||
this.tokenPayload = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Fuerza recarga del token (por si cambió en localStorage) */
|
||||
public refresh() {
|
||||
this.loadToken();
|
||||
}
|
||||
|
||||
/** Roles básicos que aparecen en el token */
|
||||
private get roles(): string[] {
|
||||
return this.tokenPayload?.roles || [];
|
||||
}
|
||||
|
||||
get isSuperAdmin(): boolean {
|
||||
return this.roles.includes('ROLE_SUPER_ADMIN');
|
||||
}
|
||||
|
||||
get isOUAdmin(): boolean {
|
||||
return this.roles.includes('ROLE_ORGANIZATIONAL_UNIT_ADMIN');
|
||||
}
|
||||
|
||||
get isOUOperator(): boolean {
|
||||
return this.roles.includes('ROLE_ORGANIZATIONAL_UNIT_OPERATOR');
|
||||
}
|
||||
|
||||
get isOUMinimal(): boolean {
|
||||
return this.roles.includes('ROLE_ORGANIZATIONAL_UNIT_MINIMAL');
|
||||
}
|
||||
|
||||
/**
|
||||
* Categoría única de usuario, según prioridades:
|
||||
* - super-admin
|
||||
* - ou-admin
|
||||
* - ou-operator
|
||||
* - ou-minimal
|
||||
* - user (fallback)
|
||||
*/
|
||||
get userCategory(): UserCategory {
|
||||
if (this.isSuperAdmin) return 'super-admin';
|
||||
if (this.isOUAdmin) return 'ou-admin';
|
||||
if (this.isOUOperator) return 'ou-operator';
|
||||
if (this.isOUMinimal) return 'ou-minimal';
|
||||
return 'user';
|
||||
}
|
||||
|
||||
/** Nombre de usuario */
|
||||
get username(): string | null {
|
||||
return this.tokenPayload?.username || null;
|
||||
}
|
||||
|
||||
/** Uuid del usuario*/
|
||||
get uuid(): any | null {
|
||||
return this.tokenPayload?.uuid || null;
|
||||
}
|
||||
|
||||
/** groupsView del usuario */
|
||||
get groupsView(): string | null {
|
||||
return this.tokenPayload?.groupsView || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve el rol principal del usuario según prioridad.
|
||||
*/
|
||||
getCurrentRole(): string {
|
||||
if (this.isSuperAdmin) return 'super-admin';
|
||||
if (this.isOUAdmin) return 'ou-admin';
|
||||
if (this.isOUOperator) return 'ou-operator';
|
||||
if (this.isOUMinimal) return 'ou-minimal';
|
||||
return 'user';
|
||||
}
|
||||
|
||||
/** Logout: limpia tokens y redirige al login */
|
||||
logout(): void {
|
||||
localStorage.removeItem('loginToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('isSuperAdmin');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('groupsView');
|
||||
localStorage.removeItem('language');
|
||||
this.tokenPayload = null;
|
||||
this.router.navigate(['/auth/login']);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
.main-container {
|
||||
height: 100dvh;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 4em;
|
||||
border: 1px solid #3f51b5;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 25px;
|
||||
font-weight: 500;
|
||||
}
|
|
@ -1 +1,7 @@
|
|||
<p>page-not-found works!</p>
|
||||
<main>
|
||||
<article class="main-container">
|
||||
<div class="content-container">
|
||||
<span class="message">Page not found</span>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
|
@ -517,6 +517,8 @@
|
|||
"hardwareInventory": "Hardware Inventory",
|
||||
"runScript": "Run Script"
|
||||
},
|
||||
"changeOU": "Change Organizational Unit",
|
||||
"moveClientsTooltip": "Move clients to another organizational unit",
|
||||
"remoteAccess": "Remote access available",
|
||||
"noRemoteAccess": "Remote access not available",
|
||||
"capacityWarning": "The capacity cannot be negative",
|
||||
|
@ -530,5 +532,6 @@
|
|||
"isDefaultLabel": "Default",
|
||||
"tracesTitleStepText": "In this screen, you can see the execution traces of each client, with its id, command, real-time status, date and actions to be performed.",
|
||||
"filtersStepText": "Here you can see the different filters to apply to the table information.",
|
||||
"tracesTableStepText": "This is the table with the execution traces updated in real time."
|
||||
}
|
||||
"tracesProgressStepText": "Here you can see the execution status updated in real time.",
|
||||
"tracesInfoStepText": "Here you can consult detailed information about the specific trace."
|
||||
}
|
||||
|
|
|
@ -518,6 +518,8 @@
|
|||
"hardwareInventory": "Inventario Hardware",
|
||||
"runScript": "Ejecutar script"
|
||||
},
|
||||
"changeOU": "Mover clientes",
|
||||
"moveClientsTooltip": "Mover clientes a otra unidad organizativa",
|
||||
"remoteAccess": "Disponible acceso remoto",
|
||||
"noRemoteAccess": "No disponible acceso remoto",
|
||||
"capacityWarning": "El aforo no puede ser",
|
||||
|
@ -533,5 +535,6 @@
|
|||
"isDefaultLabel": "Por defecto",
|
||||
"tracesTitleStepText": "En esta pantalla, puedes ver las trazas de ejecución de cada cliente, con su id, comando, estado en tiempo real, fecha y acciones a realizar.",
|
||||
"filtersStepText": "Aquí puedes ver los diferentes filtros que aplicar a la información de la tabla.",
|
||||
"tracesTableStepText": "Esta es la tabla con las trazas de ejecución actualizadas en tiempo real."
|
||||
"tracesProgressStepText": "Aquí puedes ver el estado de ejecución actualizado en tiempo real.",
|
||||
"tracesInfoStepText": "Aquí puedes consultar información detallada de la traza en concreto."
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue